DynamoDB Single-Table Design: A Practical Guide

What single-table design is, why it's faster and cheaper, and how to model entities, relationships and access patterns into one DynamoDB table.

3 min read· Updated Jun 15, 2026
On this page

Single-table design is the DynamoDB modeling technique where you store multiple entity types in one table instead of mirroring the relational habit of one table per entity. It’s the pattern AWS itself recommends for most high-scale applications — and the one that trips up newcomers the most.

This guide explains why it works, when to use it, and how to actually do it.

Why one table instead of many?

DynamoDB has no joins. In a relational database you’d put Users, Orders and OrderItems in separate tables and join them at query time. In DynamoDB, a join would mean multiple round trips — slow and expensive at scale.

Single-table design flips this: you co-locate related items so a single query returns everything you need. The benefits:

  • Fewer round trips. Fetch a user and their last 20 orders in one Query.
  • Lower cost. One request instead of several; you pay per request.
  • Consistent performance. No fan-out that degrades as data grows.

The cost is conceptual: you model around access patterns, not entities.

Start with access patterns, not entities

This is the golden rule. Before you touch keys, write down every question your application will ask, for example:

  • Get a user by id
  • List a user’s orders, newest first
  • Get an order with its line items
  • List all orders in a given status

Your key design exists to answer these queries efficiently. If you model entities first (the relational instinct), you’ll fight the database.

The building blocks: PK, SK and key overloading

Every item has a partition key (PK) and usually a sort key (SK). In single-table design you give these generic names (literally PK and SK) because different entity types reuse them with different meanings — this is key overloading.

A common convention is typed, prefixed keys:

PK = USER#123        SK = PROFILE
PK = USER#123        SK = ORDER#2026-06-01#A1
PK = USER#123        SK = ORDER#2026-05-20#B7
PK = ORDER#A1        SK = ITEM#sku-9

Now a single query PK = USER#123 AND begins_with(SK, "ORDER#") returns all of a user’s orders, already sorted by date because the SK starts with the timestamp.

A worked example

Say we have users and orders. Model them like this:

EntityPKSKOther attributes
UserUSER#123PROFILEname, email
OrderUSER#123ORDER#<ts>#<id>total, status
Order itemORDER#<id>ITEM#<sku>qty, price

Access patterns served:

  • Get user: GetItem(PK=USER#123, SK=PROFILE)
  • List a user’s orders (newest first): Query(PK=USER#123, begins_with(SK, "ORDER#"), ScanIndexForward=false)
  • Get an order’s items: Query(PK=ORDER#A1, begins_with(SK, "ITEM#"))

No joins, no scans — just direct queries.

Inverting relationships with a GSI

What about “list all orders with status SHIPPED”? The base table can’t answer it. Add a Global Secondary Index with overloaded keys:

GSI1PK = STATUS#SHIPPED    GSI1SK = ORDER#<ts>#<id>

Now Query(GSI1PK=STATUS#SHIPPED) lists shipped orders by date. One GSI can serve many such patterns by storing different values per entity type. See secondary indexes for the GSI vs LSI tradeoffs.

Tips that save you pain

  • Use a generic type attribute on every item (type = "Order") so you can filter and debug.
  • Keep keys human-readable with prefixes — your future self reading the table will thank you.
  • Don’t over-index. Add GSIs only when an access pattern demands one.
  • Model the relationship, not the object. Items that are queried together should share a partition key.

When not to use it

Single-table design adds cognitive load. If your app has a handful of unrelated entities, low traffic, or you value readability over micro-optimization, multiple simple tables are perfectly fine. The pattern pays off most when entities are related and accessed together at scale.

Visualize it before you commit

The hardest part of single-table design is seeing how overloaded keys lay out across entity types. A tool that shows real items grouped by partition — and lets you model access patterns against live data — turns an abstract exercise into something concrete. That’s exactly what Tablyne’s Single-Table Design Studio is for.

Frequently asked questions

Is single-table design always the right choice?

No. It shines when you have related entities accessed together and you want the lowest latency and cost. For simple apps, a few separate tables are easier to reason about and perfectly fine. Choose based on your access patterns, not dogma.

How many Global Secondary Indexes do I need for single-table design?

Most single-table designs use one to three GSIs. Each GSI supports a family of access patterns by overloading its keys. Start with one (often called GSI1) and add more only when a new access pattern can't be served.

Can I change my single-table design later?

Yes, but it's work. You usually add new GSIs or backfill new key attributes rather than restructure existing items. Designing around your known access patterns up front avoids painful migrations.

Keep reading