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.
What is single-table design in DynamoDB?
Single-table design stores every entity — customers, orders, order items — in one
DynamoDB table, distinguished by overloaded and
sort key prefixes. Because the keys are designed around your access patterns
rather than your entities, a parent and all its children live in one
and come back in a single Query — no joins,
no N+1 reads.
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. (For the ad-hoc
cross-entity question you never modelled for, DynoTable's SQL
Workbench runs the JOIN client-side — exploration
doesn't have to wait for a re-model.)
Sketch the design itself with the free Single-Table Design tool — it turns your access-pattern list into a PK/SK/GSI plan with example items and cost hints. 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.