Aws
AwsIntermediate

AWS DynamoDB Explained: Best Practices and Use Cases

14 min read
awsdynamodbnosqldatabaseserverless

TL;DR

A comprehensive guide to Amazon DynamoDB, including best practices, data modeling patterns, and real-world use cases for building scalable NoSQL applications.

DynamoDB: A Deep Dive for Experienced Engineers

$1

DynamoDB is a fully managed, serverless NoSQL key-value and document database offering single-digit millisecond latency at any scale. Its distributed, partition-based architecture provides inherent high availability and durability. Understanding its capacity modes (Provisioned vs. On-Demand), data modeling best practices (especially partition and sort keys), secondary indexes (GSIs, LSIs), and advanced features like Global Tables and DAX caching is critical for performance and cost optimization. Secure access via IAM and robust monitoring with CloudWatch are essential operational considerations.

$1

Amazon DynamoDB has become a cornerstone for cloud-native applications demanding predictable performance and elastic scalability. Unlike traditional relational databases, DynamoDB abstracts away infrastructure management, offering a serverless experience for both development and operations teams. Its distributed nature, coupled with a robust feature set, makes it suitable for a wide array of use cases, from high-throughput transactional systems to data-intensive IoT platforms. This guide focuses on the architectural considerations, best practices, and strategic application of DynamoDB for seasoned engineers.

``mermaid

flowchart TB

A[Application] --> B(DynamoDB Service);

B --> C{Partition Manager};

C --> D[Data Partition 1];

C --> E[Data Partition 2];

C --> F[Data Partition N];

D --> G[Replica AZ-1];

D --> H[Replica AZ-2];

D --> I[Replica AZ-3];

E --> J[Replica AZ-1];

E --> K[Replica AZ-2];

E --> L[Replica AZ-3];

F --> M[Replica AZ-1];

F --> N[Replica AZ-2];

F --> O[Replica AZ-3];

style B fill:#f9f,stroke:#333,stroke-width:2px

style C fill:#bbf,stroke:#333,stroke-width:2px

style D fill:#ddf,stroke:#333,stroke-width:1px

style E fill:#ddf,stroke:#333,stroke-width:1px

style F fill:#ddf,stroke:#333,stroke-width:1px

`

$1

DynamoDB's performance and scalability are rooted in its distributed, partition-based architecture. Data is automatically sharded across multiple storage nodes, each managed by a partition manager. This distribution ensures that read and write operations are spread across the underlying infrastructure, preventing single points of contention. Each partition is synchronously replicated across three Availability Zones (AZs) within a region, providing high availability and durability.

$1

The foundation of a DynamoDB table is its primary key, which consists of a partition key* and an optional *sort key. The partition key determines the logical partition (and thus, physical shard) where an item is stored. The partition key's cardinality and distribution are paramount for even data and request distribution, directly impacting performance and cost.

$1

DynamoDB offers two read consistency options:

* Eventually Consistent Reads (Default): In this model, a read request might return stale data if that data has been modified recently but the update has not yet propagated to all replicas. This is the lower-cost and higher-throughput option.

* Strongly Consistent Reads: A strongly consistent read guarantees that the response will reflect the result of the most recent write operation. This comes at the cost of higher latency and double the read capacity units (RCUs) compared to eventually consistent reads.

$1

Choosing the correct capacity mode is crucial for balancing performance, cost, and operational overhead.

$1

In Provisioned capacity mode, you explicitly define the read capacity units (RCUs) and write capacity units (WCUs) your table needs per second.

* Manual Scaling: You manually adjust RCUs/WCUs as your application's traffic fluctuates.

* Auto Scaling: Configures DynamoDB to automatically adjust provisioned capacity based on defined utilization targets and scaling policies. This is highly recommended over manual adjustments for most predictable workloads.

Trade-offs:

* Pros: Cost-effective for predictable, stable workloads. Offers maximum control over throughput.

* Cons: Requires upfront capacity planning. Risk of throttling if provisioned capacity is insufficient. Potential for over-provisioning and wasted cost if capacity is set too high.

$1

On-Demand capacity mode allows DynamoDB to automatically scale read and write throughput up and down to accommodate your workload. You pay per request.

Trade-offs:

* Pros: Eliminates the need for capacity planning and re-provisioning. Ideal for workloads with unpredictable traffic patterns or new applications where traffic is unknown.

* Cons: Generally more expensive than provisioned capacity for consistent, high-traffic workloads. No explicit control over provisioned throughput limits, though burst capacity is available.

| Feature | Provisioned Capacity | On-Demand Capacity |

| :----------------- | :------------------------------------------------------ | :------------------------------------------------ |

| Cost Model | Pay for provisioned RCUs/WCUs. | Pay per request. |

| Scaling | Manual adjustment or Auto Scaling. | Automatic scaling based on traffic. |

| Best For | Predictable, stable workloads. | Variable, unpredictable workloads, or new apps. |

| Capacity Planning| Required. | Not required. |

| Overhead | Higher operational overhead for planning/tuning. | Minimal operational overhead. |

$1

Effective data modeling is arguably the most critical factor for DynamoDB performance and scalability.

$1

* High Cardinality: The partition key should have a large number of distinct values.

* Even Distribution: Distribute data and request load as evenly as possible across partitions.

* Avoid Hot Keys: A "hot key" is a partition key that receives a disproportionately high volume of traffic, leading to throttling even if overall capacity is sufficient.

* Single-Key Tables: For many applications, a single table with a carefully designed partition key (and sort key) can suffice, avoiding the need for multiple tables and complex join logic.

$1

The sort key allows you to order items within a partition and retrieve ranges of data efficiently. It is essential for implementing patterns like:

* Time Series Data: Partition by a user/device ID, sort by timestamp.

* Adjacency Lists: Partition by an entity ID, sort by relationship type or related entity ID.

$1

Consider a user session table.

`json

