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.

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 normalisation optimises storage; DynamoDB modelling optimises 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:

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 partition key as a single item collection:

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 GSI 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.

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