Skip to content

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

DynamoDB 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

DynamoDB 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

DynamoDB GSI and LSI

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

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)

DynamoDB DAX

// 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

DynamoDB 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

  1. Single-table design - Multiple entity types in one table
  2. Design for access patterns - Know queries before designing
  3. Use composite sort keys - Enable hierarchical queries
  4. Avoid hot partitions - Distribute writes evenly
  5. Use sparse indexes - Only index when attribute exists
  6. Enable TTL - Auto-delete expired items
  7. Use on-demand for new apps - Switch to provisioned when patterns known
  8. Batch operations - Use BatchGetItem/BatchWriteItem
  9. Handle pagination - Use LastEvaluatedKey
  10. 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();