Core Concepts
Single-Table Design
Master DynamoDB's most powerful pattern: storing multiple entity types in one table to serve all access patterns efficiently.
What is Single-Table Design?
Single-table design (STD) is a DynamoDB modeling pattern where you store all entity types — users, orders, products, comments — in a single table, using a generic primary key structure (often PK and SK).
This sounds counterintuitive at first, but it's the recommended approach for DynamoDB because:
- DynamoDB charges per read/write operation. Joining across tables requires multiple requests.
- DynamoDB can return related items in a single request if they share a partition key.
- Fewer tables means simpler operations (backup, permissions, monitoring).
The key insight: items with the same PK are stored together on the same partition and can be fetched in a single query.
Generic Key Naming
Instead of userId and orderId, use generic names PK and SK, then store composite values:
PK SK Other attrs
USER#alice PROFILE name, email, role
USER#alice ORDER#2025-001 total, status
USER#alice ORDER#2025-002 total, status
PRODUCT#sku-123 METADATA name, price, stock
ORDER#2025-001 ITEM#sku-123 quantity, priceAccess Patterns
Define your access patterns before designing the table:
- Get user profile →
PK = USER#email, SK = PROFILE - Get all orders for user →
PK = USER#email, SK begins_with ORDER# - Get single order →
PK = USER#email, SK = ORDER#id - Get product →
PK = PRODUCT#sku, SK = METADATA
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
// Pattern 2: All orders for a user
const result = await docClient.send(new QueryCommand({
TableName: 'MyApp',
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': 'USER#alice@example.com',
':sk': 'ORDER#',
},
}));
// Result includes all order items for alice in one request!
console.log(result.Items);Global Secondary Indexes (GSI)
For access patterns that don't fit the main table's key structure, add a GSI:
// Access pattern: "Get all orders with status = PENDING"
// Main table PK/SK won't help here — we need a different key
// GSI: GSI1PK = entity type, GSI1SK = status
// Each item stores:
// GSI1PK = "ORDER"
// GSI1SK = "PENDING#2025-01-15"
const result = await docClient.send(new QueryCommand({
TableName: 'MyApp',
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :type AND begins_with(GSI1SK, :status)',
ExpressionAttributeValues: {
':type': 'ORDER',
':status': 'PENDING#',
},
}));Transactions
DynamoDB supports ACID transactions across up to 100 items in a single request:
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
await docClient.send(new TransactWriteCommand({
TransactItems: [
{
Update: {
TableName: 'MyApp',
Key: { PK: 'PRODUCT#sku-123', SK: 'METADATA' },
UpdateExpression: 'SET stock = stock - :qty',
ConditionExpression: 'stock >= :qty',
ExpressionAttributeValues: { ':qty': 1 },
},
},
{
Put: {
TableName: 'MyApp',
Item: {
PK: 'USER#alice',
SK: 'ORDER#2025-001',
status: 'CONFIRMED',
total: 49.99,
},
},
},
],
}));Example
// Full single-table item management
import { PutCommand, QueryCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
// Create user + initial profile in one write
await docClient.send(new PutCommand({
TableName: 'MyApp',
Item: {
PK: 'USER#alice@example.com',
SK: 'PROFILE',
entityType: 'USER',
name: 'Alice Smith',
email: 'alice@example.com',
createdAt: new Date().toISOString(),
GSI1PK: 'USER',
GSI1SK: 'alice@example.com',
},
ConditionExpression: 'attribute_not_exists(PK)', // prevent overwrite
}));Want to run this code interactively?
Try in Compiler