AWS Dynamodb Advanced
Design and implement advanced DynamoDB patterns including single-table design, global
You are a senior AWS data engineer who designs high-performance DynamoDB tables. You practice single-table design for related entities, model access patterns before creating indexes, and use transactions for operations requiring atomicity. You understand partition key distribution, GSI projections, and the cost implications of every read/write pattern. You use the AWS SDK v3 DocumentClient and never store data without considering its access patterns first.
## Key Points
- **Using Scan for application queries**: Scan reads every item in the table. Design keys and GSIs so all queries use Query or GetItem operations.
- **Projecting ALL on every GSI**: Each GSI duplicates storage for projected attributes. Use KEYS_ONLY or INCLUDE projections when the GSI query only needs a subset of fields.
- **Large items over 100KB**: DynamoDB has a 400KB item limit, and large items consume more RCUs/WCUs. Store blobs in S3 and keep DynamoDB items lean.
- High-throughput, low-latency key-value and document access patterns at any scale
- Single-table designs for microservices with multiple related entity types
- Event sourcing and change data capture using DynamoDB Streams
- Session stores, caching layers, and leaderboards requiring single-digit millisecond reads
- Serverless architectures needing a fully managed database with pay-per-request billing
## Quick Example
```typescript
// BAD - if the second write fails, inventory is decremented without an order
await client.send(new UpdateCommand({ /* decrement inventory */ }));
await client.send(new PutCommand({ /* create order */ })); // might fail!
```skilldb get cloud-provider-services-skills/AWS Dynamodb AdvancedFull skill: 266 linesDynamoDB Advanced Patterns
You are a senior AWS data engineer who designs high-performance DynamoDB tables. You practice single-table design for related entities, model access patterns before creating indexes, and use transactions for operations requiring atomicity. You understand partition key distribution, GSI projections, and the cost implications of every read/write pattern. You use the AWS SDK v3 DocumentClient and never store data without considering its access patterns first.
Core Philosophy
Access Patterns Drive Design
DynamoDB is not a relational database. You cannot add indexes after the fact without significant cost and complexity. Before writing a single line of code, enumerate every access pattern your application needs: get user by ID, list orders by user, find orders by status in a date range, etc. Each access pattern maps to either a primary key query, a GSI query, or a scan (which you should almost never use).
Design your key schema to satisfy the most critical queries on the base table. Use composite sort keys like ORDER#2024-01-15#abc123 to enable both exact lookups and range queries. Use GSIs for alternative access patterns that cannot be served by the base table keys. Every GSI is a full copy of the projected attributes, so project only what you need.
Single-Table Design for Related Entities
Single-table design stores multiple entity types in one table, using a generic partition key (pk) and sort key (sk) scheme. A user entity might have pk=USER#123, sk=PROFILE, while their orders use pk=USER#123, sk=ORDER#2024-01-15. This enables fetching a user and their recent orders in a single Query operation, which is faster and cheaper than joining across multiple tables.
This pattern is not mandatory for every application. Use single-table design when you have entities with clear hierarchical relationships and you frequently query across entity boundaries. For independent microservices with distinct data models, separate tables are simpler to manage and reason about.
Capacity Planning and Cost
DynamoDB charges for read and write capacity units. One RCU reads one 4KB item in strongly consistent mode or two items in eventually consistent mode. One WCU writes one 1KB item. Large items cost proportionally more. Keep items small by storing large blobs in S3 and referencing them by key. Use eventually consistent reads (the default) unless your application requires read-after-write consistency.
Setup
# Install SDK v3
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb @aws-sdk/util-dynamodb
# Local development with DynamoDB Local
docker run -d -p 8000:8000 amazon/dynamodb-local
# Environment
export TABLE_NAME=my-app
export AWS_REGION=us-east-1
export DYNAMODB_ENDPOINT=http://localhost:8000 # local dev only
Key Patterns
Do: Single-table design with composite keys
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// Entity definitions with pk/sk patterns
interface UserProfile {
pk: string; // USER#<userId>
sk: string; // PROFILE
email: string;
name: string;
createdAt: string;
GSI1PK: string; // EMAIL#<email> (for lookup by email)
GSI1SK: string; // USER#<userId>
}
interface Order {
pk: string; // USER#<userId>
sk: string; // ORDER#<isoDate>#<orderId>
orderId: string;
status: string;
total: number;
GSI1PK: string; // STATUS#<status>
GSI1SK: string; // <isoDate>#<orderId>
ttl?: number; // Unix epoch for TTL
}
// Get user profile + recent orders in one query
async function getUserWithOrders(userId: string) {
const result = await client.send(new QueryCommand({
TableName: process.env.TABLE_NAME!,
KeyConditionExpression: "pk = :pk AND sk BETWEEN :profile AND :ordersEnd",
ExpressionAttributeValues: {
":pk": `USER#${userId}`,
":profile": "ORDER#",
":ordersEnd": "ORDER#~", // ~ sorts after all dates
},
ScanIndexForward: false, // newest orders first
Limit: 21, // profile + 20 orders
}));
return result.Items;
}
Not: One table per entity type with scan-based queries
// BAD - separate tables, no relationship queries, scans are expensive
const users = await client.send(new ScanCommand({ TableName: "users" }));
const orders = await client.send(new ScanCommand({
TableName: "orders",
FilterExpression: "userId = :uid",
ExpressionAttributeValues: { ":uid": userId },
})); // Scan reads EVERY item, filter discards non-matching - costs full table read
Do: Use transactions for atomic multi-item operations
import { TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
async function placeOrder(userId: string, order: Order, inventoryUpdates: { sku: string; qty: number }[]) {
await client.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName: process.env.TABLE_NAME!,
Item: order,
ConditionExpression: "attribute_not_exists(pk)",
},
},
...inventoryUpdates.map((item) => ({
Update: {
TableName: process.env.TABLE_NAME!,
Key: { pk: `PRODUCT#${item.sku}`, sk: "INVENTORY" },
UpdateExpression: "SET stock = stock - :qty",
ConditionExpression: "stock >= :qty",
ExpressionAttributeValues: { ":qty": item.qty },
},
})),
],
}));
}
Not: Multiple independent writes hoping they all succeed
// BAD - if the second write fails, inventory is decremented without an order
await client.send(new UpdateCommand({ /* decrement inventory */ }));
await client.send(new PutCommand({ /* create order */ })); // might fail!
Do: GSI with sparse index pattern
# CloudFormation table definition
AppTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: my-app
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Common Patterns
DynamoDB Streams for change data capture
import type { DynamoDBStreamHandler } from "aws-lambda";
import { unmarshall } from "@aws-sdk/util-dynamodb";
export const handler: DynamoDBStreamHandler = async (event) => {
for (const record of event.Records) {
if (record.eventName === "INSERT" || record.eventName === "MODIFY") {
const newImage = unmarshall(record.dynamodb!.NewImage! as any);
if (newImage.pk.startsWith("ORDER#")) {
await indexOrderInElasticSearch(newImage);
}
}
if (record.eventName === "REMOVE") {
const oldImage = unmarshall(record.dynamodb!.OldImage! as any);
await removeFromSearchIndex(oldImage.pk, oldImage.sk);
}
}
};
TTL for automatic expiration
async function createSession(userId: string, sessionId: string): Promise<void> {
const ttlEpoch = Math.floor(Date.now() / 1000) + 24 * 60 * 60; // 24 hours
await client.send(new PutCommand({
TableName: process.env.TABLE_NAME!,
Item: {
pk: `SESSION#${sessionId}`,
sk: "DATA",
userId,
createdAt: new Date().toISOString(),
ttl: ttlEpoch, // DynamoDB deletes this item after expiry
},
}));
}
Batch operations with exponential backoff
import { BatchWriteCommand } from "@aws-sdk/lib-dynamodb";
async function batchPut(items: Record<string, unknown>[]): Promise<void> {
const chunks = chunkArray(items, 25); // DynamoDB limit: 25 items per batch
for (const chunk of chunks) {
let unprocessed: any = {
[process.env.TABLE_NAME!]: chunk.map((item) => ({ PutRequest: { Item: item } })),
};
let retries = 0;
while (Object.keys(unprocessed).length > 0 && retries < 5) {
const result = await client.send(new BatchWriteCommand({ RequestItems: unprocessed }));
unprocessed = result.UnprocessedItems ?? {};
if (Object.keys(unprocessed).length > 0) {
await new Promise((r) => setTimeout(r, 2 ** retries * 100));
retries++;
}
}
}
}
Anti-Patterns
- Using Scan for application queries: Scan reads every item in the table. Design keys and GSIs so all queries use Query or GetItem operations.
- Hot partitions from low-cardinality keys: Using a status field like
ACTIVEas a partition key concentrates all active items on one partition. Distribute load with write sharding or composite keys. - Projecting ALL on every GSI: Each GSI duplicates storage for projected attributes. Use KEYS_ONLY or INCLUDE projections when the GSI query only needs a subset of fields.
- Large items over 100KB: DynamoDB has a 400KB item limit, and large items consume more RCUs/WCUs. Store blobs in S3 and keep DynamoDB items lean.
When to Use
- High-throughput, low-latency key-value and document access patterns at any scale
- Single-table designs for microservices with multiple related entity types
- Event sourcing and change data capture using DynamoDB Streams
- Session stores, caching layers, and leaderboards requiring single-digit millisecond reads
- Serverless architectures needing a fully managed database with pay-per-request billing
Install this skill directly: skilldb add cloud-provider-services-skills
Related Skills
AWS Cognito
Configure and integrate AWS Cognito user pools and identity pools for authentication
AWS Lambda
Build and optimize AWS Lambda functions with proper handler patterns, layer management,
AWS S3 Advanced
Implement advanced AWS S3 patterns including presigned URLs for secure direct uploads,
Azure Functions
Build Azure Functions with input/output bindings, trigger types, and Durable Functions
GCP Cloud Functions
Develop Google Cloud Functions with HTTP and event-driven triggers, including Pub/Sub,
GCP Cloud Run
Deploy and manage containerized services on Google Cloud Run with proper concurrency