Single-Table Design in DynamoDB
Coming from SQL, the instinct is one table per entity: customers, orders,
order_items. In DynamoDB that instinct is usually wrong. A single table that
stores every entity, distinguished by overloaded key prefixes, lets you fetch
a parent and its children in one Query — no joins, no N+1.
Start from access patterns, not entities
Single-table design is access-pattern-first. Before you pick a single key,
write down every read your app makes — "get a customer's profile", "list a
customer's orders newest-first", "find all open orders" — because the keys exist
only to serve that list. Relational normalization optimizes storage; DynamoDB
modelling optimizes the queries you already know you'll run. Enumerate them, then
design keys so each one is a single Query.
The idea
Pick generic key names (PK, SK) and encode the entity type in the value:
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
Now one Query PK = "CUSTOMER#42" returns the profile and every order in a
single billed read. SK begins_with "ORDER#" narrows it to just the orders.
Visually, the overloaded items stack under one as a single :
One read of the partition hands back the customer and every order together.
Overloaded GSIs
The same trick works on indexes. Put a generic GSI1PK/GSI1SK on items, and a
single serves multiple access patterns depending on what each item writes
into those attributes:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
Now Query GSI1 WHERE GSI1PK = "STATUS#OPEN" lists open orders by date — a
pattern the base table can't answer. A different entity can reuse GSI1 with its
own meaning (e.g. CATEGORY#books). One index, many queries.
Many-to-many: the adjacency list
For relationships (a user in many teams, a team with many users), write the edge
twice with the ids swapped: PK=USER#1, SK=TEAM#9 and PK=TEAM#9, SK=USER#1.
Querying either side lists the other — the DynamoDB stand-in for a join table.
When not to single-table
It isn't free. One overloaded table is harder to reason about, harder to evolve, and analytics-hostile. If your access patterns are genuinely unknown or change constantly, or the data is mostly analytical, separate tables (or a different store) can be the saner call. Single-table wins when the patterns are known and high-volume.
Cost of the wrong shape
Modelling as separate tables forces a Scan or client-side join to reassemble a
customer, and that is the Scan footgun. Model the access
patterns first, then design keys to make each one a Query.
Estimate what these items cost per read with the item-size & capacity calculator, and try DynoTable to browse a single-table schema and see the overloaded collections side by side.