DynamoDB Design Patterns

DynamoDB rewards careful upfront design. Unlike SQL databases where you write queries first and build indexes later, DynamoDB requires you to understand your access patterns before you design your tables. The patterns in this topic represent proven approaches used by engineers building large-scale production systems.

Core Design Philosophy

Every DynamoDB design starts with one question: what queries does my application need to run? List every access pattern before writing a single line of schema. Your primary key, sort key, and indexes must collectively satisfy all of those patterns — efficiently.

Pattern 1: Single Table Design

Single Table Design stores multiple entity types in one DynamoDB table. Instead of a separate table for Users, Orders, and Products, everything lives in one table. Items are differentiated by key prefixes and a type attribute.

Why Use It

SQL databases encourage many tables with joins. DynamoDB does not support joins across tables — fetching related items from multiple tables requires multiple round trips. A single table lets you retrieve all related data in one query, reducing latency and cost.

Diagram: Single Table Design for an E-Commerce App

TABLE: AppData  (PK: PK, SK: SK)

┌──────────────────────┬──────────────────────────┬─────────────┬──────────┐
│ PK                   │ SK                       │ Type        │ Data     │
├──────────────────────┼──────────────────────────┼─────────────┼──────────┤
│ USER#U001            │ PROFILE                  │ User        │ Name...  │
│ USER#U001            │ ORDER#2024-01-05         │ Order       │ Laptop...|
│ USER#U001            │ ORDER#2024-03-12         │ Order       │ Mouse... │
│ PRODUCT#P100         │ DETAILS                  │ Product     │ Price... │
│ PRODUCT#P100         │ REVIEW#R001              │ Review      │ 5 stars  │
│ PRODUCT#P100         │ REVIEW#R002              │ Review      │ 4 stars  │
└──────────────────────┴──────────────────────────┴─────────────┴──────────┘

Query 1: Get user U001 profile + all their orders
  PK = "USER#U001" → Returns profile + all orders in one shot

Query 2: Get product P100 details + all its reviews
  PK = "PRODUCT#P100" → Returns product + all reviews in one shot

Both queries fetch everything needed in a single DynamoDB operation. No joins, no multiple round trips.

Naming Convention for Keys

The generic attribute names PK and SK are deliberate. In a single table, the same key attributes hold different logical values for different entity types. Using generic names avoids confusion.

Pattern 2: Composite Sort Key Design

Composite sort keys combine multiple data fields into one string, separated by a delimiter like #. This enables powerful prefix-based queries with begins_with.

Example: Hierarchical Location Data

PK: "REGION#Asia"
SK values:
  "COUNTRY#India"
  "COUNTRY#India#STATE#Maharashtra"
  "COUNTRY#India#STATE#Maharashtra#CITY#Mumbai"
  "COUNTRY#Japan"
  "COUNTRY#Japan#STATE#Tokyo"

Query all items in India:
  PK = "REGION#Asia" AND SK begins_with "COUNTRY#India"

Query all Maharashtra items only:
  PK = "REGION#Asia" AND SK begins_with "COUNTRY#India#STATE#Maharashtra"

This pattern navigates a hierarchy of any depth with simple begins_with conditions — no recursive queries needed.

Pattern 3: Overloaded Global Secondary Index

Instead of creating many GSIs for different entity types, you create one GSI with generic attribute names (GSI1PK and GSI1SK) and populate them with entity-specific values per item type.

Example

Base Table item for an Order:
  PK: "USER#U001"
  SK: "ORDER#ORD999"
  GSI1PK: "STATUS#Pending"      ← GSI partition key (reused)
  GSI1SK: "2024-06-01"          ← GSI sort key (reused)

Base Table item for a Support Ticket:
  PK: "USER#U001"
  SK: "TICKET#TKT001"
  GSI1PK: "STATUS#Open"         ← Same GSI attribute, different entity
  GSI1SK: "2024-06-02"

GSI Query: All Pending items today
  GSI1PK = "STATUS#Pending" → Returns matching Orders and Tickets

One GSI serves multiple access patterns across multiple entity types. This reduces the total number of GSIs you need and simplifies capacity management.

Pattern 4: Write Sharding for Hot Partitions

When many items share a low-cardinality partition key (like a daily event counter), writes concentrate on one partition. Write sharding artificially spreads writes by appending a random suffix.

Example: Daily Event Counter

Without sharding:
  PK: "EVENT#2024-06-01"  ← All writes go to one partition → Hot

With sharding (suffix 1–10):
  PK: "EVENT#2024-06-01#1"
  PK: "EVENT#2024-06-01#2"
  ...
  PK: "EVENT#2024-06-01#10"  ← Writes spread across 10 partitions

