- Software Letters
- Posts
- Mastering Single Table Design in DynamoDB: A Complete Guide
Mastering Single Table Design in DynamoDB: A Complete Guide
DynamoDB is one of AWS’s most powerful managed databases: fully serverless, instantly scalable, and with predictable performance. But to use it effectively, you can’t treat it like a relational database. That’s where Single Table Design (STD) comes in — a modeling strategy that can unlock huge performance and cost benefits if done right.
1. What is Single Table Design?
Traditionally, in relational databases, you’d store each entity type in its own table:
Users
tableOrders
tableProducts
table
Relationships are modeled with foreign keys and joined at query time.
DynamoDB works differently:
No joins
Limited query operators
Query speed depends on partition key access
Single Table Design means all entity types live in the same table, differentiated by composite primary keys and type attributes.
Example table snippet:
PK | SK | entityType | attributes... |
---|---|---|---|
USER#123 | PROFILE | User | name, email, createdAt |
USER#123 | ORDER#2025-08-01 | Order | total, status, orderDate |
PRODUCT#987 | METADATA | Product | name, price, categoryId |
CATEGORY#Shoes | PRODUCT#987 | Category | productName, price |
Here:
PK: Groups related items (User ID, Product ID, Category ID, etc.)
SK: Describes the specific item type or a timestamped version
2. Why Single Table Design Matters
DynamoDB pricing is based on read/write units. Scans (reading the whole table) are expensive and slow. If you store everything in separate tables and need to fetch related data, you’ll either:
Make multiple network calls
Perform expensive scans
Single Table Design:
Reduces queries to O(1) lookups
Eliminates joins and multi-table fetches
Centralizes throughput and indexes
Enables atomic writes across different item types in the same partition
3. Core Design Principles
Before touching DynamoDB, follow these rules:
3.1 Start with Access Patterns
You do not start with entities. You start with the questions your application will ask.
Example:
Get a user profile by ID
Get all orders for a given user
Get all products in a category
Search orders by date
Fetch product details by ID
Every query must be answered by a Query
operation using:
Partition Key + optional Sort Key condition
Or a Global Secondary Index (GSI)
3.2 Composite Keys
DynamoDB’s primary key = Partition Key (PK) + Sort Key (SK).
PK decides where data lives. All items with the same PK are stored together.
SK decides order within the partition and allows range queries.
Example:
PK =
USER#123
SK =
ORDER#2025-08-01
Query: "All orders for user 123" →PK = USER#123
and SK begins_with ORDER#
3.3 Entity Type Prefixing
Always prefix PKs and SKs with entity type:
USER#123
ORDER#001
PRODUCT#987
Benefits:
Avoids collisions
Makes data human-readable
Simplifies debugging
3.4 Denormalization
In STD, duplication is intentional.
If you need to fetch an order and its product details together, store product details in the order item. Storage is cheap; extra queries are not.
4. Example: E-Commerce in a Single Table
4.1 Requirements
We want to:
Fetch user profile by ID
Fetch all orders for a user
Fetch product details by ID
Fetch all products in a category
Fetch all orders on a specific date
4.2 Table Schema
Primary Key:
PK: Entity grouping (User ID, Product ID, Category ID)
SK: Entity type or context
PK | SK | entityType | Attributes... |
---|---|---|---|
USER#123 | PROFILE | User | name, email, joinedAt |
USER#123 | ORDER#001 | Order | total, status, orderDate |
USER#123 | ORDER#002 | Order | total, status, orderDate |
PRODUCT#987 | METADATA | Product | name, price, categoryId |
CATEGORY#Shoes | PRODUCT#987 | Category | productName, price |
4.3 Queries
User profile:
PK = USER#123
andSK = PROFILE
User orders:
PK = USER#123
andSK begins_with ORDER#
Product details:
PK = PRODUCT#987
andSK = METADATA
Category products:
PK = CATEGORY#Shoes
andSK begins_with PRODUCT#
4.4 Global Secondary Index
To get all orders by date across users:
GSI1PK =
ORDER
GSI1SK =
orderDate
Order item example:
PK | SK | GSI1PK | GSI1SK | ... |
---|---|---|---|---|
USER#123 | ORDER#001 | ORDER | 2025-08-01 | ... |
Query GSI1:GSI1PK = ORDER
and GSI1SK = 2025-08-01
5. Advanced Techniques
5.1 Sparse Indexes
If only certain items have a GSI attribute, that index is "sparse" and contains only relevant items. Useful for filtering by status, active flags, etc.
5.2 Time-Ordered SKs
Storing timestamps in SK (ORDER#<ISODate>
) allows:
Range queries (
BETWEEN
)Latest-first reads (when inverted with negative timestamp)
5.3 Aggregates & Summary Items
Instead of calculating aggregates on the fly:
Store summary items in the same partition
Example:
PK = USER#123
,SK = ORDER_SUMMARY
,totalOrders = 42
5.4 Adjacency Lists
To model many-to-many relationships, store linking items:
PK = USER#123
,SK = PRODUCT#987
PK = PRODUCT#987
,SK = USER#123
6. Common Mistakes
No Access Pattern Planning
→ Leads to costly scans.
Solution: Write out queries first.Overusing GSIs
→ Increases cost and write latency.
Solution: Reuse indexes where possible.Too Clever Keys
→ Impossible to debug.
Solution: Keep keys readable.Relational Mindset
→ Trying to normalize data.
Solution: Duplicate when it saves queries.
7. Best Practices Checklist
Start with all queries your app will run
Use type prefixes for PK/SK
Group related items in same PK
Denormalize data for query speed
Use sparse GSIs for selective queries
Keep schema documented with examples
Final Thoughts
Single Table Design requires a mindset shift:
SQL: Store data normalized → join at query time
DynamoDB: Decide queries → shape data for them
Once you embrace this, DynamoDB becomes blazingly fast and cost-efficient. You’ll spend less time fighting scans and more time delivering features.