DynamoDB Design Patterns for Real Applications
Practical DynamoDB patterns - single table design, access patterns, GSIs, and avoiding common mistakes.
Table of Contents
DynamoDB requires a different mindset than relational databases. Here’s what I’ve learned designing tables for production.
The Mindset Shift #
In SQL, you model data first, then write queries. In DynamoDB, you start with access patterns.
Ask: “What questions will I need to answer?”
Single Table Design #
Instead of multiple tables, use one table with different item types:
PK | SK | Type | Data
----------------|---------------------|---------|------------
USER#123 | PROFILE | User | {name, email}
USER#123 | ORDER#456 | Order | {total, status}
USER#123 | ORDER#456#ITEM#1 | Item | {product, qty}
ORDER#456 | ORDER#456 | Order | {userId, total}
This enables fetching related data in a single query.
Access Patterns #
Pattern 1: Get User Profile #
result, _ := client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String("MyTable"),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: "USER#123"},
"SK": &types.AttributeValueMemberS{Value: "PROFILE"},
},
})
Pattern 2: Get User’s Orders #
result, _ := client.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("MyTable"),
KeyConditionExpression: aws.String("PK = :pk AND begins_with(SK, :sk)"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "USER#123"},
":sk": &types.AttributeValueMemberS{Value: "ORDER#"},
},
})
Pattern 3: Get Order with Items #
result, _ := client.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("MyTable"),
KeyConditionExpression: aws.String("PK = :pk AND begins_with(SK, :sk)"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "USER#123"},
":sk": &types.AttributeValueMemberS{Value: "ORDER#456"},
},
})
// Returns order AND its items in one query
Global Secondary Indexes (GSI) #
For access patterns that don’t fit the main table’s keys:
GSI1PK | GSI1SK | (projected attributes)
----------------|---------------------|------------------------
ORDER#PENDING | 2024-01-15#ORDER#1 | {orderId, userId}
ORDER#SHIPPED | 2024-01-14#ORDER#2 | {orderId, userId}
Query orders by status:
result, _ := client.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("MyTable"),
IndexName: aws.String("GSI1"),
KeyConditionExpression: aws.String("GSI1PK = :status"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":status": &types.AttributeValueMemberS{Value: "ORDER#PENDING"},
},
})
Transactions #
For operations that must succeed or fail together:
_, err := client.TransactWriteItems(ctx, &dynamodb.TransactWriteItemsInput{
TransactItems: []types.TransactWriteItem{
{
Update: &types.Update{
TableName: aws.String("MyTable"),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: "USER#123"},
"SK": &types.AttributeValueMemberS{Value: "PROFILE"},
},
UpdateExpression: aws.String("SET balance = balance - :amount"),
ConditionExpression: aws.String("balance >= :amount"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":amount": &types.AttributeValueMemberN{Value: "100"},
},
},
},
{
Put: &types.Put{
TableName: aws.String("MyTable"),
Item: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: "USER#123"},
"SK": &types.AttributeValueMemberS{Value: "TXN#" + txnID},
// ... transaction details
},
},
},
},
})
Time-to-Live (TTL) #
Automatically expire items:
type Session struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
UserID string `dynamodbav:"userId"`
ExpiresAt int64 `dynamodbav:"ttl"` // Unix timestamp
}
// Item will be deleted after ExpiresAt
session := Session{
PK: "SESSION#abc123",
SK: "SESSION#abc123",
UserID: "user123",
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
Pagination #
Handle large result sets:
func getAllOrders(ctx context.Context, userID string) ([]Order, error) {
var orders []Order
var lastKey map[string]types.AttributeValue
for {
input := &dynamodb.QueryInput{
TableName: aws.String("MyTable"),
KeyConditionExpression: aws.String("PK = :pk AND begins_with(SK, :sk)"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "USER#" + userID},
":sk": &types.AttributeValueMemberS{Value: "ORDER#"},
},
ExclusiveStartKey: lastKey,
Limit: aws.Int32(100),
}
result, err := client.Query(ctx, input)
if err != nil {
return nil, err
}
// Process items...
orders = append(orders, parseOrders(result.Items)...)
if result.LastEvaluatedKey == nil {
break
}
lastKey = result.LastEvaluatedKey
}
return orders, nil
}
Common Mistakes #
1. Hot Partitions #
Avoid keys that concentrate traffic:
// Bad: All writes go to same partition
PK: "ORDERS"
// Good: Distribute across partitions
PK: "ORDERS#2024-01-15"
// or
PK: "ORDERS#" + hash(orderID) % 10
2. Scan Operations #
Scans read every item - expensive and slow:
// Bad: Scanning for orders by status
client.Scan(ctx, &dynamodb.ScanInput{
FilterExpression: aws.String("status = :s"),
})
// Good: Use GSI with status as partition key
client.Query(ctx, &dynamodb.QueryInput{
IndexName: aws.String("StatusIndex"),
KeyConditionExpression: aws.String("status = :s"),
})
3. Large Items #
DynamoDB has 400KB item limit. Store large data in S3:
type Document struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
S3Location string `dynamodbav:"s3Location"` // s3://bucket/path
Metadata string `dynamodbav:"metadata"`
}
Cost Optimization #
On-Demand vs Provisioned #
- On-Demand: Variable traffic, unpredictable load
- Provisioned: Steady traffic, predictable costs
Use Projections in GSIs #
Only project attributes you need:
GlobalSecondaryIndexes:
- IndexName: StatusIndex
KeySchema:
- AttributeName: status
KeyType: HASH
Projection:
ProjectionType: INCLUDE
NonKeyAttributes:
- orderId
- createdAt
Key Takeaways #
- Design for access patterns, not entities
- Single table design reduces queries
- Use GSIs sparingly - they duplicate data
- Avoid hot partitions with good key design
- Never scan in production
DynamoDB rewards upfront design effort with massive scale and predictable performance.