Every developer coming from relational databases makes the same DynamoDB mistake: they design tables like they design SQL schemas. One table per entity, foreign keys everywhere, normalize everything. Then they hit production and discover why DynamoDB charges per read, not per join — because there are no joins. Single-table design is the pattern that fixes this, and once you understand it, relational modeling in DynamoDB looks as wrong as it actually is.
⚡ TL;DR: In DynamoDB, put all your entities in one table. Use composite partition keys like
USER#123and sort keys likeORDER#2026-04-05#456to model relationships. Design your access patterns first, then design your keys around them. Never the reverse.
Why Single-Table Design Exists
DynamoDB is a key-value store with optional range queries. Every operation retrieves items by primary key. There are no joins, no subqueries, no cross-table operations. If you need data from two “tables”, that’s two separate round-trips — two separate costs.
Single-table design collapses all your entities into one table and uses carefully constructed partition and sort keys so that related data lives in the same partition and can be fetched in a single query. One network round-trip. One cost unit.
The Key Overloading Technique
// Multi-table design (wrong for DynamoDB):
// users table: { userId, name, email }
// orders table: { orderId, userId, total, date }
// items table: { itemId, orderId, product, qty }
// To get a user + their orders: 2 separate queries
// Single-table design (correct):
// Everything in ONE table with overloaded PK/SK:
// User record:
{ PK: "USER#u123", SK: "PROFILE", name: "Alice", email: "alice@co.com" }
// User's orders:
{ PK: "USER#u123", SK: "ORDER#2026-04-05#o456", total: 99.99, status: "shipped" }
{ PK: "USER#u123", SK: "ORDER#2026-03-20#o789", total: 45.00, status: "delivered" }
// Order's items:
{ PK: "ORDER#o456", SK: "ITEM#prod-001", product: "Laptop Stand", qty: 1 }
{ PK: "ORDER#o456", SK: "ITEM#prod-002", product: "USB Hub", qty: 2 }
// Get user profile + all orders in ONE query:
// PK = "USER#u123", SK begins_with "ORDER#" → all orders, sorted by dateAccess Patterns First — The Golden Rule
Before writing a single line of DynamoDB code, list every access pattern your application needs. Your key design will flow directly from this list. Change the access patterns later and your entire key structure may need to change.
// Example access patterns for an e-commerce system:
// 1. Get user profile by userId
// 2. Get all orders for a user (sorted newest first)
// 3. Get a specific order by orderId
// 4. Get all items in an order
// 5. Get all orders with status "pending"
// 6. Get user's most recent 10 orders
// Key design that satisfies all 6 patterns:
// Base table:
// PK SK EntityType ...attributes
// USER#u123 PROFILE USER name, email
// USER#u123 ORDER#2026-04-05 ORDER orderId, total, status
// ORDER#o456 ITEM#prod-001 ITEM product, qty
// ORDER#o456 METADATA ORDER userId, total, status
// GSI (Global Secondary Index) for pattern 5 (orders by status):
// GSI1PK GSI1SK
// STATUS#pending 2026-04-05 → all pending orders sortable by date
// Query pattern 1: GetItem PK=USER#u123, SK=PROFILE
// Query pattern 2: Query PK=USER#u123, SK begins_with ORDER#, ScanIndexForward=false
// Query pattern 5: Query on GSI1, GSI1PK=STATUS#pendingGlobal Secondary Indexes — DynamoDB’s Join Replacement
// AWS SDK v3 example — create and query with GSI
const { DynamoDBClient, QueryCommand, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');
const client = new DynamoDBClient({ region: 'us-east-1' });
// Write an order with GSI attributes
await client.send(new PutItemCommand({
TableName: 'CheatCodersStore',
Item: marshall({
PK: 'USER#u123',
SK: 'ORDER#2026-04-05#o456',
GSI1PK: 'STATUS#pending', // For orders-by-status pattern
GSI1SK: '2026-04-05T10:30:00Z', // Sortable timestamp
orderId: 'o456',
userId: 'u123',
total: 99.99,
status: 'pending',
EntityType: 'ORDER'
})
}));
// Query all pending orders (GSI1)
const pendingOrders = await client.send(new QueryCommand({
TableName: 'CheatCodersStore',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :status',
ExpressionAttributeValues: marshall({ ':status': 'STATUS#pending' }),
ScanIndexForward: false // Newest first
}));
const orders = pendingOrders.Items.map(unmarshall);
console.log(orders);The Hierarchical Data Pattern
// Model deeply nested data: Organisation → Team → Member → Task
// All in one table:
{ PK: "ORG#acme", SK: "METADATA", name: "Acme Corp" }
{ PK: "ORG#acme", SK: "TEAM#engineering", teamName: "Engineering" }
{ PK: "ORG#acme", SK: "TEAM#engineering#MEMBER#u1", name: "Alice", role: "Lead" }
{ PK: "MEMBER#u1", SK: "TASK#2026-04-05#t001", title: "Fix bug", done: false }
// Get entire org structure (departments + members) in ONE query:
// PK = "ORG#acme", SK begins_with "" → returns everything
// Get just teams:
// PK = "ORG#acme", SK begins_with "TEAM#"
// Get a specific team's members:
// PK = "ORG#acme", SK begins_with "TEAM#engineering#MEMBER#"
// This is what makes DynamoDB magical for hierarchical data —
// the sort key encodes the hierarchy, enabling partial queries at any levelCommon Mistakes and How to Avoid Them
// ❌ MISTAKE 1: Using sequential IDs as partition keys
// This creates "hot partitions" — all traffic hits one server
{ PK: "1", SK: "ORDER" } // IDs 1,2,3 all likely on same partition
// ✅ FIX: Use random UUIDs or ULIDs (time-sortable random IDs)
{ PK: "USER#01HVZK2X...", SK: "ORDER#..." } // Distributed across partitions
// ❌ MISTAKE 2: Putting filter conditions in FilterExpression instead of keys
// FilterExpression reads ALL items then filters — you still pay for reads
const wrong = await client.send(new QueryCommand({
FilterExpression: 'userId = :uid', // Scans everything!
...
}));
// ✅ FIX: Design keys so filters are on PK/SK (KeyConditionExpression)
// ❌ MISTAKE 3: Scan operations in production
// Scan reads EVERY item in the table — avoid completely
// ✅ FIX: Every access pattern should use Query on base table or GSISingle-Table Design Cheat Sheet
- ✅ List ALL access patterns before designing any keys
- ✅ Use
ENTITY#idformat for partition keys (e.g.,USER#123) - ✅ Use composite sort keys to encode relationships (
ORDER#date#id) - ✅ Add GSI attributes to items that need alternate access patterns
- ✅ Use
begins_withon sort key for prefix queries - ✅ Use
ScanIndexForward: falsefor newest-first ordering - ❌ Never design tables like SQL schemas
- ❌ Never use
Scanin production - ❌ Never put sequential IDs as partition keys
For the infrastructure side of DynamoDB deployments, the AWS Lambda cold start guide covers DynamoDB SDK initialization — a common source of Lambda cold start latency. For system design context, this single-table pattern is one of the core techniques in designing scalable data layers. See AWS DynamoDB best practices for the official guidance.
Disclosure: This post contains affiliate links. If you purchase through these links, CheatCoders earns a small commission at no extra cost to you. We only recommend tools and books we genuinely find valuable.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Related: How to Design a Global Media Delivery and Personalization Platform on AWS with CloudFront, S3, API Gateway, and DynamoDB Global Tables · AWS DynamoDB: Advanced Patterns for Production at Scale · REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Design a Production Rate Limiter: Algorithms Seniors Actually Use in Interviews and at Work - CheatCoders
Pingback: AWS S3 Presigned URLs: The Security Mistakes 90% of Developers Make - CheatCoders