AWS DynamoDB: NoSQL Database Design
Amazon DynamoDB is a fully managed, serverless NoSQL database that delivers single-digit millisecond performance at virtually any scale. But unlocking that performance requires a fundamentally different design mindset than the relational databases many engineers are accustomed to. This post walks through the core concepts and best practices for designing effective DynamoDB tables.
Relational vs. NoSQL Thinking
In relational databases, you normalize data first and figure out queries later. In DynamoDB, you invert this: you must understand your access patterns before you design your schema. DynamoDB is optimized for known, repeatable queries—not ad hoc analytics.
If you find yourself needing flexible, unpredictable queries, DynamoDB may be the wrong tool. But for high-scale applications with well-defined access patterns, it excels.
Core Building Blocks
Primary Keys
Every DynamoDB item is uniquely identified by a primary key, which comes in two forms:
- Partition key (simple): A single attribute that determines the physical partition where data is stored.
- Partition key + sort key (composite): Enables multiple items under the same partition key, sorted by the sort key.
The partition key is hashed to distribute data across partitions. Choosing a high-cardinality partition key is critical to avoid "hot partitions" that throttle throughput.
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.create_table(
TableName='Orders',
KeySchema=[
{'AttributeName': 'PK', 'KeyType': 'HASH'}, # Partition key
{'AttributeName': 'SK', 'KeyType': 'RANGE'} # Sort key
],
AttributeDefinitions=[
{'AttributeName': 'PK', 'AttributeType': 'S'},
{'AttributeName': 'SK', 'AttributeType': 'S'}
],
BillingMode='PAY_PER_REQUEST'
)
Single-Table Design
One of DynamoDB's most powerful (and counterintuitive) patterns is single-table design—storing multiple entity types in one table. This reduces round trips and leverages DynamoDB's ability to fetch related items in a single query.
Consider an e-commerce application with users and orders. Instead of two tables, we use generic PK and SK attributes:
| PK | SK | Attributes |
|---|---|---|
USER#123 |
PROFILE#123 |
name, email |
USER#123 |
ORDER#1001 |
total, status |
USER#123 |
ORDER#1002 |
total, status |
Now, a single query fetches a user and all their orders:
from boto3.dynamodb.conditions import Key
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#123') &
Key('SK').begins_with('ORDER#')
)
The begins_with condition on the sort key lets us slice related items efficiently.
Secondary Indexes
When you need to query on attributes other than the primary key, use secondary indexes.
Global Secondary Index (GSI)
A GSI has a completely different partition and sort key from the base table. It's stored separately and updated asynchronously (eventually consistent).
GlobalSecondaryIndexes=[
{
'IndexName': 'StatusIndex',
'KeySchema': [
{'AttributeName': 'status', 'KeyType': 'HASH'},
{'AttributeName': 'createdAt', 'KeyType': 'RANGE'}
],
'Projection': {'ProjectionType': 'ALL'}
}
]
Local Secondary Index (LSI)
An LSI shares the partition key of the base table but uses an alternate sort key. LSIs must be created at table creation time and support strong consistency.
Rule of thumb: Prefer GSIs for flexibility. Use LSIs only when you need strong consistency on an alternate sort key.
Overloading Keys and Indexes
To support many access patterns without proliferating indexes, DynamoDB practitioners often overload GSIs. By using generic index attribute names (e.g., GSI1PK, GSI1SK), a single index can serve multiple entity types and query patterns.
This maximizes the value of your (limited) indexes while keeping your data model compact.
Capacity and Cost Considerations
DynamoDB offers two capacity modes:
- On-demand: Pay per request. Ideal for unpredictable or spiky workloads.
- Provisioned: Reserve read/write capacity units (RCUs/WCUs). Cheaper for steady, predictable traffic; supports auto-scaling.
Every read and write consumes capacity based on item size. Design tips to control cost:
- Keep items small (max 400 KB per item).
- Use
ProjectionExpressionto fetch only needed attributes. - Avoid scans; they read every item in the table.
Common Anti-Patterns
- Hot partitions: Using low-cardinality partition keys (like a boolean or status flag).
-
Overusing
Scan: Always preferQuerywith a key condition. - Storing large blobs: Offload big objects to S3 and store references in DynamoDB.
- Treating it like SQL: Attempting joins or complex aggregations client-side at scale.
Conclusion
DynamoDB rewards upfront design discipline. By mapping your access patterns first, embracing single-table design, and thoughtfully leveraging secondary indexes, you can build systems that scale seamlessly with pred
Top comments (0)