{

"userId": "user123",

"sessionId": "sessionabc",

"loginTime": "2023-10-27T10:00:00Z",

"lastActivity": "2023-10-27T10:30:00Z",

"data": { ... }

}

`

Partition Key: userId

Sort Key: sessionId

This design allows for:

* Retrieving all sessions for a specific user.

* Retrieving a specific session by userId and sessionId.

* Ordering sessions by sessionId (if timestamp-like, e.g., loginTime was used as sort key, it would be ordered by login time).

Querying for the last active session of a user:

`python

import boto3

dynamodb = boto3.resource('dynamodb')

table = dynamodb.Table('UserSessions')

response = table.query(

KeyConditionExpression=boto3.dynamodb.conditions.Key('userId').eq('user123'),

ScanIndexForward=False, # Get most recent first

Limit=1

)

last_session = response['Items'][0] if response['Items'] else None

print(last_session)

`

$1

When your query patterns don't align with your primary key, secondary indexes are your solution.

$1

GSIs are independent indexes with their own provisioned throughput settings. They can have a different partition and sort key than the base table, allowing for flexible query patterns across the entire table.

* Use Case: Finding all orders for a specific productId when the base table is partitioned by orderId.

* Trade-offs: Adds cost (provisioned throughput and storage). Write operations to the base table incur a write to the GSI, consuming WCUs and increasing latency. Potential for eventual consistency issues if not carefully managed.

$1

LSIs share the same partition key as the base table but have a different sort key. They reside within the same partition as the base table's data.

Use Case: Querying items within the *same partition by a different sort attribute. E.g., querying all items for userId='user123' but sorting by lastActivity instead of sessionId.

* Trade-offs: Limited to a single partition key. Subject to the 10 GB per partition size limit. Data is strongly consistent. Only one LSI per table.

$1

A sparse index includes only those items that match a specific condition (e.g., an item has a certain attribute present). This can save costs and improve performance by reducing the index size.

* Example: An index for completed tasks in a task management system, where only items with a status='completed' attribute are indexed.

$1

$1

Global Tables provide a multi-region, active-active database solution. Changes to data in one region are automatically and asynchronously replicated to all other regions.

`mermaid

graph LR

subgraph "Region 1 (us-east-1)"

A[Table Replica A]

end

subgraph "Region 2 (eu-west-1)"

B[Table Replica B]

end

subgraph "Region 3 (ap-southeast-2)"

C[Table Replica C]

end

A -- Replicates --> B

B -- Replicates --> A

A -- Replicates --> C

C -- Replicates --> A

B -- Replicates --> C

C -- Replicates --> B

`

Example CloudFormation for Global Tables:

`yaml

Resources:

GlobalUsersTable:

Type: 'AWS::DynamoDB::GlobalTable'

Properties:

TableName: GlobalUsers

AttributeDefinitions:

- AttributeName: userId

AttributeType: S

KeySchema:

- AttributeName: userId

KeyType: HASH

Replicas:

- Region: us-east-1

PointInTimeRecoverySpecification:

PointInTimeRecoveryEnabled: true

- Region: eu-west-1

PointInTimeRecoverySpecification:

PointInTimeRecoveryEnabled: true

StreamSpecification:

StreamViewType: NEW_AND_OLD_IMAGES # Required for Global Tables

BillingMode: PAY_PER_REQUEST

`

* Use Case: Disaster recovery, lower latency for globally distributed users.

* Trade-offs: Increased cost due to replicated data and write operations across regions. Potential for write conflicts (though DynamoDB resolves them deterministically).

$1

DAX is a fully managed, highly available, in-memory cache for DynamoDB. It provides microsecond read latency for frequently accessed data.

* Caching Modes:

* Read Cache: Caches GET and QUERY/SCAN results.

* Write-Through Cache: Writes go to DAX first, then to DynamoDB.

* Eventual Consistency: DAX is eventually consistent with DynamoDB.

Performance Optimization Strategies:

`mermaid

