DynamoDB Single Table Design
Single table design stores several kinds of data in one DynamoDB table. Customers, orders, and products live side by side, and one request can fetch related items together. The approach trades some readability for speed and fewer requests.
The Filing Cabinet Picture
Imagine a shop that keeps one filing cabinet per customer. Each cabinet holds the customer's profile and every order. To answer a question about a customer, the owner opens one cabinet and finds everything inside. Single table design arranges data the same way. The partition key names the cabinet, and the sort key names the folder.
Why DynamoDB Has No Joins
DynamoDB cannot join tables. A relational design that spreads customers and orders across two tables needs two requests and a join in application code. DynamoDB avoids joins to keep response times steady at any scale. Single table design places related items under one partition key, so one Query returns them all.
Generic Key Names
A shared table needs generic key names because different item types use the keys differently. Developers often call them PK and SK. Key values carry a prefix that names the item type.
| PK | SK | Item Type | Other Attributes |
|---|---|---|---|
| CUSTOMER#C001 | PROFILE | Customer | Name, Email |
| CUSTOMER#C001 | ORDER#2026-06-01#O100 | Order | Total, Status |
| CUSTOMER#C001 | ORDER#2026-06-09#O105 | Order | Total, Status |
| CUSTOMER#C002 | PROFILE | Customer | Name, Email |
Item Collections
All items that share a partition key form an item collection. The collection for CUSTOMER#C001 holds one profile and two orders. A Query on that key returns the whole collection in a single request.
Query PK = "CUSTOMER#C001" → PROFILE → ORDER#2026-06-01#O100 → ORDER#2026-06-09#O105 (one request, all related data)
Narrowing with begins_with
The sort key supports the begins_with condition. The condition selects one item type from the collection.
Query PK = "CUSTOMER#C001" AND begins_with(SK, "ORDER#") → returns orders only, sorted by date Query PK = "CUSTOMER#C001" AND SK = "PROFILE" → returns the profile only
Dates written in a sortable format, such as 2026-06-01, keep orders in chronological order automatically.
Designing from Access Patterns
A relational design begins with the data. A single table design begins with the questions. List every query the application must answer, and design keys to serve those queries.
| Access Pattern | Key Condition |
|---|---|
| Get a customer profile | PK = CUSTOMER#id, SK = PROFILE |
| List orders for a customer | PK = CUSTOMER#id, SK begins_with ORDER# |
| Get one order by OrderID | Global secondary index on OrderID |
| List all orders on one date | Global secondary index on date |
Overloading Global Secondary Indexes
A global secondary index can also use generic names, such as GSI1PK and GSI1SK. Different item types fill those attributes with different values. One index serves several access patterns.
Order item: PK = CUSTOMER#C001 SK = ORDER#O100 GSI1PK = ORDER#O100 GSI1SK = CUSTOMER#C001 Query on GSI1 with GSI1PK = "ORDER#O100" finds the order directly.
Benefits
- One request returns related items, so latency drops.
- One table means one set of capacity settings, backups, and alarms.
- Fewer tables simplify permissions and infrastructure code.
Trade-Offs
- Generic keys make the table harder to read at first glance.
- New access patterns may require a redesign or a new index.
- Analytics tools find mixed item types harder to use.
- The team needs a documented key plan, or the table becomes confusing.
When to Choose Multiple Tables
Separate tables remain a valid choice. Unrelated data sets, different capacity needs, or different lifecycle rules (such as separate backup schedules) often favor multiple tables. Small applications with simple access patterns rarely need single table design. Choose the structure that your team can understand and maintain.
Summary
- Single table design stores many item types in one table under shared keys.
- Prefixed partition and sort keys group related items into collections.
- One Query fetches a whole collection, and begins_with narrows it.
- Access patterns drive the key design.
- The approach gains speed and loses some readability, so weigh both before choosing it.
