DynamoDB¶
What is DynamoDB?¶
Amazon DynamoDB is a fully managed, serverless, key-value and document NoSQL database designed for high-performance applications at any scale.
- Type: Managed NoSQL (Key-Value / Document)
- Vendor: Amazon Web Services (AWS)
- Protocol: HTTP/HTTPS REST API
- License: Proprietary (AWS managed service)
- Local Development: DynamoDB Local (Java)
Core Concepts¶
Data Model¶
Terminology¶
| Concept | Description |
|---|---|
| Table | Collection of items |
| Item | Single record (like a row) |
| Attribute | Data element (like a column) |
| Partition Key (PK) | Hash key, determines partition |
| Sort Key (SK) | Range key, enables range queries |
| Primary Key | PK alone or PK + SK |
| GSI | Global Secondary Index |
| LSI | Local Secondary Index |
| RCU | Read Capacity Unit |
| WCU | Write Capacity Unit |
Architecture¶
Core Features¶
DynamoDB offers: - Fully managed (no operations) - Serverless (pay per request option) - Single-digit millisecond latency - Automatic scaling - Built-in security (IAM, encryption) - Global tables (multi-region) - Point-in-time recovery - DynamoDB Streams (CDC) - TTL (automatic item expiration) - Transactions (ACID) - DAX (in-memory cache) - On-demand or provisioned capacity
Capacity Modes¶
On-Demand¶
Pay per request - no capacity planning
Scales automatically
Good for: Unpredictable workloads, new applications
Pricing (example):
- $1.25 per million write request units
- $0.25 per million read request units
Provisioned¶
Specify RCUs and WCUs
Auto-scaling available
Good for: Predictable workloads, cost optimization
1 RCU = 1 strongly consistent read/sec (up to 4KB)
= 2 eventually consistent reads/sec (up to 4KB)
1 WCU = 1 write/sec (up to 1KB)
Capacity Calculation¶
Example: 100 items/sec, 3KB each
Writes:
- 3KB / 1KB = 3 WCUs per item
- 100 items × 3 WCUs = 300 WCUs
Reads (strongly consistent):
- 3KB / 4KB = 1 RCU per item (rounded up)
- 100 items × 1 RCU = 100 RCUs
Reads (eventually consistent):
- 100 RCUs / 2 = 50 RCUs
Common Use Cases¶
1. Single-Table Design (User + Orders)¶
// Table: MyApp
// PK: USER#<userId> or ORDER#<orderId>
// SK: METADATA or ORDER#<timestamp>
// User item
{
"PK": "USER#123",
"SK": "METADATA",
"name": "John Doe",
"email": "[email protected]",
"type": "USER"
}
// Order items (under same PK for efficient queries)
{
"PK": "USER#123",
"SK": "ORDER#2024-01-15T10:30:00Z",
"orderId": "order_456",
"amount": 99.99,
"status": "shipped",
"type": "ORDER"
}
// Query user's orders
QueryRequest request = QueryRequest.builder()
.tableName("MyApp")
.keyConditionExpression("PK = :pk AND begins_with(SK, :skPrefix)")
.expressionAttributeValues(Map.of(
":pk", AttributeValue.builder().s("USER#123").build(),
":skPrefix", AttributeValue.builder().s("ORDER#").build()
))
.build();
2. Session Storage¶
// Table: Sessions
// PK: sessionId
// TTL: expiresAt
PutItemRequest request = PutItemRequest.builder()
.tableName("Sessions")
.item(Map.of(
"sessionId", AttributeValue.builder().s(sessionId).build(),
"userId", AttributeValue.builder().s(userId).build(),
"data", AttributeValue.builder().s(sessionData).build(),
"expiresAt", AttributeValue.builder().n(
String.valueOf(Instant.now().plus(Duration.ofHours(24)).getEpochSecond())
).build()
))
.build();
dynamoDb.putItem(request);
3. E-commerce Product Catalog¶
// Table: Products
// PK: CATEGORY#<category>
// SK: PRODUCT#<productId>
// GSI: ProductById (PK: productId)
// By category
QueryRequest byCategoryRequest = QueryRequest.builder()
.tableName("Products")
.keyConditionExpression("PK = :pk")
.expressionAttributeValues(Map.of(
":pk", AttributeValue.builder().s("CATEGORY#electronics").build()
))
.build();
// By product ID (using GSI)
QueryRequest byIdRequest = QueryRequest.builder()
.tableName("Products")
.indexName("ProductById")
.keyConditionExpression("productId = :id")
.expressionAttributeValues(Map.of(
":id", AttributeValue.builder().s("prod_123").build()
))
.build();
4. Gaming Leaderboard¶
// Table: Leaderboard
// PK: GAME#<gameId>
// SK: SCORE#<zeroPaddedScore>#<playerId> (for sorting)
// GSI: PlayerScore (PK: playerId, SK: gameId)
// Add score (zero-pad for string sorting)
String paddedScore = String.format("%010d", score);
String sk = "SCORE#" + paddedScore + "#" + playerId;
// Get top scores (descending)
QueryRequest request = QueryRequest.builder()
.tableName("Leaderboard")
.keyConditionExpression("PK = :pk")
.expressionAttributeValues(Map.of(
":pk", AttributeValue.builder().s("GAME#tetris").build()
))
.scanIndexForward(false) // Descending
.limit(100)
.build();
5. IoT Time-Series¶
// Table: SensorData
// PK: DEVICE#<deviceId>#<date> (bucket by day)
// SK: <timestamp>
// Write reading
String pk = "DEVICE#" + deviceId + "#" + LocalDate.now();
String sk = Instant.now().toString();
PutItemRequest request = PutItemRequest.builder()
.tableName("SensorData")
.item(Map.of(
"PK", AttributeValue.builder().s(pk).build(),
"SK", AttributeValue.builder().s(sk).build(),
"temperature", AttributeValue.builder().n("72.5").build(),
"humidity", AttributeValue.builder().n("45").build()
))
.build();
// Query day's readings
QueryRequest queryRequest = QueryRequest.builder()
.tableName("SensorData")
.keyConditionExpression("PK = :pk AND SK BETWEEN :start AND :end")
.expressionAttributeValues(Map.of(
":pk", AttributeValue.builder().s("DEVICE#sensor1#2024-01-15").build(),
":start", AttributeValue.builder().s("2024-01-15T00:00:00Z").build(),
":end", AttributeValue.builder().s("2024-01-15T23:59:59Z").build()
))
.build();
6. Transactions¶
// Transfer money between accounts atomically
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(
TransactWriteItem.builder()
.update(Update.builder()
.tableName("Accounts")
.key(Map.of("accountId", AttributeValue.builder().s("account1").build()))
.updateExpression("SET balance = balance - :amount")
.conditionExpression("balance >= :amount")
.expressionAttributeValues(Map.of(
":amount", AttributeValue.builder().n("100").build()
))
.build())
.build(),
TransactWriteItem.builder()
.update(Update.builder()
.tableName("Accounts")
.key(Map.of("accountId", AttributeValue.builder().s("account2").build()))
.updateExpression("SET balance = balance + :amount")
.expressionAttributeValues(Map.of(
":amount", AttributeValue.builder().n("100").build()
))
.build())
.build()
)
.build();
dynamoDb.transactWriteItems(request);
Secondary Indexes¶
Secondary Indexes¶
Creating GSI¶
// Create table with GSI
CreateTableRequest request = CreateTableRequest.builder()
.tableName("Users")
.keySchema(
KeySchemaElement.builder().attributeName("userId").keyType(KeyType.HASH).build()
)
.attributeDefinitions(
AttributeDefinition.builder().attributeName("userId").attributeType(ScalarAttributeType.S).build(),
AttributeDefinition.builder().attributeName("email").attributeType(ScalarAttributeType.S).build()
)
.globalSecondaryIndexes(
GlobalSecondaryIndex.builder()
.indexName("EmailIndex")
.keySchema(
KeySchemaElement.builder().attributeName("email").keyType(KeyType.HASH).build()
)
.projection(Projection.builder().projectionType(ProjectionType.ALL).build())
.build()
)
.billingMode(BillingMode.PAY_PER_REQUEST)
.build();
DynamoDB Streams¶
// Lambda trigger for stream
public class StreamHandler implements RequestHandler<DynamodbEvent, Void> {
@Override
public Void handleRequest(DynamodbEvent event, Context context) {
for (DynamodbStreamRecord record : event.getRecords()) {
if ("INSERT".equals(record.getEventName())) {
Map<String, AttributeValue> newImage = record.getDynamodb().getNewImage();
// Process new item
} else if ("MODIFY".equals(record.getEventName())) {
Map<String, AttributeValue> oldImage = record.getDynamodb().getOldImage();
Map<String, AttributeValue> newImage = record.getDynamodb().getNewImage();
// Process update
} else if ("REMOVE".equals(record.getEventName())) {
Map<String, AttributeValue> oldImage = record.getDynamodb().getOldImage();
// Process deletion
}
}
return null;
}
}
DAX (DynamoDB Accelerator)¶
// DAX client (same API as DynamoDB)
AmazonDaxClientBuilder daxBuilder = AmazonDaxClientBuilder.standard();
daxBuilder.withRegion("us-east-1")
.withEndpointConfiguration("dax-cluster.amazonaws.com");
AmazonDynamoDB daxClient = daxBuilder.build();
// Use just like DynamoDB client
daxClient.getItem(getItemRequest);
Global Tables¶
Trade-offs¶
| Pros | Cons |
|---|---|
| Fully managed | Vendor lock-in (AWS) |
| Serverless option | Complex pricing |
| Single-digit ms latency | Query limitations |
| Automatic scaling | No joins |
| Built-in security | Eventual consistency by default |
| Global tables | 400KB item size limit |
| ACID transactions | Hot partition issues |
| DynamoDB Streams | Expensive at scale |
Performance Characteristics¶
| Metric | Value |
|---|---|
| Read latency | 1-10ms (DAX: microseconds) |
| Write latency | 1-10ms |
| Item size limit | 400 KB |
| Partition throughput | 3000 RCU, 1000 WCU |
| GSIs per table | 20 |
| LSIs per table | 5 |
| Max query result | 1 MB |
Anti-Patterns¶
❌ Scan operations (full table scan)
Use Query with partition key instead
❌ Hot partitions
Use composite keys or add randomness
❌ Large items (>400KB)
Store in S3, reference in DynamoDB
❌ Relational data model
Denormalize, use single-table design
❌ Many small tables
Use single-table design with PK/SK patterns
❌ Filter instead of query
Design keys for access patterns
When to Use DynamoDB¶
Good For: - Serverless applications - Key-value lookups - Session management - Gaming leaderboards - IoT data - User profiles - Shopping carts - Catalog/inventory
Not Good For: - Ad-hoc queries - Complex relationships - Data warehousing - Full-text search - Large blob storage - Frequently changing access patterns
DynamoDB vs Alternatives¶
| Feature | DynamoDB | MongoDB | Cassandra | Redis |
|---|---|---|---|---|
| Type | Managed NoSQL | Document | Wide-column | In-memory |
| Hosting | AWS only | Any cloud | Any cloud | Any cloud |
| Consistency | Tunable | Tunable | Tunable | Strong |
| Scaling | Automatic | Manual | Manual | Manual |
| Transactions | Yes | Yes | Limited | Limited |
| Query Language | API/PartiQL | MQL | CQL | Commands |
| Latency | 1-10ms | 1-10ms | 1-10ms | <1ms |
Best Practices¶
- Single-table design - Multiple entity types in one table
- Design for access patterns - Know queries before designing
- Use composite sort keys - Enable hierarchical queries
- Avoid hot partitions - Distribute writes evenly
- Use sparse indexes - Only index when attribute exists
- Enable TTL - Auto-delete expired items
- Use on-demand for new apps - Switch to provisioned when patterns known
- Batch operations - Use BatchGetItem/BatchWriteItem
- Handle pagination - Use LastEvaluatedKey
- Monitor with CloudWatch - Track throttling, latency
API Cheat Sheet¶
// Put Item
PutItemRequest.builder()
.tableName("Table")
.item(Map.of("PK", AttributeValue.builder().s("pk").build()))
.build();
// Get Item
GetItemRequest.builder()
.tableName("Table")
.key(Map.of("PK", AttributeValue.builder().s("pk").build()))
.consistentRead(true) // Optional: strongly consistent
.build();
// Query
QueryRequest.builder()
.tableName("Table")
.keyConditionExpression("PK = :pk AND begins_with(SK, :prefix)")
.filterExpression("status = :status") // Post-filter (less efficient)
.expressionAttributeValues(Map.of(...))
.scanIndexForward(false) // Descending
.limit(100)
.build();
// Update
UpdateItemRequest.builder()
.tableName("Table")
.key(Map.of("PK", AttributeValue.builder().s("pk").build()))
.updateExpression("SET #name = :name, #count = #count + :inc")
.conditionExpression("attribute_exists(PK)")
.expressionAttributeNames(Map.of("#name", "name", "#count", "count"))
.expressionAttributeValues(Map.of(...))
.build();
// Delete
DeleteItemRequest.builder()
.tableName("Table")
.key(Map.of("PK", AttributeValue.builder().s("pk").build()))
.conditionExpression("attribute_exists(PK)")
.build();
// Batch Write (up to 25 items)
BatchWriteItemRequest.builder()
.requestItems(Map.of("Table", List.of(
WriteRequest.builder()
.putRequest(PutRequest.builder().item(...).build())
.build()
)))
.build();