Table of contents
Open Table of contents
Scaling Beyond Relational Limits
If you’ve run high-scale workloads, you know that relational databases eventually hit physical write ceilings due to lock contention and index overhead. Once you reach tens of thousands of writes per second, you need a NoSQL database built for horizontal scaling. AWS DynamoDB is designed specifically for this, offering single-digit millisecond latency at massive scale.
But DynamoDB’s performance predictability isn’t magic. It is the constraint of strict partitioning rules and capacity management models.
In this post, we will look at DynamoDB’s storage partitioning internals, explore how it distributes data across physical nodes, and analyze how its Adaptive Capacity engine works to mitigate hot partition issues.
1. Physical Partitioning & The Hash Ring
At its core, DynamoDB achieves horizontal scalability by partitioning data across multiple physical storage nodes (each running on SSDs and replicated across three Availability Zones using Paxos).
When you create a DynamoDB table, you must define a Primary Key, which can be:
- Simple Primary Key: A Partition Key (PK) only.
- Composite Primary Key: A Partition Key (PK) and a Sort Key (SK).
[ Client PutItem Request ]
|
v
+---------+----------+
| Request Router |
+---------+----------+
|
| HashValue = MD5(Partition Key)
v
+---------+---------------------------------------------------------------+
| Hash Space (Range Partitioning) |
| |
| [ 0x00000... - 0x55555... ] -> Physical Partition 1 (Replica Paxos Group)|
| [ 0x55556... - 0xAAAAA... ] -> Physical Partition 2 (Replica Paxos Group)|
| [ 0xAAAAB... - 0xFFFFF... ] -> Physical Partition 3 (Replica Paxos Group)|
+-------------------------------------------------------------------------+
The Hashing Mechanics
- When a client sends a
PutItemrequest, it lands on an stateless Request Router. - The Request Router extracts the Partition Key and computes its cryptographic hash (using a custom MD5-based algorithm).
- The resulting hash value maps to a specific 128-bit hash range.
- The Request Router maintains a partition map in memory (cached from a metadata service called the coordinator). It maps the hash range directly to the IP address of the leader node in the Paxos replica group responsible for that physical partition.
- The request is routed directly to the storage node, completely bypassing any query parsing or plan generation overhead.
Composite Keys: The Local B-Tree
If you use a Composite Primary Key:
- The Partition Key is used to locate the physical partition.
- All items sharing the same Partition Key are stored close together on the same physical partition, sorted internally by their Sort Key using a local B-Tree index. This allows efficient range queries (such as
begins_with,between, and comparison operators) within a single logical partition.
2. The Hard Physical Limits of a Partition
DynamoDB does not allocate a physical partition for every single partition key. Instead, it packs millions of logical partition keys into shared physical partitions.
A single physical partition in DynamoDB is bound by two strict physical limits:
- Storage Capacity Limit: A single physical partition can hold a maximum of $10\text{ GB}$ of data. Once a partition’s data size exceeds $10\text{ GB}$, DynamoDB automatically splits it into two new physical partitions, recalculating the hash range boundaries.
- Throughput Capacity Limit: A single physical partition is hard-capped at:
- $1,000$ Write Capacity Units (WCUs): One WCU = 1 write up to 1 KB per second.
- $3,000$ Read Capacity Units (RCUs): One RCU = 1 strongly consistent read up to 4 KB per second (or 2 eventually consistent reads).
+-------------------------------------------------------------+
| SINGLE PHYSICAL PARTITION |
| |
| Storage Limit : [██████████████████████████████] 10 GB |
| Write Limit : [██████████] 1,000 WCUs |
| Read Limit : [██████████████████████████] 3,000 RCUs |
+-------------------------------------------------------------+
The Throughput Allocation Fallacy
When you provision capacity (e.g., $10,000$ RCUs and $10,000$ WCUs) for a table, DynamoDB allocates physical partitions based on the following math:
$$Partitions_{\text{by_throughput}} = \max\left(\frac{Provisioned\text{ }RCU}{3,000}, \frac{Provisioned\text{ }WCU}{1,000}\right)$$
$$Partitions_{\text{by_storage}} = \frac{Total\text{ }Data\text{ }Size}{10\text{ GB}}$$
$$Total\text{ }Partitions = \lceil\max(Partitions_{\text{by_throughput}}, Partitions_{\text{by_storage}})\rceil$$
If you provision $10,000$ WCUs, DynamoDB will allocate exactly 10 physical partitions. Under legacy architectures, your provisioned throughput was divided equally among all partitions. Each partition received only $1,000$ WCUs.
If your application sent $2,000$ WCUs to a single “hot” partition key (even if the other 9 partitions were completely idle), your requests were immediately choked with a ProvisionedThroughputExceededException.
3. How Adaptive Capacity Resolves Hot Partitions
To solve this limitation, AWS introduced Adaptive Capacity. This feature allows your application to consume throughput unevenly across partitions, dynamically allocating unused capacity to hot partitions.
Adaptive Capacity works using two key mechanisms: Instant Passthrough and Dynamic Partition Splitting.
+------------------------------------------------------------------------+
| ADAPTIVE CAPACITY IN ACTION |
| |
| [ Partition 1 ] [ Partition 2 (HOT) ] [ Partition 3 ] |
| Allocated: 1K Allocated: 1K + 1K Allocated: 1K |
| Used : 200 Used : 2,000 Used : 100 |
| ^ (Borrows 1K idle |
| capacity dynamically) |
+------------------------------------------------------------------------+
Mechanism A: Instant Passthrough (Capacity Borrowing)
If your table’s overall throughput is below the total provisioned capacity, DynamoDB allows a single physical partition to exceed its standard allocation (up to its absolute physical maximum of $1,000$ WCUs / $3,000$ RCUs).
- DynamoDB monitors your traffic pattern continuously.
- If
Partition 2experiences a burst of $1,800$ WCUs whilePartition 1andPartition 3are mostly idle, DynamoDB dynamically shifts the unused capacity allocation toPartition 2. - This happens in real-time, typically within milliseconds, preventing temporary traffic spikes from throwing throttling errors.
Mechanism B: Dynamic Partition Splitting (The Long-Term Fix)
If a partition is continuously hot due to high data volume or sustained high access rates:
- DynamoDB identifies the hot partition and isolates the high-traffic partition keys.
- It splits the partition, creating two new physical partitions and shifting the hot key into its own dedicated partition hash range.
- Once isolated, the hot key can consume the full $1,000$ WCUs / $3,000$ RCUs allocation of its own new physical partition without sharing it with neighboring keys.
4. DynamoDB Streams: High-Performance Event-Driven Scaling
To build reactive microservices, you often need to trigger events when database rows change. DynamoDB Streams captures an ordered sequence of item-level modifications in real-time.
[ PutItem/UpdateItem ] ---> [ Paxos Group (Storage Nodes) ]
|
v (Write Ahead Log)
[ DynamoDB Stream Shards ]
/ | \
v v v
[ Shard 1 ] [ Shard 2 ] [ Shard 3 ]
The Stream Sharding Architecture:
- DynamoDB Streams is structured similarly to AWS Kinesis. It divides the change log into Stream Shards.
- A Stream Shard is an ordered log of transactional changes.
- Strict 1:1 Mapping: DynamoDB automatically manages stream shards to match physical partition splits. When a physical partition splits, its corresponding stream shard splits as well.
- This design guarantees that changes to items sharing the same partition key are always routed to the same stream shard and processed in the exact order they occurred.
- The JVM/Java consumer (using the DynamoDB Streams Kinesis Adapter or Spring Cloud AWS) polls these shards using dedicated workers. Because shard processing is isolated, scaling your consumer instances matches the database’s physical partition layout out of the box.
5. Architectural Best Practices for DynamoDB Sizing
To build highly performant DynamoDB integrations in your Java microservices:
- Maximize Partition Key Cardinality: Choose partition keys with high unique values (e.g.,
UUID,user_id,order_id). Avoid low-cardinality attributes likestatus,gender, ordatewhich group large data volumes onto single physical partitions. - Utilize Global Secondary Indexes (GSIs) Wisely:
- GSIs have their own provisioned throughput capacity, separate from the base table.
- If a GSI’s write capacity is starved, it will backpressure and throttle writes on the base table, even if the base table has plenty of write capacity remaining! Always size GSI write capacity to match or exceed the base table’s write volume.
- Avoid Local Secondary Indexes (LSIs) When Possible:
- LSIs share the physical partition storage space with the base table.
- Using an LSI subjects your items to a hard $10\text{ GB}$ Item Collection Limit (the total size of all items sharing the same Partition Key across the base table and LSIs cannot exceed $10\text{ GB}$). GSIs do not have this limit.
- Leverage Write Sharding for Extreme Bottlenecks:
- If your application must write millions of events per second under a single partition key (e.g., capturing raw sensor data for a single highly active device), implement write sharding.
- Append a random suffix (e.g.,
-1to-10) to the partition key during writes, and query across all 10 sharded keys during reads:# Example Sharded Keys DEVICE_99182-1 DEVICE_99182-2