mindmap

root((DynamoDB Performance Optimization))

Partition Key Design

High Cardinality

Uniform Distribution

Avoid Hot Partitions

Sort Key Design

Efficient Range Queries

Single-Table Design Enablement

Secondary Indexes

GSI for Diverse Queries

LSI for In-Partition Queries

Sparse Indexes for Cost Savings

DAX Caching

Microsecond Latency

Read Heavy Workloads

Write Through/Behind Options

Capacity Management

On-Demand for Variable Loads

Provisioned + Auto Scaling for Predictable Loads

Reserved Capacity for Consistent High Throughput

Query Optimization

Use KeyConditionExpressions

FilterExpressions Post-Read

Limit Scans Where Possible

Data Modeling

Denormalization vs. Normalization

Single-Table Design

`

$1

$1

DynamoDB leverages AWS Identity and Access Management (IAM) for fine-grained access control.

* Table-Level Permissions: Grant or deny access to specific tables.

* Item-Level Permissions: Use IAM policies with conditions to restrict access to specific items based on partition keys or other item attributes.

* Attribute-Level Permissions: (Less common, via VPC Endpoints with granular policies) Control access to specific attributes.

Example IAM Policy for Item-Level Access Control (User can only access their own data):

`json

{

"Version": "2012-10-17",

"Statement": [

{

"Effect": "Allow",

"Action": [

"dynamodb:GetItem",

"dynamodb:PutItem",

"dynamodb:UpdateItem",

"dynamodb:DeleteItem",

"dynamodb:Query",

"dynamodb:Scan"

],

"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/MyUserTable",

"Condition": {

"ForAllValues:StringEquals": {

"dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]

}

}

}

]

}

`

Note: ${cognito-identity.amazonaws.com:sub} is a placeholder for a Cognito Identity ID, often used to represent a user's unique identifier.

$1

* Encryption at Rest: DynamoDB encrypts all data at rest by default using AWS-owned keys, AWS-managed keys, or customer-managed keys (CMKs) via AWS KMS.

* Encryption in Transit: Data is encrypted in transit using TLS when accessed via the AWS SDKs or API.

$1

* AWS CloudWatch: Essential for monitoring table metrics (Consumed Capacity, Throttled Requests, Latency, etc.) and setting alarms.

* AWS CloudTrail: Logs API calls made to DynamoDB for auditing purposes.

Example CloudWatch Dashboard Configuration:

`yaml

Resources:

DynamoDBPerformanceDashboard:

Type: 'AWS::CloudWatch::Dashboard'

Properties:

DashboardName: DynamoDBPerformanceMetrics

DashboardBody: !Sub |

{

"widgets": [

{

"type": "metric",

"properties": {

"metrics": [

["AWS/DynamoDB", "ConsumedReadCapacityUnits", "TableName", "${TableName}"],

[".", "ConsumedWriteCapacityUnits", ".", "."],

[".", "ThrottledRequests", ".", "."],

[".", "SuccessfulRequestLatency", ".", "."]

],

"period": 300,

"stat": "Sum",

"region": "${AWS::Region}",

"title": "Table Throughput and Latency"

}

},

{

"type": "text",

"properties": {

"text": "Key metrics for DynamoDB performance monitoring."

}

}

]

}

`

$1

$1

A common pattern involves denormalizing order data for fast retrieval.

`mermaid

erDiagram

