DynamoDB differs from every other database: you design the table around your access patterns, not your normalized data model. Primary keys are your only indexes — everything else is a full scan. The trick is composite sort keys and GSIs that encode multiple lookup patterns into one table. Done right, DynamoDB scales infinitely with single-digit millisecond latency and zero database administration. Claude Code designs single-table schemas, writes condition expressions that prevent race conditions, and implements DynamoDB Streams processors.
CLAUDE.md for DynamoDB Projects
## DynamoDB Design
- Single-table design: all entity types in one table (avoids cross-table transactions)
- PK: entity type prefix (ORDER#id, USER#id, PRODUCT#id)
- SK: access pattern (METADATA, ORDER#date#id, STATUS#shipped)
- GSIs: up to 20 per table; name by access pattern (GSI-by-user-id, GSI-by-status)
- Condition expressions: enforce constraints atomically (stock > 0, version matches)
- No scans: every query uses PK or GSI — never table scans in production
- DynamoDB Streams: CDC for downstream listeners; process with Lambda
Single-Table Schema Design
Table: myapp-production
Entity: Order
PK = "ORDER#<orderId>"
SK = "METADATA"
GSI1PK = "USER#<userId>" → Query all orders for a user
GSI1SK = "ORDER#<createdAt>#<orderId>" → Time-ordered
GSI2PK = "STATUS#<status>" → Query by status (admin dashboard)
GSI2SK = "ORDER#<createdAt>"
Entity: OrderItem
PK = "ORDER#<orderId>"
SK = "ITEM#<itemId>"
Entity: User
PK = "USER#<userId>"
SK = "METADATA"
Access Patterns:
1. Get order by ID → Query PK="ORDER#id", SK="METADATA"
2. Get all items for order → Query PK="ORDER#id", SK begins_with "ITEM#"
3. Get all orders for user → Query GSI1, PK="USER#id" (sorted by time)
4. Get orders by status → Query GSI2, PK="STATUS#pending"
DynamoDB Client Setup
// db/dynamodb.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand, TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export const ddb = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
});
export const TABLE = process.env.DYNAMODB_TABLE ?? 'myapp-production';
// Helper: build keys with type prefix
export const keys = {
order: (id: string) => ({ PK: `ORDER#${id}`, SK: 'METADATA' }),
orderItems: (orderId: string) => ({ PK: `ORDER#${orderId}`, SK: 'ITEM#' }),
user: (id: string) => ({ PK: `USER#${id}`, SK: 'METADATA' }),
};
Core Operations
// db/orders.ts
import { ddb, TABLE, keys } from './dynamodb';
import { GetCommand, PutCommand, QueryCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
export async function getOrder(orderId: string): Promise<Order | null> {
const { Item } = await ddb.send(new GetCommand({
TableName: TABLE,
Key: keys.order(orderId),
}));
return Item ? (Item as Order) : null;
}
export async function createOrder(order: Order): Promise<void> {
const now = new Date().toISOString();
await ddb.send(new PutCommand({
TableName: TABLE,
Item: {
PK: `ORDER#${order.id}`,
SK: 'METADATA',
GSI1PK: `USER#${order.userId}`,
GSI1SK: `ORDER#${now}#${order.id}`,
GSI2PK: `STATUS#${order.status}`,
GSI2SK: `ORDER#${now}`,
...order,
createdAt: now,
updatedAt: now,
version: 1,
},
// Condition: prevent re-creation with same ID
ConditionExpression: 'attribute_not_exists(PK)',
}));
}
// Query all orders for a user (using GSI1)
export async function getOrdersByUser(userId: string, limit = 20): Promise<Order[]> {
const { Items = [] } = await ddb.send(new QueryCommand({
TableName: TABLE,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk AND begins_with(GSI1SK, :sk)',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':sk': 'ORDER#',
},
ScanIndexForward: false, // Descending (newest first)
Limit: limit,
}));
return Items as Order[];
}
// Optimistic locking: only update if version matches
export async function updateOrderStatus(
orderId: string,
newStatus: string,
expectedVersion: number,
): Promise<void> {
try {
await ddb.send(new UpdateCommand({
TableName: TABLE,
Key: keys.order(orderId),
UpdateExpression: 'SET #status = :status, GSI2PK = :gsi2pk, version = :v, updatedAt = :now',
ConditionExpression: 'version = :expectedVersion',
ExpressionAttributeNames: { '#status': 'status' },
ExpressionAttributeValues: {
':status': newStatus,
':gsi2pk': `STATUS#${newStatus}`,
':v': expectedVersion + 1,
':expectedVersion': expectedVersion,
':now': new Date().toISOString(),
},
}));
} catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
throw new Error('Concurrent modification — retry');
}
throw err;
}
}
DynamoDB Transactions
// db/place-order.ts — atomic order creation with inventory check
export async function placeOrderWithInventory(
order: Order,
items: CartItem[],
): Promise<void> {
// Build transaction: deduct inventory AND create order atomically
const transactItems = [
// Create order
{
Put: {
TableName: TABLE,
Item: buildOrderItem(order),
ConditionExpression: 'attribute_not_exists(PK)',
},
},
// Deduct inventory for each item
...items.map(item => ({
Update: {
TableName: TABLE,
Key: { PK: `PRODUCT#${item.productId}`, SK: 'INVENTORY' },
UpdateExpression: 'SET stock = stock - :qty',
ConditionExpression: 'stock >= :qty', // Prevent negative stock
ExpressionAttributeValues: {
':qty': item.quantity,
},
},
})),
];
try {
await ddb.send(new TransactWriteCommand({ TransactItems: transactItems }));
} catch (err) {
if (err.name === 'TransactionCanceledException') {
// Check which condition failed
const reasons = err.CancellationReasons;
if (reasons?.some((r: any) => r.Code === 'ConditionalCheckFailed')) {
throw new Error('Insufficient inventory for one or more items');
}
}
throw err;
}
}
DynamoDB Streams + Lambda
// lambda/streams-processor.ts
import type { DynamoDBStreamEvent } from 'aws-lambda';
import { unmarshall } from '@aws-sdk/util-dynamodb';
export async function handler(event: DynamoDBStreamEvent) {
for (const record of event.Records) {
if (record.eventName === 'INSERT' && record.dynamodb?.NewImage) {
const item = unmarshall(record.dynamodb.NewImage as any);
// Only process order creation events
if (item.PK?.startsWith('ORDER#') && item.SK === 'METADATA') {
await notifyOrderCreated(item as Order);
}
}
if (record.eventName === 'MODIFY' && record.dynamodb?.NewImage) {
const newItem = unmarshall(record.dynamodb.NewImage as any);
const oldItem = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage as any) : null;
if (newItem.PK?.startsWith('ORDER#') && newItem.status !== oldItem?.status) {
await handleOrderStatusChange(newItem as Order, oldItem?.status);
}
}
}
}
For the PostgreSQL alternative when you need flexible queries rather than key-value access patterns, the PostgreSQL advanced guide covers indexing strategies for relational workloads. For the AWS CDK infrastructure that provisions DynamoDB tables with streams and Lambda processors, the AWS CDK guide covers L2 DynamoDB constructs. The Claude Skills 360 bundle includes DynamoDB skill sets covering single-table design, GSI patterns, condition expressions, and Streams processing. Start with the free tier to try DynamoDB schema design generation.