Topic 11.4
DynamoDB: Keys, GSIs and Single-Table Design
In one line
DynamoDB stores items by partition key (and optional sort key) across auto-managed partitions, each with a throughput limit (about 3,000 read units and 1,000 write units per second). Model from access patterns: composite sort keys and overloaded GSIs serve many entity types in one table. Conditional writes provide uniqueness and optimistic locking; TTL expires items; transactions cover up to 100 items.
Think of it like this
A warehouse with numbered aisles (partition key) and shelves sorted within each aisle (sort key). You can ask "everything in aisle 42 between shelves A and F", but not "every red item anywhere" unless you've built a second catalogue (a GSI).
Key ideas
- 01
Operations: GetItem by full key; Query within one partition key with sort-key conditions (begins_with, between, <, >); Scan reads the whole table (avoid on hot paths). Items max 400 KB.
- 02
Single-table design: store several entity types in one table with generic keys (
PK = CUSTOMER#42,SK = ORDER#2026-09-14#9001), so one Query fetches a customer and their recent orders together (an item collection). - 03
GSIs: alternative key schemas, eventually consistent, with their own throughput; overload a GSI (
GSI1PK,GSI1SK) to serve several patterns. LSIs share the partition key, must be created with the table, and cap each item collection at 10 GB. - 04
Conditional writes:
attribute_not_exists(PK)for create-if-absent (uniqueness),version = :vfor optimistic locking.TransactWriteItemsmakes up to 100 items atomic (at twice the write cost). - 05
Capacity: on-demand (pay per request) or provisioned with auto scaling. Hot partitions throttle even when total capacity is free; adaptive capacity helps but a single key still caps at ~1,000 WCU per second. Spread writes with high-cardinality keys or write sharding.
Code & diagrams
PK SK type attributes
CUSTOMER#42 PROFILE customer name, email
CUSTOMER#42 ORDER#2026-09-14#9001 order total, status, GSI1PK=ORDER#9001
CUSTOMER#42 ORDER#2026-09-02#8812 order total, status, GSI1PK=ORDER#8812
ORDER#9001 ITEM#1 item sku, qty, price
ORDER#9001 ITEM#2 item sku, qty, price
Access patterns
1. Customer profile + recent orders : Query PK=CUSTOMER#42, ScanIndexForward=false (profile + orders)
2. Order with items : Query PK=ORDER#9001
3. Order by id (no customer known) : Query GSI1 where GSI1PK=ORDER#9001
4. Orders by status for ops : GSI2PK=STATUS#open#shard-3, GSI2SK=createdAt (write-sharded)# create username only if not taken (uniqueness via a separate item)
aws dynamodb transact-write-items --transact-items '[
{"Put": {"TableName": "app", "Item": {"PK": {"S": "USERNAME#asha"}, "SK": {"S": "USERNAME"}},
"ConditionExpression": "attribute_not_exists(PK)"}},
{"Put": {"TableName": "app", "Item": {"PK": {"S": "USER#42"}, "SK": {"S": "PROFILE"},
"username": {"S": "asha"}},
"ConditionExpression": "attribute_not_exists(PK)"}}
]'
# fails with TransactionCanceledException / ConditionalCheckFailed if the username existsInterview problem
The problem
Design an e-commerce order table in DynamoDB
Access patterns: get a customer's orders newest first; get an order with its items by order ID; list open orders for the warehouse by creation time (5K/sec peak writes); enforce unique email per customer. Design keys and indexes.
When it breaks
Low-cardinality GSI partition key (status)
What you see
All open orders share GSI2PK = open; the GSI throttles, and GSI throttling back-pressures writes to the base table.
Fix & prevent
Write-shard the key (open#0..N) or use a higher-cardinality key; make the index sparse.
Explain it without notes
Why do DynamoDB designs start with the access pattern list?
Practice
How do you implement optimistic locking for a profile item?
Trade-offs
- ↔
Predictable single-digit-millisecond latency at any scale, fully managed, in exchange for rigid access patterns, per-partition limits, and harder analytics.
Done when you can
I can design DynamoDB keys, GSIs and conditional writes from an access-pattern list.