IntermediateReferenceDatabases

DynamoDB

Amazon DynamoDB in practice - the partition key contract, consistency options, secondary indexes, transactions, and what throttling actually means.

establishedUpdated Reviewed

DynamoDB is the trade you make when you would rather have a hard performance floor than a flexible query language. In exchange for giving up joins, ad-hoc queries, and aggregations, you get a key-value and document store where a point read costs single-digit milliseconds at effectively any throughput, with no server to patch and no failover to script. Everything about DynamoDB, including its failure modes, follows from how it decides where your data physically sits.

The partition key contract

Every table requires a partition key and optionally a sort key. DynamoDB hashes the partition key value to decide which internal partition stores the item. Two hard limits follow from that design:

  • A single partition key value can serve at most 1,000 write capacity units and 3,000 read capacity units. A celebrity user id, a device type, or a date-stamped key that all writes share hits this ceiling no matter how much throughput the table has provisioned.
  • An item is at most 400 KB, including attribute names. Items approaching that bound are usually a sign that related entities have been folded into one item when they belong as separate items in the same item collection.

The sort key exists to make the partition key useful. All items sharing a partition key value form an item collection stored together, ordered by sort key, so begins_with, between, and comparison queries work inside it. The idiomatic design is a high-cardinality partition key (user id, device id, order id) with a sort key that models the one-to-many relationships you would otherwise join on.

Querying

Query is the only operation you should plan around: it reads items within one partition key value, optionally filtered on the sort key. Results return in 1 MB pages.

Scan reads the entire table and bills for every item examined, whether or not the filter expression keeps it. A filter on Scan reduces traffic back to the client, not the read cost. Scans are for exports and migrations, not request paths.

Everything else is single-item: GetItem, PutItem, UpdateItem, DeleteItem. There is no server-side aggregation, no GROUP BY, no full-text search. If the access pattern cannot be expressed as “items under this partition key, possibly ranged by sort key”, the answer is a secondary index, a different table shape, or a different database.

Consistency

Reads are eventually consistent by default: a GetItem can return data that does not yet reflect a recent write, because the read may hit a replica that has not applied it. A strongly consistent read returns the latest write but costs twice the read capacity and cannot be served from global secondary indexes at all.

Writes are durable on success: a 200 means the item is stored across multiple availability zones in the region. The consistency question only concerns reads.

Secondary indexes

Property Local secondary index (LSI) Global secondary index (GSI)
Partition key Same as base table Any attribute
Sort key Alternative to base sort key Any attribute
Consistent reads Supported Not supported
Created Only at table creation Any time
Per-key size limit 10 GB item collection None
Throughput Shares the table’s Own provisioned capacity

The GSI is how DynamoDB answers a second access pattern (orders by customer and orders by status, for example). Two caveats matter operationally: GSI reads are always eventually consistent, and a GSI with insufficient write capacity back-pressures writes to the base table, so an under-provisioned index can throttle the table itself.

Transactions

TransactWriteItems and TransactGetItems provide all-or-nothing operations across up to 100 items in one or more tables, in the same region and account. Condition checks on items not otherwise modified are supported. Transactional operations cost twice the capacity of their non-transactional equivalents, and conflicts between concurrent transactions cause TransactionConflict errors rather than lock waits.

Transactions are real but not the centre of the design: they exist so the common case, writing several related items at once, stays safe without giving up the partitioned model.

Capacity and billing

Two modes, switchable per table - provisioned to on-demand up to four times in a rolling 24-hour window, and back to provisioned at any time:

  • Provisioned: you set read and write capacity units, optionally with application auto scaling. Predictable cost for predictable load; a table-sized throttle when the load is not.
  • On-demand: pay per request. No capacity planning, instant headroom for spiky traffic, and a bill that scales with the spike rather than a limit that rejects it.

One read capacity unit is one strongly consistent 4 KB read (two eventually consistent). One write capacity unit is one 1 KB write. Throughput set at the table level is divided among the table’s partitions, so doubling capacity on a table DynamoDB has split into more partitions buys less per-partition headroom than the numbers suggest.

Replication and recovery

Every write is stored across three availability zones in the region; there is no replica configuration to manage. Global tables extend this to a multi-region, multi-active topology: any regional replica accepts writes, replicated everywhere else with last-writer-wins conflict resolution. Point-in-time recovery covers 35 days of continuous backups; on-demand backups are separate and do not expire.

What actually breaks

  • ProvisionedThroughputExceededException on a hot key, while the rest of the table idles. The diagnostic is CloudWatch’s per-key contributor insights, not the table-level throttle count.
  • Scan on the request path: works in development, bills for the whole table in production.
  • A GSI under-provisioned for writes, quietly throttling base-table writes.
  • Item collections that grow past the LSI 10 GB bound, at which point writes to that partition key fail.

Where DynamoDB is the wrong choice

If the workload needs JOINs, foreign keys, or aggregations the application did not precompute, a relational engine is the right tool - see PostgreSQL or SQL Server. If the primary need is ad-hoc search or aggregation over the same data, the DynamoDB-native answer is usually a stream feeding a purpose built store rather than more indexes. And if the access pattern is genuinely unknown at design time, that is itself the disqualifier: DynamoDB rewards knowing exactly how data will be read, and punishes finding out later.

Where DynamoDB sits

How this component relates to the rest of the stack. Items without a link are not covered here yet.

Alternatives

Solves a similar problem differently