CUSTOMER ||--o{ ORDER : "places"

ORDER ||--|{ ORDER_ITEM : "contains"

PRODUCT ||--o{ ORDER_ITEM : "is part of"

CUSTOMER {

string customerId PK

string name

string email

string address

}

ORDER {

string orderId PK

string customerId FK "GSI partition key"

datetime orderDate

string status

string shippingAddress

float totalAmount

}

ORDER_ITEM {

string orderId PK

string orderItemId SK "Unique item ID within order"

string productId FK

number quantity

number price

string productName

}

PRODUCT {

string productId PK

string name

float price

number stock

}

`

DynamoDB Table Structure:

* Order Table:

* Partition Key: orderId

* Attributes: customerId, orderDate, status, totalAmount, shippingAddress

* Global Secondary Index (GSI) on customerId to retrieve all orders for a customer.

* Order Items Table (or integrated into Order Table):

* Partition Key: orderId

* Sort Key: orderItemId

* Attributes: productId, quantity, price, productName

Rationale:

* Retrieving an order by orderId is a direct GetItem operation.

* Retrieving all orders for a customer is a Query on the customerId GSI.

* Retrieving items within an order is a Query on the orderId (base table) with an optional orderItemId sort.

$1

Leaderboards are a prime use case for DynamoDB's sort key capabilities.

Table Structure:

* Partition Key: gameId

* Sort Key: score (descending) or userId

* Attributes: userId, score, rank, achievements, lastUpdated

Example Leaderboard Query (Top 10 players for a specific game):

`python

import boto3

dynamodb = boto3.resource('dynamodb')

table = dynamodb.Table('Leaderboards')

response = table.query(

KeyConditionExpression=boto3.dynamodb.conditions.Key('gameId').eq('game-xyz'),

ScanIndexForward=False, # Order by score descending

Limit=10

)

leaderboard_entries = response['Items']

print(leaderboard_entries)

`

* Alternative: Partition by gameId, Sort by userId. Create a GSI partitioned by gameId and sorted by score descending to achieve the same leaderboard. This latter approach might be more efficient if many players have the same score.

$1

Storing time-series sensor data.

Table Structure:

* Partition Key: deviceId

* Sort Key: timestamp (ISO 8601 format string)

* Attributes: sensorType, reading, unit, location

Example Data Ingestion Pattern:

`python

import boto3

from datetime import datetime

dynamodb = boto3.resource('dynamodb')

table = dynamodb.Table('SensorReadings')

device_id = "sensor-123"

current_time = datetime.utcnow().isoformat() + "Z"

reading_data = {

'deviceId': device_id,

'timestamp': current_time,

'sensorType': 'temperature',

'reading': 25.5,

'unit': 'Celsius'

}

response = table.put_item(Item=reading_data)

print(f"PutItem succeeded: {response}")

`

Querying for recent readings from a device:

`python

import boto3

from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')

table = dynamodb.Table('SensorReadings')

device_id = "sensor-123"

Get readings from the last hour (assuming timestamp is ISO 8601)

one_hour_ago = (datetime.utcnow() - timedelta(hours=1)).isoformat() + "Z"

response = table.query(

KeyConditionExpression=Key('deviceId').eq(device_id) & Key('timestamp').gt(one_hour_ago)

)

recent_readings = response['Items']

print(f"Found {len(recent_readings)} readings for {device_id} in the last hour.")

`

TTL (Time To Live): DynamoDB TTL can automatically delete old data to manage storage costs. Configure a TTL attribute (e.g., ttlTimestamp`) and set an expiration time.

$1

DynamoDB is a powerful tool, but it's not a universal solution. Consider alternatives when:

* Complex Relational Queries are Required: If your application heavily relies on complex joins across many tables, ACID transactions with referential integrity, or ad-hoc analytical queries, a relational database (like Amazon RDS or Aurora) is likely a better fit.

* Schema Flexibility is Paramount and Unstructured: While DynamoDB is schema-flexible at the attribute level, it's not designed for truly unstructured or highly variable JSON documents that require deep querying capabilities on arbitrary fields without pre-defined indexes. Services like Amazon DocumentDB or Elasticsearch might be more suitable.

* Cost Predictability for Very Low Throughput is Critical: For extremely low, consistent, and predictable workloads where even the base cost of On-Demand can be a concern, carefully provisioned RDS might be cheaper. However, this is rare for cloud-native applications.

* Full ACID Transactions Across Multiple Items/Tables: DynamoDB supports atomic counters and conditional writes for single-item operations. For multi-item ACID transactions, consider Aurora Serverless or RDS. DynamoDB introduced transactional APIs, but their scope is different from traditional RDBMS ACID.

$1

Use this checklist to assess if DynamoDB is the right choice for your project:

* [ ] Scalability Needs: Does the application require high, predictable read/write throughput that scales elastically?

* [ ] Performance Requirements: Is single-digit millisecond latency critical for core operations?

* [ ] Data Model: Is the data primarily key-value or document-oriented? Can it be modeled effectively with partition and sort keys?

* [ ] Query Patterns: Are query patterns well-defined and can they be supported by primary keys and secondary indexes?

* [ ] Operational Overhead: Is minimizing infrastructure management and server maintenance a priority?

* [ ] Cost Model: Is a pay-per-request or provisioned-throughput cost model acceptable?

* [ ] ACID Transaction Needs: Are multi-item ACID transactions a primary requirement? (If yes, reconsider DynamoDB for that specific part of the system, or explore its transactional capabilities with awareness of scope.)

* [ ] Relational Complexity: Does the application require complex joins, foreign keys, and relational integrity checks across many entities? (If yes, consider RDS/Aurora.)

By carefully evaluating these points, engineering teams can make informed decisions about leveraging DynamoDB's strengths effectively.

Why This Matters

Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

Trade-offs to Consider

Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

When NOT to Use This

Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

Decision Framework

Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.