To read the total count for the day:
  Query all 10 shards → sum the counts in your application

This pattern trades read simplicity (must query all shards) for write scalability (no hot partition).

Pattern 5: Sparse Index for Efficient Filtering

A sparse index (via GSI) only includes items that have a specific attribute. Use this to create a small, efficient index of a filtered subset of items — without scanning the entire table.

Example: Index Only Unshipped Orders

Orders table has 1 million items.
Only 500 orders have "UnshippedFlag" attribute (others do not).

GSI on "UnshippedFlag":
  → GSI contains only those 500 items
  → Query the GSI to get all unshipped orders instantly
  → No table scan needed

When an order ships:
  Remove the "UnshippedFlag" attribute using UpdateItem
  → Item disappears from GSI automatically

The GSI self-maintains. Items join the index when the attribute is added and leave when it is removed.

Pattern 6: Adjacency List (Many-to-Many Relationships)

DynamoDB does not support foreign keys or joins. The adjacency list pattern models many-to-many relationships (like students enrolled in courses, or users following other users) inside a single table.

Example: Course Enrollment

TABLE: Enrollments

┌──────────────────┬──────────────────┬────────────────┐
│ PK               │ SK               │ Type           │
├──────────────────┼──────────────────┼────────────────┤
│ COURSE#C001      │ COURSE#C001      │ Course         │
│ COURSE#C001      │ STUDENT#S001     │ Enrollment     │
│ COURSE#C001      │ STUDENT#S002     │ Enrollment     │
│ STUDENT#S001     │ STUDENT#S001     │ Student        │
│ STUDENT#S001     │ COURSE#C001      │ Enrollment     │
│ STUDENT#S001     │ COURSE#C002      │ Enrollment     │
└──────────────────┴──────────────────┴────────────────┘

Query: All students in Course C001
  PK = "COURSE#C001" AND SK begins_with "STUDENT#"

Query: All courses that Student S001 is enrolled in
  PK = "STUDENT#S001" AND SK begins_with "COURSE#"

Both directions of the relationship are queryable without a second table or a scan.

Pattern 7: DynamoDB Accelerator (DAX) for Read-Heavy Workloads

DAX is a fully managed, in-memory cache designed specifically for DynamoDB. It sits in front of your DynamoDB table and caches read results. Subsequent reads for the same item return from cache in microseconds instead of single-digit milliseconds from DynamoDB.

Without DAX:
  App → DynamoDB → 5ms response → App

With DAX:
  App → DAX Cache (item found) → 100 microseconds → App
  App → DAX Cache (item not found) → DynamoDB → 5ms → App → cache stored

When DAX Helps

  • The same items are read thousands of times per second (trending products, game leaderboards)
  • Read-heavy workloads where you want to reduce RCU consumption and cost
  • Applications that need microsecond response times

When DAX Does Not Help

  • Write-heavy workloads (DAX caches reads, not writes)
  • Applications requiring strongly consistent reads (DAX returns cached data, which may be milliseconds old)
  • Infrequently read items that are unlikely to be in the cache

Pattern 8: Versioning and Audit Trail

Instead of updating an item in place, you write a new item for every version. Each version gets a sort key like v1, v2, v3. The latest version is always LATEST or the highest version number.

TABLE: Documents

PK: "DOC#D001", SK: "v1" → Original document (created 2024-01-01)
PK: "DOC#D001", SK: "v2" → Edited by User A (2024-03-15)
PK: "DOC#D001", SK: "v3" → Edited by User B (2024-05-20)
PK: "DOC#D001", SK: "LATEST" → Copy of v3 (for fast latest read)

Read the current document:
  PK = "DOC#D001", SK = "LATEST" → one fast GetItem

Read full version history:
  PK = "DOC#D001" → Query returns all versions in order

Design Checklist Before Building

StepAction
1List all access patterns your application needs
2Identify which patterns are the most frequent and latency-sensitive
3Design primary key (PK + SK) to satisfy the top patterns directly
4Add GSIs to cover remaining access patterns
5Consider single table design if entities have overlapping access patterns
6Check partition key cardinality — avoid hot partitions
7Verify no critical access pattern requires a Scan
8Plan TTL for temporary data and enable PITR for production tables

Summary

DynamoDB design is access-pattern-first. Model your data around what your application needs to read and write — not around how the data is logically related. Single table design reduces round trips. Composite sort keys enable hierarchical queries. Overloaded GSIs serve multiple access patterns from one index. Sparse indexes filter efficiently without scans. These patterns — applied thoughtfully — produce DynamoDB tables that remain fast, affordable, and maintainable at any scale.

Leave a Comment

Your email address will not be published. Required fields are marked *