PrepZone Logo
PrepZone

DynamoDB Single-Table Design

Entity overloading, composite keys, and one table for an entire microservice domain.

Read these first

Why this matters

  • VaultCommerce's order microservice collapsed five tables into one VaultCommerce table — fewer cross-table transactions, one on-call surface, lower latency for "load my account page."
  • The trade-off is cognitive: every engineer must read the access-pattern matrix before adding an entity.
  • GSIs on the same table provide inverted lookups (email → user) while keeping writes atomic within a partition where possible.
Partition key: USER#42Routes to shard
Items in partition
ORDER#2026-001Sort key
ORDER#2026-002Sort key
PROFILESort key
Partition key routes to a shard. Sort key orders items within that partition.

Key overloading pattern

Java
{ "PK": "USER#42",       "SK": "PROFILE",           "email": "alice@vault.test", "tier": "GOLD" }
{ "PK": "USER#42",       "SK": "ORDER#2026-0042",   "total": 129.99, "status": "PAID" }
{ "PK": "USER#42",       "SK": "WISHLIST#SKU-8842", "addedAt": "2026-04-01" }
{ "PK": "PRODUCT#SKU-8842", "SK": "METADATA",      "title": "Trail Pack", "price": 89 }

Single-table conventions

  • PK — partition anchor (USER#id, PRODUCT#sku, ORDER#id).
  • SK — entity discriminator and sort order (PROFILE, ORDER#date, METADATA).
  • GSI1 — inverted lookups (GSI1PK=EMAIL#alice@vault.test → find user).
  • Type attribute — optional entityType: "Order" for client-side filtering.

One Query PK=USER#42 with SK begins_with prefixes assembles the account dashboard.

Access pattern matrix

| Pattern | Operation | Key condition | |---------|-----------|---------------| | User profile | GetItem | PK=USER#42, SK=PROFILE | | User orders | Query | PK=USER#42, SK begins_with ORDER# | | Product detail | GetItem | PK=PRODUCT#SKU, SK=METADATA | | Login by email | Query GSI1 | GSI1PK=EMAIL#... |

Design the matrix before writing code — adding a pattern late often means new GSIs and backfill jobs.

Write pattern with denormalization

VaultCommerce duplicates customerName on order items to avoid a second lookup at read time. Accept duplication; update with TransactWrite when profile renames must propagate (or emit async reconciliation via Streams).

Java
table.put_item(Item={
    "PK": f"USER#{user_id}",
    "SK": f"ORDER#{order_id}",
    "GSI1PK": f"STATUS#{status}",
    "GSI1SK": created_at,
    "customerName": name,
    "total": total,
})

Quick recall

Everything you need if you only revisit this box.

  • Single-table design overloads PK/SK prefixes to colocate related entities.
  • One Query per partition replaces multiple SQL joins for dashboard reads.
  • Maintain an access-pattern matrix before schema changes.
  • Denormalize attributes to avoid extra reads; reconcile async when needed.
  • GSIs on the same table power inverted lookups (email → user).
  • VaultCommerce stores users, orders, and wishlists under USER# partitions.

Test yourself

Answer these before moving on — recall is what makes it stick.