DynamoDB Design Patterns
DynamoDB rewards careful upfront design. Unlike SQL databases where you write queries first and build indexes later, DynamoDB requires you to understand your access patterns before you design your tables. The patterns in this topic represent proven approaches used by engineers building large-scale production systems.
Core Design Philosophy
Every DynamoDB design starts with one question: what queries does my application need to run? List every access pattern before writing a single line of schema. Your primary key, sort key, and indexes must collectively satisfy all of those patterns — efficiently.
Pattern 1: Single Table Design
Single Table Design stores multiple entity types in one DynamoDB table. Instead of a separate table for Users, Orders, and Products, everything lives in one table. Items are differentiated by key prefixes and a type attribute.
Why Use It
SQL databases encourage many tables with joins. DynamoDB does not support joins across tables — fetching related items from multiple tables requires multiple round trips. A single table lets you retrieve all related data in one query, reducing latency and cost.
Diagram: Single Table Design for an E-Commerce App
TABLE: AppData (PK: PK, SK: SK) ┌──────────────────────┬──────────────────────────┬─────────────┬──────────┐ │ PK │ SK │ Type │ Data │ ├──────────────────────┼──────────────────────────┼─────────────┼──────────┤ │ USER#U001 │ PROFILE │ User │ Name... │ │ USER#U001 │ ORDER#2024-01-05 │ Order │ Laptop...| │ USER#U001 │ ORDER#2024-03-12 │ Order │ Mouse... │ │ PRODUCT#P100 │ DETAILS │ Product │ Price... │ │ PRODUCT#P100 │ REVIEW#R001 │ Review │ 5 stars │ │ PRODUCT#P100 │ REVIEW#R002 │ Review │ 4 stars │ └──────────────────────┴──────────────────────────┴─────────────┴──────────┘ Query 1: Get user U001 profile + all their orders PK = "USER#U001" → Returns profile + all orders in one shot Query 2: Get product P100 details + all its reviews PK = "PRODUCT#P100" → Returns product + all reviews in one shot
Both queries fetch everything needed in a single DynamoDB operation. No joins, no multiple round trips.
Naming Convention for Keys
The generic attribute names PK and SK are deliberate. In a single table, the same key attributes hold different logical values for different entity types. Using generic names avoids confusion.
Pattern 2: Composite Sort Key Design
Composite sort keys combine multiple data fields into one string, separated by a delimiter like #. This enables powerful prefix-based queries with begins_with.
Example: Hierarchical Location Data
PK: "REGION#Asia" SK values: "COUNTRY#India" "COUNTRY#India#STATE#Maharashtra" "COUNTRY#India#STATE#Maharashtra#CITY#Mumbai" "COUNTRY#Japan" "COUNTRY#Japan#STATE#Tokyo" Query all items in India: PK = "REGION#Asia" AND SK begins_with "COUNTRY#India" Query all Maharashtra items only: PK = "REGION#Asia" AND SK begins_with "COUNTRY#India#STATE#Maharashtra"
This pattern navigates a hierarchy of any depth with simple begins_with conditions — no recursive queries needed.
Pattern 3: Overloaded Global Secondary Index
Instead of creating many GSIs for different entity types, you create one GSI with generic attribute names (GSI1PK and GSI1SK) and populate them with entity-specific values per item type.
Example
Base Table item for an Order: PK: "USER#U001" SK: "ORDER#ORD999" GSI1PK: "STATUS#Pending" ← GSI partition key (reused) GSI1SK: "2024-06-01" ← GSI sort key (reused) Base Table item for a Support Ticket: PK: "USER#U001" SK: "TICKET#TKT001" GSI1PK: "STATUS#Open" ← Same GSI attribute, different entity GSI1SK: "2024-06-02" GSI Query: All Pending items today GSI1PK = "STATUS#Pending" → Returns matching Orders and Tickets
One GSI serves multiple access patterns across multiple entity types. This reduces the total number of GSIs you need and simplifies capacity management.
Pattern 4: Write Sharding for Hot Partitions
When many items share a low-cardinality partition key (like a daily event counter), writes concentrate on one partition. Write sharding artificially spreads writes by appending a random suffix.
Example: Daily Event Counter
Without sharding: PK: "EVENT#2024-06-01" ← All writes go to one partition → Hot With sharding (suffix 1–10): PK: "EVENT#2024-06-01#1" PK: "EVENT#2024-06-01#2" ... PK: "EVENT#2024-06-01#10" ← Writes spread across 10 partitions To read the total count for the day: Query all 10 shards → sum the counts in your application
This pattern trades read simplicity (must query all shards) for write scalability (no hot partition).
Pattern 5: Sparse Index for Efficient Filtering
A sparse index (via GSI) only includes items that have a specific attribute. Use this to create a small, efficient index of a filtered subset of items — without scanning the entire table.
Example: Index Only Unshipped Orders
Orders table has 1 million items. Only 500 orders have "UnshippedFlag" attribute (others do not). GSI on "UnshippedFlag": → GSI contains only those 500 items → Query the GSI to get all unshipped orders instantly → No table scan needed When an order ships: Remove the "UnshippedFlag" attribute using UpdateItem → Item disappears from GSI automatically
The GSI self-maintains. Items join the index when the attribute is added and leave when it is removed.
Pattern 6: Adjacency List (Many-to-Many Relationships)
DynamoDB does not support foreign keys or joins. The adjacency list pattern models many-to-many relationships (like students enrolled in courses, or users following other users) inside a single table.
Example: Course Enrollment
TABLE: Enrollments ┌──────────────────┬──────────────────┬────────────────┐ │ PK │ SK │ Type │ ├──────────────────┼──────────────────┼────────────────┤ │ COURSE#C001 │ COURSE#C001 │ Course │ │ COURSE#C001 │ STUDENT#S001 │ Enrollment │ │ COURSE#C001 │ STUDENT#S002 │ Enrollment │ │ STUDENT#S001 │ STUDENT#S001 │ Student │ │ STUDENT#S001 │ COURSE#C001 │ Enrollment │ │ STUDENT#S001 │ COURSE#C002 │ Enrollment │ └──────────────────┴──────────────────┴────────────────┘ Query: All students in Course C001 PK = "COURSE#C001" AND SK begins_with "STUDENT#" Query: All courses that Student S001 is enrolled in PK = "STUDENT#S001" AND SK begins_with "COURSE#"
Both directions of the relationship are queryable without a second table or a scan.
Pattern 7: DynamoDB Accelerator (DAX) for Read-Heavy Workloads
DAX is a fully managed, in-memory cache designed specifically for DynamoDB. It sits in front of your DynamoDB table and caches read results. Subsequent reads for the same item return from cache in microseconds instead of single-digit milliseconds from DynamoDB.
Without DAX: App → DynamoDB → 5ms response → App With DAX: App → DAX Cache (item found) → 100 microseconds → App App → DAX Cache (item not found) → DynamoDB → 5ms → App → cache stored
When DAX Helps
- The same items are read thousands of times per second (trending products, game leaderboards)
- Read-heavy workloads where you want to reduce RCU consumption and cost
- Applications that need microsecond response times
When DAX Does Not Help
- Write-heavy workloads (DAX caches reads, not writes)
- Applications requiring strongly consistent reads (DAX returns cached data, which may be milliseconds old)
- Infrequently read items that are unlikely to be in the cache
Pattern 8: Versioning and Audit Trail
Instead of updating an item in place, you write a new item for every version. Each version gets a sort key like v1, v2, v3. The latest version is always LATEST or the highest version number.
TABLE: Documents PK: "DOC#D001", SK: "v1" → Original document (created 2024-01-01) PK: "DOC#D001", SK: "v2" → Edited by User A (2024-03-15) PK: "DOC#D001", SK: "v3" → Edited by User B (2024-05-20) PK: "DOC#D001", SK: "LATEST" → Copy of v3 (for fast latest read) Read the current document: PK = "DOC#D001", SK = "LATEST" → one fast GetItem Read full version history: PK = "DOC#D001" → Query returns all versions in order
Design Checklist Before Building
| Step | Action |
|---|---|
| 1 | List all access patterns your application needs |
| 2 | Identify which patterns are the most frequent and latency-sensitive |
| 3 | Design primary key (PK + SK) to satisfy the top patterns directly |
| 4 | Add GSIs to cover remaining access patterns |
| 5 | Consider single table design if entities have overlapping access patterns |
| 6 | Check partition key cardinality — avoid hot partitions |
| 7 | Verify no critical access pattern requires a Scan |
| 8 | Plan TTL for temporary data and enable PITR for production tables |
Summary
DynamoDB design is access-pattern-first. Model your data around what your application needs to read and write — not around how the data is logically related. Single table design reduces round trips. Composite sort keys enable hierarchical queries. Overloaded GSIs serve multiple access patterns from one index. Sparse indexes filter efficiently without scans. These patterns — applied thoughtfully — produce DynamoDB tables that remain fast, affordable, and maintainable at any scale.
