I've designed dozens of DynamoDB single-table schemas. I built FluentDynamoDB specifically to enforce best practices and reduce the boilerplate. And honestly? It still makes my head hurt every time I start a new one.
That's not a complaint—it's the nature of the problem. Single-table design requires thinking about your data differently than relational databases. The payoff is massive (single-digit millisecond reads at any scale), but the upfront planning is real work.
Why Single-Table Design Is Hard
Relational databases let you normalize your data and figure out queries later. JOINs handle the complexity at read time. DynamoDB flips this: you denormalize upfront and design your schema around how you'll query it.
The Mental Shift
Stop thinking about entities. Start thinking about access patterns.
- • "Get user by ID" → partition key design
- • "Get all orders for a user" → sort key design
- • "Get all orders in a date range" → GSI design
- • "Get user by email" → another GSI
Every access pattern you need must be supported by either the primary key or a Global Secondary Index. Miss one during planning, and you're either doing expensive scans or restructuring your table.
The Planning Process
Before writing any code or creating any tables, I work through these steps:
List Every Access Pattern
Write down every way your application needs to read or write data. Be specific: "Get user by ID," "List orders for user sorted by date," "Find all users in organization."
Identify the Primary Key
Find the access pattern that's most critical or most frequent. That usually drives your partition key (PK) and sort key (SK) design.
Design GSIs for Remaining Patterns
Each access pattern that can't be served by the primary key needs a GSI. You get 20 per table—use them strategically.
Plan Your Key Prefixes
Single-table means multiple entity types in one table. Prefixes like "USER#123" and "ORDER#456" keep them organized and queryable.
The Spreadsheet Phase
I literally use a spreadsheet to map this out. Columns for PK, SK, GSI1PK, GSI1SK, etc. Rows for each entity type. It's not glamorous, but it catches problems before they're baked into code.
Common Patterns
Hierarchical Data
Parent-child relationships using composite sort keys:
PK: ORG#123 | SK: ORG#123 → Organization record
PK: ORG#123 | SK: USER#456 → User in organization
PK: ORG#123 | SK: USER#456#ORDER#789 → User's order
Query PK = "ORG#123" to get everything in the org. Add SK begins_with "USER#456" to get one user's data.
Inverted Index (GSI)
When you need to query the "other direction":
Table: PK: USER#123 | SK: ORDER#456
GSI1: PK: ORDER#456 | SK: USER#123
Table gives you "orders for user." GSI gives you "user for order."
Sparse Index
GSI that only contains items with a specific attribute:
GSI2PK: Only populated on "active" orders
Query GSI2 → Only returns active orders
Efficient for filtering without scanning. Only items with the GSI attribute appear in the index.
Mistakes I've Made (So You Don't Have To)
Hot Partitions
Using a low-cardinality partition key (like "status" or "type") concentrates all traffic on a few partitions. DynamoDB throttles you even if you have capacity to spare.
Forgetting an Access Pattern
Discovered mid-project that we needed "get all items created in the last 24 hours." No GSI supported it. Options were expensive scan or table restructure.
Over-Indexing
Every GSI duplicates data and consumes write capacity. Adding GSIs "just in case" gets expensive fast. Only index what you actually query.
Large Items in GSIs
GSIs copy the entire item by default. If your items are large, project only the attributes you need for that access pattern.
Why I Built FluentDynamoDB
After implementing single-table patterns repeatedly, I got tired of the boilerplate. The AWS SDK is powerful but verbose. I wanted something that:
- ✓ Enforces key prefix conventions automatically
- ✓ Generates type-safe repository code from entity definitions
- ✓ Works with AOT compilation (no reflection at runtime)
- ✓ Makes the "right way" the easy way
FluentDynamoDB doesn't make the planning easier—you still need to think through your access patterns. But once you have the design, it makes implementation faster and less error-prone.
When Single-Table Isn't Worth It
Single-table design isn't always the right choice. Consider multiple tables when:
Multiple Tables Make Sense
- • Entities have completely different access patterns
- • Different capacity/scaling needs per entity
- • Team boundaries align with entity boundaries
- • Simpler mental model is worth the trade-off
Single-Table Shines
- • Related entities queried together
- • Transactional writes across entity types
- • Consistent access patterns across entities
- • Minimizing round trips matters