Durgesh Rai

DynamoDB: Data Modeling

Single-table design, prefixed keys, and the two shapes a primary key can take, worked through step by step.

September 26, 20264 min read1 / 4

A managed NoSQL database with a single-digit millisecond latency guarantee at any scale. DAX (DynamoDB Accelerator) sits in front as an optional read-through cache, for when even single-digit milliseconds isn't fast enough.

That latency guarantee only holds if the table is designed around how the application actually queries data, not designed first and queried however happens to come up later.

Two databases optimizing for different things

Relational databases optimize for storage: normalize the data across tables, eliminate duplicate copies, pay the cost back in joins at query time. That tradeoff made sense when storage was the expensive resource.

DynamoDB optimizes for the opposite: duplicate data across items so a query never needs a join, because compute per request is what actually costs money at scale, not storage. Designing a DynamoDB table like a relational schema is the single most common way newcomers end up with a surprise bill, paying for joins a document database was never built to do cheaply.

Start from the queries, not the entities

The single most important step in modeling a DynamoDB table: list out almost every access pattern the application needs before deciding on a single key or index. Not the entities, the actual queries. Get one item. List all of a parent's children. Filter by status. Whatever the application genuinely does.

Most applications, per AWS's own guidance, only need one table. Every entity type, organizations, projects, employees, whatever a given app has, lives in the same table as differently-shaped items, instead of a separate table per entity, each with its own throughput to provision and pay for.

A repeatable process

  1. Draw an entity diagram. Map the real-world entities before touching a key.
  2. Identify the relationships between them. One-to-many, many-to-many. A many-to-many is usually worth splitting into two one-to-many relationships through a junction entity (a project-employee item, say), so each side can be queried directly off a key.
  3. List every access pattern, per entity. The step worth spending the most time on.
  4. Pick the primary key for each entity. A good key alone should satisfy most of the access patterns from step 3, no index required.
  5. Add secondary indexes for whatever the primary key still can't answer. Covered in DynamoDB: Indexes & Cost Tradeoffs.

One table, told apart by prefixed keys

A single table holding multiple entity types needs a way to tell them apart and to fetch related ones together. The common pattern: prefix key values by entity type, ORG#<id> as a partition key, PROJECT#<id> or EMPLOYEE#<id> as a sort key.

A query filtering by sort-key prefix returns only project items; dropping that condition returns every item under the same organization ExpandA query filtering by sort-key prefix returns only project items; dropping that condition returns every item under the same organization

That prefixing does two jobs at once.

  • A begins_with condition on the sort key filters to one entity type. Query PK = ORG#1234, SK begins_with PROJECT# and get only that organization's projects. Drop the sort-key condition entirely, and the same query returns every child item, projects and employees together, in one request.
  • Child entities inherit the parent's ID as their partition key. A project's partition key is its organization's ID, not its own. That keeps every query naturally scoped to the right organization without an extra filter. It also mirrors the real hierarchy: a project can't exist without an organization, so it doesn't get a partition of its own.

Two shapes for a primary key

  • A hash key alone (a simple partition key), when one attribute uniquely identifies an item.
  • A hash key plus a range key (a composite key), when items need to be grouped under a partition and then queried within it, sorted by the range key.

Getting this right matters because DynamoDB doesn't let arbitrary attributes be queried efficiently by default, only the key.

Only the key's attributes need declaring up front, nothing else does. Creating a table means specifying an attribute definition (name and type, string/number/binary) and a key schema (hash, or hash plus range) for the key alone. Every other attribute an item might ever hold, a note's title, its body, whatever comes later, is never declared in the table's schema at all. Two items in the same table can hold entirely different sets of non-key attributes, added freely at write time, which is the whole reason DynamoDB is called schemaless: the schema that exists is only ever about the key.

The Essentials

  1. DynamoDB optimizes for compute, relational databases optimize for storage. Modeling one like the other produces the wrong bill.
  2. List access patterns before picking a key. Most applications need exactly one table, with entities told apart by prefixed keys.
  3. A begins_with condition and inheriting the parent's ID as partition key are what make a single table queryable per entity type or all together.
  4. Only the key needs declaring up front. Every other attribute is added freely, at write time, per item.

Next: indexes

A good primary key answers most access patterns, but not all of them. DynamoDB: Indexes & Cost Tradeoffs covers what to do with the rest.

Further Reading and Watching