DynamoDB Advanced: Single-Table Design & DAX Optimization
Master DynamoDB single-table design and DAX caching.
20+ years shipping high-throughput database systems. Drawn from code that ran under real load.
- ✓Basic understanding of DynamoDB (tables, items, keys)
- ✓Familiarity with Python and boto3
- ✓AWS account with permissions to create DynamoDB tables and DAX clusters
- Single-table design stores multiple entity types in one table using composite keys.
- DAX (DynamoDB Accelerator) is a fully managed in-memory cache that reduces read latency to microseconds.
- Access patterns drive table design, not entity relationships.
- Overloading a table with too many entity types can lead to hot partitions.
- DAX is ideal for read-heavy, latency-sensitive workloads like gaming leaderboards.
Think of a single-table design like a filing cabinet where you store all your documents (invoices, contracts, receipts) in one drawer, but you label each folder with a unique code that tells you what it is and how to find it. DAX is like a personal assistant who keeps the most frequently requested folders on their desk, so you don't have to walk to the cabinet every time.
When you first learn DynamoDB, the natural instinct is to design tables the same way you would in a relational database: one table per entity. But DynamoDB is not relational—it's a key-value and document database optimized for scale. The real power of DynamoDB emerges when you embrace single-table design, a pattern where you store multiple entity types (e.g., users, orders, products) in a single table, using clever key structures to model complex relationships. This approach reduces the number of network calls, avoids expensive joins (which DynamoDB doesn't support), and keeps your data access patterns fast and predictable.
But with great power comes great responsibility. A poorly designed single table can lead to hot partitions, throttling, and skyrocketing costs. That's where DynamoDB Accelerator (DAX) comes in—a fully managed, in-memory cache that can reduce read latencies from single-digit milliseconds to microseconds. DAX is not a silver bullet; it works best for read-heavy, eventually consistent workloads. In this tutorial, you'll learn how to design a single-table schema for an e-commerce platform, implement it with Python (boto3), and integrate DAX to handle peak traffic. We'll also dissect a real production incident where a missing DAX cluster caused a 10-second latency spike during a flash sale. By the end, you'll be equipped to build scalable, high-performance DynamoDB applications.
Understanding Single-Table Design
Single-table design is a DynamoDB modeling technique where you store multiple entity types (e.g., users, orders, products) in one table. The goal is to support all your application's access patterns with as few tables as possible—ideally one. This is possible because DynamoDB's flexible schema allows items to have different attributes. The key is to design a composite primary key (partition key + sort key) that can uniquely identify any item and also enable efficient queries across entities.
For example, consider an e-commerce platform. You have Users, Orders, and Products. In a relational database, you'd have three tables. In DynamoDB, you can store them in one table with a partition key like 'PK' and a sort key like 'SK'. A user item might have PK='USER#123' and SK='PROFILE', while an order item might have PK='USER#123' and SK='ORDER#456'. This allows you to fetch all orders for a user with a single query on PK='USER#123' and SK beginning with 'ORDER#'.
The benefits are significant: fewer network round trips (no joins), simpler application code, and lower cost (one table to provision). However, it requires careful planning. You must identify all access patterns upfront and design your keys accordingly. A common mistake is to over-normalize and create separate tables for each entity, defeating the purpose of DynamoDB.
Designing Access Patterns with GSIs
Global Secondary Indexes (GSIs) are essential for supporting access patterns that your primary key cannot satisfy. In single-table design, you often create multiple GSIs to enable different query paths. Each GSI has its own partition key and sort key, and you can project attributes to control cost.
For example, in our e-commerce app, you might need to find all orders by status (e.g., 'SHIPPED'). The primary key is organized by user, so you need a GSI with partition key 'order_status' and sort key 'order_date'. You can store the status in a attribute like 'GSI1PK' and date in 'GSI1SK'.
When designing GSIs, remember that they are eventually consistent (unless you use consistent reads, which cost more). Also, each GSI incurs additional write capacity costs because every write to the table also writes to the GSI. Therefore, only create GSIs that you actually need.
A common pattern is to use a 'GSI Overloading' technique where the same GSI is used for multiple entity types by using different key prefixes. For instance, GSI1PK could be 'STATUS#SHIPPED' for orders and 'CATEGORY#ELECTRONICS' for products. This keeps the number of GSIs low.
Implementing DAX for Read Performance
DynamoDB Accelerator (DAX) is an in-memory cache that sits between your application and DynamoDB. It can reduce read latencies from single-digit milliseconds to microseconds for eventually consistent reads. DAX is ideal for read-heavy workloads like gaming leaderboards, social media feeds, or any application where you read the same data frequently.
DAX works by caching the results of GetItem, BatchGetItem, Query, and Scan operations. It supports both eventually consistent and strongly consistent reads (though strongly consistent reads bypass the cache). You can also configure TTL for cached items.
To use DAX, you create a DAX cluster (which is a cluster of nodes) and then update your application to use the DAX client endpoint instead of the DynamoDB endpoint. The DAX client is a drop-in replacement for the DynamoDB client.
Important considerations: DAX is not free; you pay for the cluster nodes. Also, DAX is not suitable for write-heavy workloads because writes still go to DynamoDB directly. Finally, DAX is eventually consistent by default, so if you need strongly consistent reads, you should not use DAX for those operations.
Advanced DAX Patterns: Write-Through and TTL
To get the most out of DAX, you can implement write-through caching, where you update the cache on every write. However, DAX does not automatically invalidate cache on writes; you must manually update or invalidate the cache. One approach is to use DAX's 'PutItem' with the same key to update the cache after a write to DynamoDB.
Another pattern is to use TTL (Time-to-Live) to automatically expire cached items. You can set TTL at the item level when writing to DAX. This is useful for data that changes periodically, like a daily leaderboard.
You can also use DAX's 'GetItem' with 'ConsistentRead=True' to bypass the cache for strongly consistent reads. But this defeats the purpose of caching, so use it sparingly.
A common mistake is to cache too much data, leading to high memory usage and evictions. Only cache frequently accessed items. Use CloudWatch metrics to monitor cache utilization.
Common Pitfalls in Single-Table Design
Single-table design is powerful but easy to get wrong. Here are common mistakes:
- Hot Partitions: If you use a partition key that doesn't distribute well (e.g., a single user ID for all orders), that partition can become hot and throttle. Solution: use a more granular partition key, like USER#ID for user-related items, and consider using a composite key that includes a date or random suffix.
- Large Items: Storing too many attributes in one item can exceed the 400 KB item size limit. Solution: split large entities into multiple items (e.g., separate profile and preferences).
- Too Many GSIs: Each GSI adds write cost and complexity. Only create GSIs for access patterns you actually use.
- Ignoring Sort Key Order: DynamoDB sorts items by sort key within a partition. Use this to your advantage (e.g., store timestamps in ISO 8601 format to enable range queries).
- Not Planning for All Access Patterns: If you miss an access pattern, you may need to add a GSI later, which requires a data migration. Always list all access patterns before designing.
Migrating from Relational to Single-Table Design
Migrating an existing relational database to DynamoDB single-table design requires careful planning. Start by listing all your application's access patterns. For each pattern, determine the primary key and any GSIs needed.
For example, if you have a Users table and an Orders table in SQL, you might combine them into one DynamoDB table. Users become items with PK='USER#id' and SK='PROFILE'. Orders become items with PK='USER#id' and SK='ORDER#order_id'. You can then query all orders for a user with a single query.
You'll also need to handle relationships. In SQL, you use foreign keys. In DynamoDB, you embed related data or use composite keys. For example, an order might include product IDs as a list attribute, or you might store product details in a separate item with PK='PRODUCT#id'.
Data migration can be done using DynamoDB's import/export tools or by writing a script that reads from SQL and writes to DynamoDB. Be sure to test with a subset of data first.
Cost Optimization with Single-Table and DAX
Cost is a major consideration in DynamoDB. Single-table design can reduce costs by minimizing the number of tables (each table has a minimum provisioned capacity). However, it can also increase costs if you store large items or have many GSIs.
- Use on-demand capacity for unpredictable workloads, but be aware that it can be more expensive than provisioned for steady traffic.
- Use TTL to automatically delete expired data.
- Compress attributes (e.g., use short attribute names).
- Project only necessary attributes in GSIs.
- Use DAX to reduce read capacity consumption: each cache hit saves one read capacity unit.
DAX itself has a cost (per node hour). For read-heavy workloads, the savings in read capacity can offset the DAX cost. Use the AWS Pricing Calculator to estimate.
The Flash Sale That Brought Down the Leaderboard
- Always design GSIs to support your exact access patterns, especially sorting.
- Use DAX for read-heavy, latency-sensitive workloads to absorb traffic spikes.
- Monitor throttling and latency metrics in CloudWatch to catch issues early.
- Test with production-scale data before going live.
aws dynamodb describe-table --table-name my-tableaws dax describe-clusters --cluster-name my-cluster| File | Command / Code | Purpose |
|---|---|---|
| single_table_schema.py | dynamodb = boto3.resource('dynamodb') | Understanding Single-Table Design |
| gsi_query.py | dynamodb = boto3.resource('dynamodb') | Designing Access Patterns with GSIs |
| dax_client.py | from amazon.dax import AmazonDaxClient | Implementing DAX for Read Performance |
| dax_write_through.py | from amazon.dax import AmazonDaxClient | Advanced DAX Patterns |
| migration_script.py | dynamodb = boto3.resource('dynamodb') | Migrating from Relational to Single-Table Design |
| cost_optimization.py | table.put_item( | Cost Optimization with Single-Table and DAX |
Key takeaways
Common mistakes to avoid
3 patternsUsing the same partition key for all items (e.g., 'PK' = 'APP')
Creating too many GSIs without considering write costs
Not setting TTL on DAX cached items
Interview Questions on This Topic
Explain how you would design a single table for a social media app with users, posts, and comments.
Frequently Asked Questions
20+ years shipping high-throughput database systems. Drawn from code that ran under real load.
That's NoSQL. Mark it forged?
5 min read · try the examples if you haven't