Home Database DynamoDB Advanced: Single-Table Design & DAX Optimization
Advanced 5 min · July 13, 2026

DynamoDB Advanced: Single-Table Design & DAX Optimization

Master DynamoDB single-table design and DAX caching.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic understanding of DynamoDB (tables, items, keys)
  • Familiarity with Python and boto3
  • AWS account with permissions to create DynamoDB tables and DAX clusters
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is DynamoDB Advanced?

DynamoDB single-table design is a data modeling technique where you store multiple entity types in one table using composite primary keys and global secondary indexes to support all your application's access patterns.

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.
Plain-English First

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.

single_table_schema.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ECommerce')

# Insert a user item
table.put_item(
    Item={
        'PK': 'USER#123',
        'SK': 'PROFILE',
        'name': 'Alice',
        'email': 'alice@example.com'
    }
)

# Insert an order item for the same user
table.put_item(
    Item={
        'PK': 'USER#123',
        'SK': 'ORDER#456',
        'order_date': '2024-01-15',
        'total': 99.99
    }
)

# Query all items for user 123
response = table.query(
    KeyConditionExpression='PK = :pk',
    ExpressionAttributeValues={':pk': 'USER#123'}
)
for item in response['Items']:
    print(item)
Output
{'PK': 'USER#123', 'SK': 'PROFILE', 'name': 'Alice', 'email': 'alice@example.com'}
{'PK': 'USER#123', 'SK': 'ORDER#456', 'order_date': '2024-01-15', 'total': 99.99}
💡Key Naming Convention
📊 Production Insight
In production, always use a naming convention like 'ENTITY#ID' for partition keys to avoid ambiguity. Also, consider using a single attribute for the partition key (e.g., 'pk') and sort key (e.g., 'sk') to simplify code.
🎯 Key Takeaway
Single-table design uses composite keys to store multiple entities in one table, enabling efficient queries for all access patterns.

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.

gsi_query.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ECommerce')

# Query orders by status using GSI
response = table.query(
    IndexName='StatusDateIndex',
    KeyConditionExpression='GSI1PK = :status',
    ExpressionAttributeValues={':status': 'STATUS#SHIPPED'}
)
for item in response['Items']:
    print(item['SK'], item['order_date'])
Output
ORDER#456 2024-01-15
ORDER#789 2024-01-16
⚠ GSI Write Costs
📊 Production Insight
In production, monitor GSI usage with CloudWatch. If a GSI is rarely used, consider removing it to save costs. Also, be aware of GSI partition limits: each GSI partition can hold up to 10 GB of data.
🎯 Key Takeaway
GSIs enable alternative access patterns but add write costs. Use them sparingly and consider overloading them with different entity types.

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.

dax_client.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
import boto3
from amazon.dax import AmazonDaxClient

# Create a DAX client
session = boto3.Session()
dax_client = AmazonDaxClient(session, endpoints=['my-cluster.dax-cluster.us-east-1.amazonaws.com:8111'])

# Use DAX client like a regular DynamoDB client
table = dax_client.Table('ECommerce')
response = table.get_item(Key={'PK': 'USER#123', 'SK': 'PROFILE'})
print(response['Item'])
Output
{'PK': 'USER#123', 'SK': 'PROFILE', 'name': 'Alice', 'email': 'alice@example.com'}
🔥DAX Consistency
📊 Production Insight
In production, monitor DAX cache hit ratio. A low hit ratio indicates that your access patterns are not cache-friendly. Consider adjusting TTL or pre-warming the cache.
🎯 Key Takeaway
DAX reduces read latency to microseconds for eventually consistent reads. It's a drop-in replacement for the DynamoDB client.

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.

dax_write_through.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import boto3
from amazon.dax import AmazonDaxClient

session = boto3.Session()
dax_client = AmazonDaxClient(session, endpoints=['my-cluster.dax-cluster.us-east-1.amazonaws.com:8111'])
dynamodb = session.resource('dynamodb')

table_dax = dax_client.Table('ECommerce')
table_ddb = dynamodb.Table('ECommerce')

# Write to DynamoDB first
table_ddb.put_item(Item={'PK': 'USER#123', 'SK': 'PROFILE', 'name': 'Alice'})

