DynamoDB
During Prime Day, Amazon has reported DynamoDB peaks of well over a hundred million requests a second, with single digit millisecond responses throughout. No query planner, no joins, no surprise full table scan at the worst moment. DynamoDB gets there by making a bargain with you: it will be fast and predictable at any scale, as long as you tell it up front exactly how you will read your data.
That bargain is the whole chapter. Understand the partition key and you understand how DynamoDB scales, why it is fast, and why designing for it feels backwards the first time.
With DynamoDB you are told to list every query your app will make before you design the table. A relational database never asks for that. Why would DynamoDB need it?
Tables, items and keys#
A table holds items (like rows), each up to 400 KB, made of attributes (like columns, but each item can have different ones). Every item has a primary key, in one of two forms:
- a partition key alone, such as
user_id, when you only ever fetch one item by its id - a partition key plus a sort key, such as
customer_idandorder_date, when you want all of a customer's orders, sorted by date, in one request
The partition key decides where an item is stored. The sort key decides the order of items that share a partition key. Those two facts explain almost everything else.
How it works inside#
- Your request reaches a request router, which hashes the partition key.
- The hash falls in a range owned by one partition. Each partition holds a slice of the table, up to about 10 GB, and DynamoDB splits partitions automatically as data grows.
- A partition is a group of three replicas in three availability zones. One is the leader. A write is acknowledged once it is durable on the leader and at least one other replica, so losing a whole data centre loses nothing.
This is consistent hashing at work, the same idea as in Consistent hashing and Design a key value store. Every request goes to exactly one partition, so a table with a billion items answers as fast as one with a thousand.
The partition key decides your fate#
Each partition can serve a limited amount of traffic, roughly 3,000 reads and 1,000 writes a second. Spread traffic across many partition key values and DynamoDB scales almost without limit. Concentrate it on a few and you hit that ceiling no matter how much capacity the table has. That is a hot partition, and it is the most common DynamoDB problem.
Capacity is measured in units#
DynamoDB bills and throttles in read capacity units and write capacity units. One write unit is one write a second of an item up to 1 KB. One read unit is one strongly consistent read a second of up to 4 KB, or two eventually consistent ones. Bigger items cost more units.
A 3 KB item costs three write units per write. Trimming items, and storing large blobs in object storage with only a pointer in DynamoDB, cuts cost directly. You can pay per request (on demand) or reserve capacity (provisioned, with auto scaling), which is cheaper for steady traffic.
Query, not scan#
There are two ways to read many items, and the gap between them is the gap between fast and broken.
Query takes a partition key, and optionally a condition on the sort key (equals, between, begins with). It reads one partition's items in sort key order and stops. Fast at any table size.
Scan reads every item in the table and filters afterwards. It is slow, and it is billed for everything it reads, not just what it returns. In production code, a Scan is almost always a sign the table is missing a key or an index for that access pattern.
Design from the access patterns#
This is the answer to the question at the top. DynamoDB has no planner to find a clever way to answer a query you did not prepare for. Every fast read is a Query on a key you designed. So you list the questions first, then shape keys that answer each one with a single Query.
| PK | partition key | PK | CUSTOMER#17 for a customer's items, ORDER#44821 for an order's items. Different entity types share the table, distinguished by prefix. |
| SK | sort key | PK | PROFILE for the customer record, ORDER#2026-10-15#44821 for each order, ITEM#1 and ITEM#2 for an order's lines. |
| GSI1PK, GSI1SK | index keys | IDX | RESTAURANT#9 and the order time, so a restaurant can list its orders. A global secondary index with its own keys. |
| status, total, ... | attributes | Ordinary attributes. Only key attributes can be queried efficiently. |
Putting several entity types in one table with prefixed keys is called single table design. It makes related data come back in one request. It is also harder to read and to change, and many teams reasonably use one table per entity instead. Either way, the access patterns come first.
Secondary indexes#
When you need to query by something other than the primary key, add an index.
Global secondary index (GSI). Any partition and sort key you like, such as restaurant and time. It is a separate copy of the data, updated asynchronously, so reads from it are eventually consistent, and it has its own capacity. You can add one at any time.
Local secondary index (LSI). Same partition key, different sort key, such as a customer's orders sorted by total instead of date. It allows strongly consistent reads, but must be created with the table, and limits each partition key value to 10 GB of data.
Safe writes without locks#
DynamoDB has no long running transactions, but it has precise tools. A condition expression
makes a write happen only if something is true: attribute_not_exists(PK) to create an item only
if it is new, which is how you enforce uniqueness, or version = 7 for optimistic locking, the
same idea as the isolation levels chapter. TransactWriteItems
groups up to 100 writes across items and tables into one all or nothing operation, at twice the
capacity cost.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| DynamoDB | Predictable single digit millisecond latency at any scale, fully managed, no servers to run. | Every query must be planned in advance, no joins, and ad hoc analytics is painful. | Known, key based access patterns at high or spiky scale: carts, sessions, orders by customer. |
| A relational database | Ask any question later, joins, and rich transactions. | Scaling writes beyond one primary takes real work. | Access patterns that will keep changing, or heavy relational queries. |
| On demand capacity | No capacity planning, and it absorbs sudden spikes. | Higher price per request than well used provisioned capacity. | New tables, unpredictable traffic, and spiky workloads. |
Adding a new access pattern later means a new index or reshaping keys and backfilling data. If the product is young and the questions change every sprint, a relational database will let you move faster, and you can revisit when the patterns settle. The SQL vs NoSQL chapter has eight workloads to practise this choice on.
Checkpoint#
1. Why must you design DynamoDB tables from your access patterns?
2. Every write today is throttled even though the table has plenty of total capacity. The partition key is order_date. Why?
3. How do you guarantee a username is unique in DynamoDB?
- Partition key picks where, sort key picks order. Items up to 400 KB.
- About 3,000 reads and 1,000 writes a second per partition. Spread the key.
- 1 WCU = one 1 KB write a second. 1 RCU = one 4 KB strong read, or two eventual.
- Query, never Scan, in production. Design keys from the access patterns.
DynamoDB stores items in tables with a primary key that is either a partition key or a partition key plus a sort key. The partition key is hashed to choose a partition, each replicated three ways across availability zones, so every request touches one partition and latency stays flat as the table grows. Each partition has a throughput limit, so I choose a high cardinality partition key to avoid hot partitions. Capacity is measured in read and write units that scale with item size. Because there is no planner or joins, I list the access patterns first and shape keys so each is a single Query, using global secondary indexes for other patterns, condition expressions for uniqueness and optimistic locking, and transactions when several items must change together. If the questions are still changing, I would start relational instead.