Skip to content
Go back

DynamoDB Architecture: Deep Dive into Partitioning and Adaptive Capacity

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:

  1. Simple Primary Key: A Partition Key (PK) only.
  2. 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

  1. When a client sends a PutItem request, it lands on an stateless Request Router.
  2. The Request Router extracts the Partition Key and computes its cryptographic hash (using a custom MD5-based algorithm).
  3. The resulting hash value maps to a specific 128-bit hash range.
  4. 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.
  5. 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:


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:

  1. 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.
  2. 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).

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:

  1. DynamoDB identifies the hot partition and isolates the high-traffic partition keys.
  2. It splits the partition, creating two new physical partitions and shifting the hot key into its own dedicated partition hash range.
  3. 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:


5. Architectural Best Practices for DynamoDB Sizing

To build highly performant DynamoDB integrations in your Java microservices:

  1. Maximize Partition Key Cardinality: Choose partition keys with high unique values (e.g., UUID, user_id, order_id). Avoid low-cardinality attributes like status, gender, or date which group large data volumes onto single physical partitions.
  2. 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.
  3. 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.
  4. 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., -1 to -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

Share this post on:

Previous Post
Demystifying Merkle Trees: Anti-Entropy in Cassandra and Blockchains
Next Post
Java Reactive Programming: Project Reactor Internals vs. Virtual Threads