# Then write to DAX to update cache
table_dax.put_item(Item={'PK': 'USER#123', 'SK': 'PROFILE', 'name': 'Alice'})

# Read from DAX (will be fast)
response = table_dax.get_item(Key={'PK': 'USER#123', 'SK': 'PROFILE'})
print(response['Item'])
Output
{'PK': 'USER#123', 'SK': 'PROFILE', 'name': 'Alice'}
💡Cache Invalidation
📊 Production Insight
In production, use DAX's TTL feature to expire stale data. For example, set TTL to 5 minutes for a leaderboard that updates every 10 minutes.
🎯 Key Takeaway
Write-through caching and TTL are essential for keeping DAX cache consistent with DynamoDB.

Common Pitfalls in Single-Table Design

Single-table design is powerful but easy to get wrong. Here are common mistakes:

  1. 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.
  2. 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).
  3. Too Many GSIs: Each GSI adds write cost and complexity. Only create GSIs for access patterns you actually use.
  4. 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).
  5. 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.
hot_partition_example.pyPYTHON
1
2
3
4
5
6
# Bad: partition key is just user ID, causing hot partition for active users
# PK = 'USER#123', SK = 'ORDER#456'

# Better: use a composite partition key that includes a date or random suffix
# PK = 'USER#123#2024-01', SK = 'ORDER#456'
# This distributes orders across multiple partitions by month.
⚠ Hot Partitions
📊 Production Insight
In production, use CloudWatch metrics like 'ConsumedReadCapacityUnits' per partition to detect hot partitions. If you see a partition using more than 80% of its capacity, consider re-sharding.
🎯 Key Takeaway
Avoid hot partitions by designing partition keys that distribute traffic evenly. Plan all access patterns upfront.

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.

migration_script.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import boto3
import pymysql

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ECommerce')

# Connect to MySQL
conn = pymysql.connect(host='localhost', user='user', password='pass', db='ecommerce')
cursor = conn.cursor()

# Migrate users
cursor.execute('SELECT id, name, email FROM users')
for row in cursor:
    table.put_item(Item={'PK': f'USER#{row[0]}', 'SK': 'PROFILE', 'name': row[1], 'email': row[2]})

# Migrate orders
cursor.execute('SELECT id, user_id, order_date, total FROM orders')
for row in cursor:
    table.put_item(Item={'PK': f'USER#{row[1]}', 'SK': f'ORDER#{row[0]}', 'order_date': str(row[2]), 'total': row[3]})

conn.close()
🔥Migration Strategy
📊 Production Insight
In production, consider using AWS Database Migration Service (DMS) for continuous replication from SQL to DynamoDB during migration.
🎯 Key Takeaway
Migrating to single-table design requires mapping all access patterns to keys. Use scripts to transform relational data into DynamoDB items.

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.

To optimize costs
  • 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.

cost_optimization.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
# Example: Using TTL to expire old orders
import time

table.put_item(
    Item={
        'PK': 'USER#123',
        'SK': 'ORDER#456',
        'order_date': '2024-01-15',
        'ttl': int(time.time()) + 86400  # expire in 1 day
    }
)
💡Short Attribute Names
📊 Production Insight
In production, set up billing alerts and monitor cost per table. Use DynamoDB's 'TimeToLive' feature to automatically delete old data and reduce storage costs.
🎯 Key Takeaway
Cost optimization involves reducing item size, using TTL, projecting only necessary attributes in GSIs, and leveraging DAX to reduce read capacity.
● Production incidentPOST-MORTEMseverity: high

The Flash Sale That Brought Down the Leaderboard

