Single-table design is not a clever schema trick; it is an operational bet that your access patterns are stable enough to encode into keys.

Situation

DynamoDB rewards teams that know exactly how their application reads and writes data. It gives predictable latency at large scale, managed replication, automatic partitioning, streams, TTL, conditional writes, transactions, and global secondary indexes. In exchange, it asks a hard question early: what are the queries?

That tradeoff is why single-table design exists. Instead of creating one table per entity, a team stores multiple entity types in one table and uses composite primary keys to place related items together. An order, its line items, payment events, fulfillment records, and audit entries may all share the same partition key and differ by sort key prefixes.

The result can be excellent. A request that would require joins in a relational database can become one partition query. A service can fetch an aggregate view with one call, keep latency stable under load, and avoid distributed transactions across multiple tables.

But the pattern gets oversold. Single-table design is not automatically more scalable than multi-table design. It is more scalable when the shape of the workload matches the shape of the keys.

The Problem

The failure usually starts after launch, not during the first schema review.

A team models the happy-path access pattern: get customer dashboard, list orders by account, fetch order detail, append events. The key design works. The service is fast. Costs are reasonable.

Then product behavior changes. Support wants to find all failed payments by provider. Finance wants reconciliation by settlement date. Operations wants open orders by warehouse and priority. Analytics wants historical exports. A new feature needs to query relationships in the opposite direction from the original aggregate.

The table still contains the data, but it no longer contains the access path.

Now the team has bad options. Add a global secondary index and backfill it. Overload an existing index with another entity shape and hope the naming convention remains understandable. Duplicate data into another item type. Stream changes into OpenSearch, S3, or a relational store. Run scans for rare workflows and accept cost spikes. Or migrate the model while production traffic continues.

The core question is: when is DynamoDB single-table design an architecture advantage, and when does it become accumulated coupling disguised as performance?

Core Concept

The answer is to treat single-table design as an access-pattern contract, not as a default modeling style.

Use it when the service has bounded, high-volume operational queries. Avoid it when the service is still discovering its query surface, when ad hoc investigation is central to the workflow, or when many teams will independently add new entity relationships over time.

A healthy single-table design starts with the request paths, not the nouns.

flowchart TD
  A[product request — fetch account workspace] --> B[access pattern inventory]
  B --> C[partition key — account scope]
  C --> D[sort key — entity and time ordering]
  D --> E[primary query — account aggregate]
  D --> F[index query — status queue]
  D --> G[index query — user lookup]
  E --> H[service response — bounded read]
  F --> I[worker response — bounded queue]
  G --> J[support response — bounded lookup]

The design is good when each important request maps to a bounded key condition. The design is weak when important requests require scans, client-side filtering over broad partitions, or fragile conventions that only one engineer understands.

A practical test: write the production questions as code comments before writing the entity model.

Get account workspace by account id
List open tasks by account id and status
Fetch task detail by account id and task id
List tasks assigned to user id
Append task event if version matches
Expire invitation after ttl

Those statements tell you whether the table needs a primary key only, one global secondary index, a sparse index, duplicated lookup items, or a separate read model.

In Practice

Context

Amazon’s DynamoDB documentation and public talks describe single-table design as a pattern for known access patterns, especially workloads that need high scale and low-latency key-value or document access. The documented pattern is to model item collections around partition keys, use sort keys for hierarchy and ordering, and add secondary indexes for alternate access paths.

This is not a relational modeling exercise. DynamoDB does not optimize arbitrary joins later. The schema is physical from the beginning: partition key choice affects distribution, sort key shape affects query behavior, and index definitions affect write amplification.

Action

The strong version of the pattern is deliberate denormalization.

For an ecommerce workflow, an account partition might contain profile metadata, active carts, orders, order items, and order events. Sort keys encode stable query order:

PK = ACCOUNT#123
SK = PROFILE#123

PK = ACCOUNT#123
SK = ORDER#2022-07-25#9001

PK = ACCOUNT#123
SK = ORDER#2022-07-25#9001#ITEM#1

PK = ACCOUNT#123
SK = ORDER#2022-07-25#9001#EVENT#2022-07-25T10:30:00Z

A sparse global secondary index might project only open fulfillment work:

GSI1PK = FULFILLMENT#OPEN
GSI1SK = WAREHOUSE#DAL#PRIORITY#HIGH#ORDER#9001

The application writes extra fields because the read path matters more than normalization. Conditional writes protect versioned updates. Transactions are reserved for small, critical multi-item changes. Streams can publish changes into downstream projections for search, analytics, or auditing.

Result

The result is operationally strong when the workload stays inside those paths.

The account view is a partition query. The fulfillment queue is an index query. The order detail is a bounded range query. The service avoids joins at request time and keeps predictable latency because the database is doing exactly the work the keys describe.

The result is operationally weak when the table becomes a dumping ground for every future question. Overloaded indexes become difficult to reason about because GSIs project different attributes for different entity types, forcing generic attribute names (Data1, Data2) and increasing storage costs. Backfills become risky because every item type has different attributes. Hot partitions appear when one tenant, status, or queue key receives disproportionate traffic. Cost shifts from read latency to write amplification and migration complexity.

Learning

The documented pattern is not “put everything in one table.” The pattern is “put items that serve the same operational access patterns in one table.”

That distinction matters. A single table can be a clean aggregate store. It can also become an undocumented protocol where every key prefix is a hidden API. The difference is whether the team maintains an access-pattern registry, capacity assumptions, ownership rules, and test coverage for key construction.

Where It Breaks

Failure modeWhy it hurtsBetter response
Unknown query surfaceNew product questions do not match existing keysStart with multi-table or relational storage until access patterns stabilize
Ad hoc investigationScans become normal operating procedureExport to S3, index into OpenSearch, or use a relational read model
Hot partitionsOne tenant, queue, or status hits the 10GB or 1000 WCU partition limitsAdd write sharding, redesign queue keys, or isolate the workload
Index overloading without disciplineKey prefixes become tribal knowledge; GSI write amplification explodesMaintain a key catalog and tests for every access pattern
Excessive denormalizationEvery write updates many item shapesSeparate read models by workflow and accept asynchronous projection
Cross-aggregate transactionsBusiness invariants span many partitionsReconsider whether DynamoDB is the system of record for that workflow
Multi-team ownershipIndependent features mutate one physical tableDefine table ownership or split bounded contexts

The most dangerous failure is not a bad key name. It is a table whose operational contract is implicit.

Once multiple services write different item types into the same table, the schema lives in application code, migration scripts, dashboards, and engineer memory. That can work for a disciplined platform team. It is painful for a fast-moving product surface without strong ownership.

What to Do Next

  • Problem: If your team cannot list the top access patterns, single-table design will force premature decisions into the physical schema.
  • Solution: Model requests first, then map each request to a primary key, sort key, index, or external projection.
  • Proof: Verify every critical workflow with bounded Query operations, conditional write tests, backfill rehearsal, and partition hot-spot analysis.
  • Action: Use single-table design for stable operational aggregates; use separate tables or read models when query discovery, analytics, or independent team ownership matters more than one-call retrieval.