Intermediate4 min read

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:

PKSKattributes
CUSTOMER#42PROFILEname, email, plan
CUSTOMER#42ORDER#2026-001total, status
CUSTOMER#42ORDER#2026-002total, 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 :

Partition: CUSTOMER#42SK: PROFILESK: ORDER#2026-001SK: ORDER#2026-002One Query

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:

PKSKGSI1PKGSI1SK
ORDER#001METADATASTATUS#OPEN2026-01-04
ORDER#002METADATASTATUS#OPEN2026-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 modeled 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.

Updated

Try this design interactively

Sketch your entities and access patterns in the free DynamoDB Single-Table Design tool — it suggests PK/SK key templates, previews the item collections, and shows which patterns need a GSI.

Open the Single-Table Design tool