Intermediate8 min read

DynamoDB Single-Table Design: The Complete Guide

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.

Run the access patterns
Query PK = "CUSTOMER#42"

3 of 5 items read — one Query, one partition. The key condition filters before any data leaves it, so you pay only for the matches.

Base table, in stored order
#PKSKGSI1PKGSI1SKAttributes
1CUSTOMER#42ORDER#2026-001STATUS#OPEN2026-01-04total, status=open
2CUSTOMER#42ORDER#2026-002STATUS#OPEN2026-01-05total, status=open
3CUSTOMER#42PROFILEname, email, plan
CUSTOMER#77ORDER#2026-003total, status=paid
CUSTOMER#77PROFILEname, email, plan

The items and keys are this guide’s own example. Highlighted rows are what the one call above touches; the numbers show the order they come back in. “Items read” counts items, not read units — real billing rounds each read up to 4 KB blocks.

Start from access patterns, not nouns

Single-table design begins with a numbered list of reads and writes your application actually performs — "show customer profile", "list open orders for customer", "lookup order by id globally" — not with an ER diagram of entity boxes. Each pattern must compile to a Query or GetItem on the base table or on an overloaded GSI. If a pattern needs a Scan, the keys are wrong; fix the model before shipping.

Write the list down explicitly. A three-pattern app might look like:

  1. Fetch customer + all orders for one customer id (high volume).
  2. List all open orders across customers, sorted by date (moderate volume).
  3. Fetch one order by its public order id (high volume).

Pattern 1 is a base-table Query on PK = CUSTOMER#<id>. Pattern 2 lands on GSI1PK = STATUS#OPEN with GSI1SK as the date. Pattern 3 might use PK = ORDER#<id>, SK = METADATA as a separate item collection from pattern 1's CUSTOMER#<id> / ORDER#<id> rows — duplicate the order summary twice if both access paths are hot, or accept a second Query when one path is rare.

The free Single-Table Design tool accepts that pattern list and emits a PK/SK/GSI layout with example items, CreateTable JSON, and per-pattern cost hints so you can compare alternatives before committing.

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.

Read cost: one Query vs many GetItems

The billing win is real when children live under the parent's partition key. Suppose a customer profile (1 KB) and twelve order summaries (1 KB each) share PK = CUSTOMER#42. One eventually-consistent Query on that partition reads 13 KB total → 2 read request units (13 KB rounds up to four 4 KB blocks at 0.5 RCU each). Fetching the same thirteen items with thirteen GetItem calls meters the same per-item rounding individually → 7 units (thirteen × 0.5 RCU minimum one block per item). Same data, more than triple the read units — before network round trips.

Access styleRound tripsRead units (13 × 1 KB items, EC)
Query PK = CUSTOMER#4212
13 × GetItem by full primary key137
Scan + filter on customer id1+ pageswhole table

Numbers shift with item size — profile blobs at 6 KB each cross two 4 KB blocks per item — which is why the item-size calculator sits next to the design tool. Writes obey a 1 KB rounding boundary (one WCU per kilobyte, doubled inside transactions); fat order rows cost more to denormalize onto the parent partition.

Entity typing and sparse indexes

Most teams add a string type or entity attribute (CUSTOMER, ORDER, LINE) so application code can branch when a Query returns mixed sort-key prefixes. GSIs can be sparse: only items that populate GSI1PK/GSI1SK appear on the index, so a customer profile row with those attributes empty does not consume index storage or show up in unrelated queries.

When two entity types share an index, document what each prefix means (STATUS#, CATEGORY#, EMAIL#) in the same place you document the base-table prefixes. Future you should not have to reverse-engineer meaning from CloudWatch graphs alone.

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.

SignalLean separate tablesLean single-table
Access patterns documented and stable
Many unrelated domains with no shared reads
Team new to DynamoDB, needs obvious table names
Parent + children fetched together constantly
Heavy ad-hoc analytics across arbitrary joins✓ (or warehouse)
Hot multi-entity dashboard on one partition key

Regulatory or organizational boundaries (different data owners, distinct backup policies) can also justify multiple tables even when a single-table model fits access patterns technically.

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

Evolving a live single-table layout usually means adding GSIs or new item types, not renaming PK/SK semantics in place. Treat prefix contracts like API versions: add ORDER#v2# rather than repurpose ORDER# mid-flight. Migrations that rewrite every item's keys belong in a controlled backfill with dual-write periods, not in a Friday-afternoon script.

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 compose the resulting Query programs in the query builder when you're ready to ship SDK code. Read composite primary keys if partition + sort key mechanics still feel fuzzy — single-table design is mostly disciplined composite-key usage with shared attribute names.

Try DynoTable to browse a single-table schema and see the overloaded collections side by side — partition inspector view lines up sort-key order with the access patterns you sketched.

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