Symptom
Users saw leaderboard loading times of 10+ seconds during a flash sale, and some requests timed out.
Assumption
The developer assumed that DynamoDB's provisioned capacity was sufficient and that the leaderboard query was efficient.
Root cause
The leaderboard query scanned the entire table to compute rankings because the GSI was missing the sort key for score. Additionally, no caching layer was in place, so every request hit the table directly.
Fix
Created a GSI with partition key as game_id and sort key as score (descending). Deployed a DAX cluster in front of the table and updated the application to use DAX for leaderboard reads.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
High read latency (>10ms) on frequently accessed items
Fix
Check if DAX is enabled and configured correctly. Verify that the DAX cluster is in the same VPC and subnet as your application. Use CloudWatch metrics for DAX cache hit ratio.
Symptom · 02
Throttling errors (ProvisionedThroughputExceededException)
Fix
Examine your access patterns: are you reading too many items in a single query? Consider adding a GSI to distribute reads. Increase read capacity units or switch to on-demand mode.
Symptom · 03
Unexpectedly high costs
Fix
Review your table's item size and read/write patterns. Single-table designs can lead to large items if you store too many attributes. Use DynamoDB TTL to expire old data. Consider compressing attributes.
Symptom · 04
Inconsistent query results (missing items)
Fix
Verify that your composite key design is correct. Check for duplicate sort keys within the same partition. Ensure that your application uses consistent reads if needed.
★ Quick Debug Cheat SheetCommon DynamoDB single-table and DAX issues and immediate actions.
Slow reads
Immediate action
Enable DAX
Commands
aws dynamodb describe-table --table-name my-table
aws dax describe-clusters --cluster-name my-cluster
Fix now
Deploy a DAX cluster and update your application endpoint.
Throttling+
Immediate action
Increase capacity or add GSI
Commands
aws dynamodb update-table --table-name my-table --provisioned-throughput ReadCapacityUnits=10
aws dynamodb describe-table --table-name my-table --query 'Table.GlobalSecondaryIndexes'
Fix now
Switch to on-demand mode temporarily.
High cost+
Immediate action
Optimize item size
Commands
aws dynamodb scan --table-name my-table --select COUNT
aws dynamodb describe-table --table-name my-table --query 'Table.TableSizeBytes'
Fix now
Add TTL and remove unused attributes.
FeatureSingle-Table DesignMulti-Table Design
Number of tables1N
Query complexitySimple queries with composite keysRequires joins or multiple queries
Access pattern flexibilityMust be planned upfrontCan add tables later
CostLower provisioned throughput (one table)Higher due to multiple tables
ScalabilityCan hit partition limits if not designed wellEasier to scale individual tables
MaintenanceSimpler (one table to manage)More tables to monitor
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
single_table_schema.pydynamodb = boto3.resource('dynamodb')Understanding Single-Table Design
gsi_query.pydynamodb = boto3.resource('dynamodb')Designing Access Patterns with GSIs
dax_client.pyfrom amazon.dax import AmazonDaxClientImplementing DAX for Read Performance
dax_write_through.pyfrom amazon.dax import AmazonDaxClientAdvanced DAX Patterns
migration_script.pydynamodb = boto3.resource('dynamodb')Migrating from Relational to Single-Table Design
cost_optimization.pytable.put_item(Cost Optimization with Single-Table and DAX

Key takeaways

1
Single-table design stores multiple entities in one table using composite keys, reducing the number of tables and enabling efficient queries.
2
DAX is an in-memory cache that reduces read latency to microseconds, ideal for read-heavy workloads.
3
Plan all access patterns before designing your table to avoid costly migrations later.
4
Monitor hot partitions and GSI usage to prevent throttling and optimize costs.
5
Use write-through caching and TTL to keep DAX cache consistent with DynamoDB.

Common mistakes to avoid

3 patterns
×

Using 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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how you would design a single table for a social media app with ...
Q02SENIOR
What are the trade-offs of using DAX vs. ElastiCache for caching DynamoD...
Q03SENIOR
How do you avoid hot partitions in a single-table design?
Q01 of 03SENIOR

Explain how you would design a single table for a social media app with users, posts, and comments.

ANSWER
Use PK='USER#userId' and SK='PROFILE' for user profiles. For posts, use PK='USER#userId' and SK='POST#postId'. For comments, use PK='POST#postId' and SK='COMMENT#commentId'. Create a GSI with PK='POST#postId' and SK='timestamp' to fetch comments in order. This allows querying all posts by a user, all comments on a post, etc.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
When should I use single-table design vs. multiple tables?
02
Does DAX support strongly consistent reads?
03
How do I handle many-to-many relationships in single-table design?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,165
articles · all by Naren
🔥

That's NoSQL. Mark it forged?

5 min read · try the examples if you haven't

Previous
ClickHouse: Column-Oriented OLAP Database
19 / 27 · NoSQL
Next
SurrealDB: Multi-Model Database