DynamoDB

A complete guide to Amazon DynamoDB, from the first table to the internals. The data model, the API, how requests are served, capacity and throttling, indexes, transactions, data modelling patterns, streams, global tables, backups, caching, security and operations, with the trade-offs at each step.

On this page
  1. 01What it is and why it exists
  2. 02The data model
  3. 03Working with data: the API
  4. 04How it works inside
  5. 05Reads and consistency
  6. 06Capacity, throughput and cost
  7. 07Secondary indexes
  8. 08Writes that stay correct
  9. 09Data modelling
  10. 10Hot keys and scaling patterns
  11. 11Streams, TTL and event-driven patterns
  12. 12Global tables and multi-region
  13. 13Backup, restore and recovery
  14. 14Caching with DAX
  15. 15Security
  16. 16Operating DynamoDB in production
  17. 17When to use it, trade-offs, numbers
  18. 18DynamoDB in an interview or a design review
  19. Common mistakes
  20. Check yourself
  21. Further reading

DynamoDB is a managed NoSQL database from AWS. You give up query flexibility and get steady performance at any size, with no servers to run. It is a common answer when a design needs a database that scales without a cluster to look after, and it is also a common source of surprise bills and throttling when the data model is wrong.

This lesson starts at the first table and goes down to how a request is served. Read it in order once. Later, use the headings and the closing cheat sheet to revise.

Crux: in DynamoDB the key design is the design. List the questions first, choose keys so that each question is one query, and check the numbers (units per second, hot keys) before you trust the design.

How to read it: sections 01 to 03 are the basics. Section 04 explains how it works inside, which is what an interviewer or a design review will probe. Sections 05 to 08 are the working knowledge: reads, capacity, indexes and transactions. Section 09 is data modelling, the skill that matters most. Sections 10 to 16 cover scaling, events, regions, backups, caching, security and operations. Sections 17 and 18 are about choosing it and talking about it. Facts and limits were checked against the AWS documentation and the DynamoDB team's 2022 paper on 2 November 2026. Limits change, so re-check any number that decides your design. I leave out prices on purpose.

What it is and why it exists

DynamoDB stores items in tables. An item is like a row, but only the key is fixed: every other attribute can differ from one item to the next. You find items by key, and the database answers in a few milliseconds whatever the size of the table.

The goal that shaped it is predictable performance. The team's own paper states the aim: complete requests with low single-digit millisecond latencies, and keep them there as tables grow from megabytes to hundreds of terabytes. For scale, the same paper reports that during Amazon's 66-hour Prime Day event in 2021, Amazon systems made trillions of calls and peaked at 89.2 million requests per second.

Where it came from

  • Dynamo (2007). Amazon built an internal key-value store for things like the shopping cart. It had to stay writable and fast during failures. Teams ran their own copies, which was hard work.
  • SimpleDB and S3. Managed services showed that teams preferred not to run databases. SimpleDB was managed but had small limits and unpredictable latencies.
  • DynamoDB (2012). The paper says it combined Dynamo's scaling and predictable speed with SimpleDB's ease of use. It shares most of the name of Dynamo but little of its architecture. Section 04 shows what changed.

What you get

  • No servers. AWS handles hardware, patching, replication, failures and upgrades.
  • Scale in both directions. Tables grow and spread across more servers on their own. There is no limit on table size.
  • Replication across data centres. Data is copied across several availability zones in a region. The paper states a 99.99 percent availability design target for regional tables and 99.999 percent for global tables (tables replicated across regions).
  • A flexible model. Key-value or document, with no fixed schema except the key.
  • Optional guarantees when you need them. Strongly consistent reads, conditional writes and multi-item transactions.

What you give up

  • Ad hoc queries and joins. You can only look data up through the keys and indexes you planned.
  • Easy changes of mind. A new question can mean a new index, new keys or a data migration.
  • Portability. It is an AWS service. Your data model is shaped around it.
  • Free multi-item transactions. They exist, with limits, and they cost double.
Remember: DynamoDB is fast because it does less. It does one thing very well, a lookup by key, and everything else is a design problem that you solve with keys.

The data model

Tables, items and attributes

  • A table is a collection of items.
  • An item is a collection of attributes, found by its primary key.
  • An attribute is a name with a value. Apart from the key, you do not declare attributes in advance. Two items in the same table can have completely different attributes.

The primary key

The primary key is chosen when the table is created and cannot change. It has one of two shapes.

  • Partition key only (a simple key). One attribute, such as user_id. Each value identifies exactly one item.
  • Partition key plus sort key (a composite key). Two attributes. Items that share a partition key are stored together, ordered by the sort key. The pair must be unique. This gives you ranges: "the messages of this chat after this time".

The partition key value goes through a hash function, and the result decides where the item is stored. That is why you can only ask for a partition key by equality. Key attributes must be a string, a number or binary.

partition key: chat_id chat_id = 7 ts 100 "hi"ts 101 "hello"ts 102 "lunch?" chat_id = 9 ts 090 "ok"ts 095 "see you"ts 120 "bye" query: chat 7, ts > 100one partition, one sorted slice other keys live elsewherethe key decides where data sits
A partition key picks where items sit, and the sort key orders them inside, so one query returns a sorted slice.

The group of all items with the same partition key is called an item collection. Hold on to that term. A great deal of modelling is the art of putting the right items into the same item collection.

Data types

  • Scalars: string, number, binary, boolean and null.
  • Documents: a list (ordered, any types) and a map (named attributes, any types), which can be nested up to 32 levels.
  • Sets: a string set, number set or binary set. Values are unique and unordered. Empty sets are not allowed.

Facts that catch people out:

  • There is no date type. Store dates as an ISO 8601 string (it sorts correctly) or as a number such as epoch seconds.
  • Numbers carry up to 38 digits of precision. If precision matters, send the number as a string in the request.
  • Strings sort by their UTF-8 bytes. That is plain byte order, not language-aware order.
  • Empty strings and empty binary values are allowed, but not in a key attribute.

Size limits and names

  • An item can be at most 400 KB. The size counts attribute names as well as values, so very long names cost space on every item.
  • A partition key value is 1 to 2,048 bytes. A sort key value is 1 to 1,024 bytes.
  • Table and index names are 3 to 255 characters. Attribute names can be long, but they count against the item size and the throughput you are billed, so keep them short on big tables.

The item limit matters for design. A profile with a 300 KB biography, or a list that grows forever, will hit it. Keep large content in blob storage and store a pointer, or split the item into several items.

Schemaless does not mean schema-free

The database does not enforce a schema, but your application has one. Put it in code: types, required fields and a version attribute. Without it, a table fills with items of inconsistent shapes, and nobody knows what is safe to change.

Remember: the key is the only fixed part of an item, so choose it with care. Everything else you can add later.

Working with data: the API

The four item operations

Every table has the same four calls, and the 2022 paper lists them as the basic operations.

  • PutItem writes an item. If one with the same key exists, it is replaced entirely.
  • GetItem reads one item by its full primary key. It is eventually consistent by default, with an option for a strongly consistent read.
  • UpdateItem changes some attributes of an item. It creates the item if it does not exist, so it is an upsert.
  • DeleteItem removes one item by key. Without conditions, deleting twice is harmless.

All write calls can carry a condition, and can ask for the old or new values back, so you do not need an extra read.

Many items: Query and Scan

  • Query reads items from one partition key. You give the partition key by equality and, optionally, a condition on the sort key. It returns items in sort-key order, ascending by default, and you can ask for descending.
  • Scan reads the whole table, or the whole index. It costs in proportion to the table size, and returns items in no guaranteed order. Keep it for offline jobs. In an online path, a scan is a design bug.

For a sort key you can use =, <, <=, >, >=, BETWEEN and begins_with. The prefix function is the one most used in modelling, as section 09 shows. It does not work on a number sort key.

A query or scan returns at most 1 MB per call. If more is available, the answer includes LastEvaluatedKey. Pass it back as the start key for the next page. Two details are worth knowing: only an empty LastEvaluatedKey means you are done, and a page can come back with zero items and still carry a key, because the filter is applied after the 1 MB is read.

The five expressions

Most of the API is driven by small expression strings. Learn to tell them apart.

  • Key condition: which partition (and sort-key range) a query reads. This is the cheap, indexed part.
  • Filter: drops items after they are read. It does not reduce the capacity you pay for, and the 1 MB limit applies before it. Use it to tidy results, not to avoid reading.
  • Projection: returns only some attributes. It saves bandwidth but not capacity, which is based on the stored item size, not on what comes back.
  • Condition: a check that must be true for a write to happen (section 08).
  • Update: what to change, using four verbs: SET (set or compute a value, including if_not_exists and list_append), REMOVE (delete attributes), ADD (add to a number or set) and DELETE (remove elements from a set).

A Query example, as a request:

{ "TableName": "Orders",
  "KeyConditionExpression":
    "customer_id = :c AND begins_with(sk, :p)",
  "ExpressionAttributeValues": {
    ":c": { "S": "cust_83921" },
    ":p": { "S": "ORDER#2026" } },
  "ScanIndexForward": false,
  "Limit": 20 }

An update that changes a field and appends to a history list in one call:

UpdateExpression:
  SET order_status = :s,
      history = list_append(history, :entry)
ConditionExpression: order_status = :expected

A word on names: some words, such as status and name, are reserved. Use a placeholder (#s) in the expression and map it in ExpressionAttributeNames.

Batches

  • BatchGetItem reads up to 100 items (16 MB) from one or more tables, in one call. Items that do not fit come back as UnprocessedKeys.
  • BatchWriteItem puts or deletes up to 25 items (16 MB). It cannot update items, takes no conditions and is not atomic. Failures come back as UnprocessedItems. You must retry those, with backoff.

A batch saves network round trips. It does not save capacity, and it is not a transaction.

PartiQL and DynamoDB Local

  • PartiQL gives a SQL-like way to read and write. Take care: a SELECT without a partition-key condition becomes a full scan. It is a different syntax over the same engine, not a different engine.
  • DynamoDB Local is a downloadable version for development and tests. It ignores provisioned throughput and does not do parallel scans, so it cannot tell you about throttling or cost.
Remember: Query reads one partition and is cheap. Scan reads everything and is not. Filters and projections change what comes back, never what you pay.

How it works inside

You can use DynamoDB for a long time without knowing this. You cannot reason about throttling, consistency or availability without it. This section follows the DynamoDB team's own 2022 paper, and says so where something is my inference.

Partitions

A table is divided into partitions. Each partition holds a disjoint, contiguous part of the table's key range. The partition key value is hashed, and the hash together with the sort key decides where an item is stored. This is why the same partition key always lands in the same place, and why a query on one partition key touches one partition.

Partitions are not fixed. DynamoDB splits them in two cases:

  • For size. A partition that holds too much data is split.
  • For consumption. A partition that receives too much traffic is split. The split point is chosen from the key distribution the partition has seen, not blindly in the middle. A split usually takes minutes.

Splitting cannot fix every kind of heat. The paper says a partition that receives high traffic to a single item, or one whose key range is accessed in order (for example an ever-increasing sort key), does not benefit from a split, and DynamoDB avoids splitting it. That is the root of the hot key problem in section 10.

Three copies, one leader

Every partition has replicas in different availability zones. The replicas form a replication group, and the group uses Multi-Paxos to elect a leader and agree on order.

  • Only the leader serves writes and strongly consistent reads. It keeps its position with a lease that it renews.
  • A write becomes a record in a write-ahead log. The leader sends the record to its peers, and the write is acknowledged once a quorum (two of the three) has saved it.
  • Any replica can serve an eventually consistent read. That read may hit a replica that is slightly behind.
  • If the leader fails, another replica proposes an election. The new leader must wait for the old lease to expire, which takes a couple of seconds. During that time the partition accepts no writes or strongly consistent reads.
  • Log replicas. When a replica is unhealthy, the leader adds a log replica, which keeps only the recent log and no data. This takes seconds and restores the write quorum fast. Rebuilding a full replica (the B-tree and the log) takes minutes.

A storage replica holds two things: the write-ahead log and a B-tree with the key-value data.

client requestrouter metadata leader (zone a)log + B-tree replica (zone b)log + B-tree replica (zone c)log + B-tree write: ack once 2 of 3saved the log record strong read: leadereventual read: any replica
A request goes through a router to the partition's leader. The leader copies each write to two replicas in other zones.

The request path

A request passes through a few services, which the paper describes.

  • The request router authenticates and authorizes the request (through IAM, and through KMS for tables with customer keys), finds where the key lives and forwards the request. It holds no item data.
  • The metadata service stores which partition holds which key range and where its replicas are. Routers cache this. The team found that a cache with a 99.75 percent hit rate still caused trouble: when a new router started with an empty cache, every request needed a lookup, and the metadata service saw sudden, huge load. They built an in-memory distributed store (MemDS) and made routers refresh their cache on every hit, so the metadata service sees steady traffic whatever the hit rate.
  • The storage nodes hold the replicas. A node can host thousands of replicas of unrelated tables.
  • The autoadmin service is the control plane. It watches the health of partitions and nodes, replaces bad replicas, scales tables and runs table-creation and update requests.
  • Admission control decides whether a request is allowed to run at all (section 06).

Other services behind the scenes provide backups, streams, global tables, indexes and transactions.

Durability and correctness

The paper describes several layers.

  • The write-ahead log is on all three replicas and is archived to S3 regularly.
  • Checksums everywhere. Every log entry, message and log file carries one, so silent corruption is caught as data moves between nodes.
  • Continuous verification. A scrub process compares the replicas of a group with each other, and with a replica rebuilt from the archived logs, to catch bit rot and bugs.
  • Formal methods. The core replication protocol was specified in TLA+ and model-checked, and the team also runs failure-injection tests.
  • Backups and point-in-time restore are built from the archived logs, so they do not affect the table's performance.

Availability

  • The 99.99 percent (regional) and 99.999 percent (global tables) figures are design targets measured per five-minute interval as the share of successful requests.
  • No maintenance windows. Deployments are rolled out gradually with automatic rollback, and leaders step down gracefully so a new leader does not wait for a lease.
  • Statically stable dependencies. If IAM or KMS were unavailable, routers keep using cached answers for a while, so requests keep flowing.

What changed from the original Dynamo

The 2007 Dynamo paper described a different system, and people mix them up. This is the contrast, with one caution: the 2022 paper does not list what was dropped. The points about what is missing are my inference from what it describes.

  • Replication. Dynamo was leaderless: any node in a key's preference list took writes, using sloppy quorums, hinted handoff and vector clocks to merge conflicting versions. DynamoDB has a leader per partition and Multi-Paxos.
  • Conflicts. In Dynamo the client could be handed several versions to merge. In a DynamoDB table there is one agreed order per item. Conflicts across regions in global tables are resolved by last writer wins (section 12).
  • Operations. Dynamo ran as a single-tenant system that each team had to operate. DynamoDB is a shared, managed service, and isolation between tenants is handled by admission control.
  • Data model. Dynamo was a plain key-value store. DynamoDB adds sort keys, secondary indexes, conditions and transactions.

What this means for you

  • A strongly consistent read goes to one node, the leader. That is why it costs twice as much and why it cannot be served from an index.
  • Latency does not grow with table size. A lookup goes to one partition.
  • A partition has a ceiling, so one busy key cannot be fixed by buying more table capacity.
  • Most of what feels like "the database is slow" is throttling, which comes from admission control, and is a function of your key design and request rate.
Remember: the database is a fleet of three-replica groups, each with a leader, and every request is routed to one group by hashing the partition key.

Reads and consistency

The three read types

  • Eventually consistent (the default). The response might not include a write that finished a moment ago, because it may come from a replica that has not caught up yet. Half the cost.
  • Strongly consistent. The response reflects every write that succeeded before the read. It is served by the leader. Request it with ConsistentRead.
  • Transactional. TransactGetItems reads several items with serializable isolation. Double the cost.

Where each applies:

  • A table and a local secondary index support both normal read types.
  • A global secondary index supports only eventual reads. Asking an index for a consistent read returns a validation error.
  • Streams are also eventually consistent.
  • BatchGetItem is read-committed, and Query and Scan are read-committed. Only a transaction gives serializable reads.

The classic bug

You write an item, then read it by key in the next line of code, and the read returns the old value or nothing. This is not a bug in DynamoDB. The default read may come from a replica that is behind. Fix it in one of three ways:

  • Read with ConsistentRead when you need your own write back.
  • Ask the write to return the new values (ReturnValues) instead of reading again.
  • Design so that nothing breaks when a read is a moment late. Most pages can tolerate it.

AWS gives no guaranteed time bound for eventual consistency. In practice it is short, but code that needs a bound should not assume one.

Consistency is not the same as isolation

Read-committed means you see only committed data. It does not stop someone else changing the item right after your read. If you read a value, compute something and write it back, another request can slip in between. Section 08 shows how to make the write itself check what you read.

Global tables change the story

In a global table (section 12), a strongly consistent read is strongly consistent only in the replica you read from. If the item was last written in another region, the read can still be stale.

Remember: strong reads are for the few reads that must see the latest write. Everything else should be eventually consistent, and the writes should carry their own checks.

Capacity, throughput and cost

DynamoDB measures every request in units, and a request that would exceed the available capacity is refused. This is the section to read twice.

What one unit buys

  • A read unit pays for one strongly consistent read per second of an item up to 4 KB. An eventually consistent read costs half a unit, and a transactional read costs two.
  • A write unit pays for one write per second of an item up to 1 KB. A transactional write costs two.
  • Sizes are rounded up. Reads round to the next 4 KB, writes to the next 1 KB. A 3.5 KB read costs the same as a 4 KB read, and a 2 KB write costs 2 units (4 as part of a transaction).
  • A query is one read. DynamoDB adds up the sizes of all items it reads, then rounds that total up to 4 KB. A filter does not make it cheaper, and neither does a projection.
  • Older documents call these RCU and WCU. On-demand tables bill them as read request units and write request units.
one item of 6 KB write6 KB ÷ 1 KB → 66 units strong read6 KB ÷ 4 KB → 22 units eventual readhalf of 2 → 11 unit round the size up first, then count steps; a bigger item costs more on every access
The same 6 KB item costs 6 write units, 2 strong read units or 1 eventual read unit.

Two ways to pay

  • On-demand is the default and the recommended option for most workloads, according to AWS. You pay per request and set no capacity. A table instantly handles up to double its previous peak, and a brand new table can sustain up to 4,000 writes and 12,000 reads per second from the start. If traffic jumps to more than double the previous peak, DynamoDB allocates more, but throttling can still occur in the meantime. You can set an optional maximum throughput as a guard against runaway cost. It is applied on a best-effort basis, so treat it as a target.
  • Provisioned means you set read and write units per second, and pay for them whether you use them or not. Auto scaling can adjust them. It works on target utilization (between 20 and 90 percent) and reacts after consumption has been above target for two minutes in a row, so it follows traffic and does not predict it. A spike that does not last two minutes may never trigger it. There are limits on how often you may lower capacity.
  • Reserved capacity is a commitment for provisioned tables (not on-demand or Standard-IA) that lowers the price of steady usage. It is bought per region in blocks of units.
  • Switching modes is allowed, with limits: from provisioned to on-demand up to four times in a rolling 24 hours, and the change can take several minutes.

How to choose: steady, predictable traffic that you can forecast favours provisioned with auto scaling. Spiky, new or unknown traffic favours on-demand. Compare both against your real numbers on the AWS pricing page, because the answer depends on how busy the table is.

Two storage classes

  • Standard is the default.
  • Standard-IA (infrequent access) is optimised for tables where storage is the dominant cost, such as logs, old posts or order history. The price trade is on the AWS pricing page. All indexes of a table share its class, and you may change the class no more than twice in 30 days.

The part that bites: limits per partition

Whatever you provision, a partition is designed to deliver at most 3,000 read units and 1,000 write units per second. Table-level capacity cannot lift this. If a single partition key value is hit harder than that, it throttles while the rest of the table sits idle.

Other limits that decide designs:

  • A table's default throughput quota is 40,000 read and 40,000 write units, and an account's provisioned total is 80,000 each. On-demand tables have no account-level cap. These are quotas that you can ask to raise, not hard limits.
  • Provisioned capacity can be as low as 1 read unit and 1 write unit.

How the service protects itself, and you

The paper tells the story, and it explains behaviour you will see.

  • At first, each partition got a fixed share of the table's capacity. A table with 3,200 write units and a 1,000-unit partition cap got four partitions of 800. When a partition split, its share was divided.
  • Hot partitions and dilution. Real traffic is not uniform. A hot partition ran out of its share while the table had spare capacity. And splitting a partition for size divided its throughput among the children, so the hot part could end up with less than before. Teams worked around this by over-provisioning.
  • Burst capacity. A partition may use capacity it did not use earlier, for up to 300 seconds, if the node has spare throughput. It absorbs short spikes.
  • Adaptive capacity. For longer skew, DynamoDB boosts the share of a partition that is throttling while the table is under its total. The paper says it removed over 99.99 percent of throttling caused by skewed access.
  • Global admission control. Request routers hold local token buckets and refill them every few seconds from a central service that tracks the table's total. A hot partition can then use the whole table's capacity, up to the partition ceiling. Partition-level buckets remain as a safety net.
  • Split for consumption. Traffic-driven splits spread hot ranges over more partitions, as long as the heat is spread over several keys.
requests request routerlocal tokens global admission refill every few seconds partitionown tokens safetynet throttled no tokens at either level:the request is refused
A request needs tokens, first at the router (refilled from a global service) and then at the partition. With none, it is throttled.

Throttling in practice

When a request is refused you get a throttling exception: ProvisionedThroughputExceededException or ThrottlingException. Newer errors also carry a throttling reason that says which limit was hit. The documented causes fall into four groups:

  • A key range is too hot, in either mode.
  • The table's provisioned capacity is exhausted.
  • An account or table quota was exceeded.
  • An on-demand maximum that you configured was exceeded.

The AWS SDKs retry throttled requests automatically with exponential backoff. The docs suggest that a careful client stops retrying after about a minute, and adds jitter when many clients retry together. Retries use capacity, so under constant overload they add load.

Capacity maths you can do on a whiteboard

Here are three exercises with my own numbers. Show the rounding.

A write-heavy table. An order service writes 20,000 items per second, each 2.5 KB. Each write rounds up to 3 KB, so 3 write units. Peak need is 20,000 × 3 = 60,000 write units per second. That is above the default 40,000 table quota, so you would ask for an increase, and you should check whether the keys spread the load enough that no partition exceeds 1,000.

A read-heavy table. 10,000 reads per second of 6 KB items. A strongly consistent read of 6 KB rounds to 8 KB, so 2 units each, 20,000 units per second. The same reads eventually consistent cost 1 unit each, 10,000 units per second. Choosing the default halves the bill.

A hot key. One counter row takes 3,000 updates per second of under 1 KB. Each costs 1 write unit, so 3,000 write units on one partition, three times its ceiling. Provisioning 100,000 units on the table changes nothing. The fix is in the key design (section 10).

Remember: size, rounding and the per-partition ceiling decide the bill and the throttling, so compute them before you build.

Secondary indexes

The primary key gives you one way to find an item. A secondary index gives you another. An index is a second, automatically maintained copy of some of your data, organised by different keys. DynamoDB keeps it up to date, and you query it like a table.

Two kinds

  • Local secondary index (LSI): the same partition key as the table and a different sort key. It lets you sort one item collection in a second way, such as a customer's orders by status instead of by date. An LSI must be created with the table. You cannot add one later or remove one. A table can have up to 5. The sort key is exactly one scalar attribute.
  • Global secondary index (GSI): its own partition key and sort key. It opens a whole new access path, such as orders by product. You can add or remove one at any time, one per update call. The default quota is 20 per table. Its keys must be top-level string, number or binary attributes. Index keys do not have to be unique.

What each costs and promises

  • Consistency. A GSI is updated asynchronously and supports eventually consistent reads only. An LSI supports both. Asking a GSI for a consistent read is an error.
  • Capacity. A GSI has its own capacity. Its reads and writes use the index's units, not the table's. An LSI shares the table's capacity: LSI queries use the base table's read units, and updates to the LSI use its write units.
  • Writes multiply. Every write to an item that appears in an index is also a write to the index. A table with three GSIs can turn one table write into up to four. Index entries are only written when the item carries the index's key attributes.
  • Throttling reaches the table. If a GSI does not have enough write capacity, writes to the base table are throttled too. AWS advises giving an index at least the write capacity of the table.
  • Size. A GSI has no size limit. A table with an LSI has a hard limit of 10 GB per partition key value, counting the table and all its LSIs together. When it is reached, writes that grow the collection fail with an error, though deletes and shrinking updates are still allowed.
  • Deleting by TTL removes the entry from every index too.
base table: key = order_idindex: key = customer_id o-1 cust A 120o-2 cust B 80o-3 cust A 45 async copy A: o-1 120A: o-3 45B: o-2 80 strong or eventual readseventual reads only its own writes drive the copyown capacity, can lag a write to the base is also a write to every index that holds the item
A global secondary index is a second copy, filled asynchronously and keyed differently.

Projections: what the index carries

When you create an index, you choose which attributes are copied into it.

  • KEYS_ONLY: the index keys and the table's keys.
  • INCLUDE: the keys plus a list of attributes you name. A table may project up to 100 attributes in total across all of its indexes.
  • ALL: every attribute. The index is as large as the table, and AWS notes that in most cases it doubles storage and write cost.

The table's primary key is always in the index. If a query asks for an attribute that is not projected:

  • An LSI fetches it from the table, at higher latency and cost.
  • A GSI cannot. The attribute is just not available. Project what the query needs.

Rule of thumb: project the fewest attributes that answer your queries without a second lookup.

Adding a GSI to a live table

You can do it while the table is serving traffic. DynamoDB backfills the index from existing items, and the index cannot be queried until it is active. Two things to know: giving the index more write capacity does not make the backfill faster, and if the new index's key is badly skewed, the backfill can throttle writes on the base table.

Choosing

  • Pick an LSI only when you must sort the same partition key a second way, you need strongly consistent index reads, and you are sure the item collection will stay under 10 GB. You decide at table creation, so lean to a GSI when unsure.
  • Pick a GSI for any new key. It is the default tool.
  • Keep the number small. Every index costs storage and writes. AWS advises keeping them to a minimum.
  • Watch the GSI's own key. A GSI with a low-cardinality partition key (a status, a boolean) puts all matching traffic on one partition, just as a bad table key would.

A newer option lets a GSI key be made of up to four attributes, so you do not need to glue them into one string. Check the current documentation before using it.

Remember: an index is a copy that you pay for on every write. Add one for each question that matters, project little, and never assume it is up to date.

Writes that stay correct

A database is only useful if concurrent writes cannot corrupt it. DynamoDB gives you building blocks, and the pattern is always the same: put the check inside the write. Do not read, decide in your code and write back. Two requests can interleave between your read and your write.

Conditional writes

Any PutItem, UpdateItem or DeleteItem can carry a condition expression. DynamoDB checks it and applies the write as one atomic step. If it is false, nothing changes and you get a ConditionalCheckFailedException. That is a normal answer, not a fault, and retrying the same request will not help.

The condition can use attribute_exists, attribute_not_exists, attribute_type, begins_with, contains, size, comparisons and AND, OR and NOT. A condition compares attributes with values you pass in. It cannot do arithmetic on attributes, so total - sold >= n is not allowed. Keep the number you test (such as available) as its own attribute.

Common recipes:

ConditionExpression: attribute_not_exists(pk)

Create the item only if no item with this key exists. This is a uniqueness check on the key, and a safe way to make a request repeatable.

UpdateExpression:    SET available = available - :n
ConditionExpression: available >= :n

Take stock only if enough is left. This cannot oversell, even with a thousand buyers at once.

UpdateExpression:
  SET price = :p, #v = #v + :one
ConditionExpression: #v = :seen

Optimistic locking. Write only if the item still has the version you read. If someone saved first, your write is rejected, and you re-read and decide again. (version is on the reserved words list, so the expression uses the placeholder #v and maps it to the real name in ExpressionAttributeNames.)

Ask for ReturnValuesOnConditionCheckFailure and DynamoDB hands back the existing item when a condition fails, at no extra read cost. That saves a round trip when you need to know why.

Atomic counters, and why retries bite

ADD or SET x = x + :n changes a number on the server in one step, with no read first. It is the right tool for counters. The catch is that it is not idempotent. If a request times out and you send it again, it may be applied twice. AWS recommends SET where you need an idempotent operation. Use a counter only where a slight over-count is acceptable, or guard it with a condition or a transaction token.

Transactions

For changes that must succeed or fail together, use a transaction.

  • TransactWriteItems groups up to 100 Put, Update, Delete and ConditionCheck actions, from one or more tables in the same account and region. The total size is limited to 4 MB.
  • TransactGetItems reads up to 100 items, from tables only (not indexes), and returns a consistent snapshot.
  • All or nothing. If any condition fails, nothing is written, and you get a TransactionCanceledException. It lists one reason per action, in the same order as your request, with a null code for the ones that had no problem.
  • No item twice. Two actions in one transaction cannot target the same item. If you need to check and change one item, put the check in the update's own condition.
  • Cost. A transactional read or write costs twice the units of a normal one.
  • Isolation. Transactions are serializable against each other and against single-item reads and writes. A plain Query, Scan or BatchGetItem is only read-committed, and BatchWriteItem is not serializable as a whole.
  • Idempotency. A ClientRequestToken makes a retried transaction safe: the same token with the same request is applied once, and the same token with different parameters is an error. The token is valid for 10 minutes after the first request finishes. The AWS SDKs add one for you.
  • Conflicts. Two transactions on the same item at the same time collide, and one is cancelled with a conflict reason. Plan on retrying those.
  • Global tables. Transactions are atomic only in the region that took them. Other regions can briefly see parts of a transaction, and multi-region strongly consistent tables do not support transactions at all.

Keep transactions small and few. AWS advises against grouping operations that do not need to succeed or fail together, and a hot item inside a transaction is a bottleneck for everything that touches it.

Batch is not a transaction

BatchWriteItem and BatchGetItem save round trips. They are not atomic, take no conditions on writes and can return unprocessed items. Use them for bulk loads and fan-out reads. Use a transaction when the parts depend on each other.

Making a request safe to repeat

Networks time out, and clients retry. Plan for each write to run twice.

  • Create: a conditional put on a key that comes from the caller (an order id), so a second attempt fails the condition. Then check that the existing item matches the request, and return it.
  • Update: make it a SET to a value (idempotent), not an ADD (not idempotent), or guard it with a condition.
  • Multi-item: use a transaction with a client token.
Remember: conditions turn races into clear errors. Whenever the answer to "what if two of these run together?" is not "nothing bad", add a condition.

Data modelling

This is the skill that decides whether DynamoDB is easy or painful. Relational modelling starts from the data and normalises it, then writes queries later. DynamoDB modelling starts from the questions and shapes the data so each is a cheap lookup. AWS puts it plainly: do not start designing the schema until you know the questions it must answer.

The routine

design from the questions, then check the numbers list thequestionsdraw theentitiespick keysper questionaddindexescheck hotkeys, units a new question later? back to step 1
The routine: list the questions, draw the entities, pick keys, add indexes, check hot keys and units, and loop back for new questions.

AWS's modelling guidance lays out a longer version of the same routine, which is useful for a real project.

  • 1. Identify the use cases and draw the entities. What does the application do, and what are the nouns and their relationships? Gather item sizes, data volume and read and write rates.
  • 2. Estimate cost early, for both capacity modes, in every environment.
  • 3. List the access patterns in a matrix: who asks, what they ask, how often and how fresh the answer must be. For an existing system, mine the query logs.
  • 4. Gather the technical requirements such as latency, consistency and retention.
  • 5. Create the model: the primary key, then sort key, then indexes. Keep tables few, project little and handle large items.
  • 6. Write the queries against DynamoDB Local or a real table, with sample data.
  • 7. Validate that each pattern is one cheap query and collect numbers.
  • 8. Review the cost with real volumes.
  • 9. Deploy with infrastructure as code.

An example: orders

Take an online shop. First, the questions:

  • A1. Get a customer's profile.
  • A2. List a customer's orders, newest first.
  • A3. Get one order with its items.
  • A4. List all open orders, for the warehouse.
  • A5. Find an order when you know only the order id.

Now the keys. One table, with the partition key pk and sort key sk:

pk                sk                   holds
CUST#c_1042       PROFILE              name, email
CUST#c_1042       ORDER#2026-11-02#o7  order summary
ORDER#o7          META                 customer, status
ORDER#o7          ITEM#1               product, qty, price

How each question is answered:

  • A1: GetItem on CUST#c_1042, PROFILE.
  • A2: Query on CUST#c_1042 with begins_with(sk, "ORDER#"), descending. The date is inside the sort key, so the order is newest first. A copy of the order summary lives here, which is deliberate duplication.
  • A3: Query on ORDER#o7. It returns the order and all its items, in one call and one partition.
  • A4: a sparse index (below) on orders that are open.
  • A5: GetItem on ORDER#o7, META.

Notice what happened. An order exists twice, once under the customer for listing and once in its own item collection for detail. That duplication is the price of "no joins". The writes that change an order must update both places, usually in one transaction.

Patterns you will use again and again

Each pattern below is a trick for a recurring problem.

One-to-many with a shared partition key.

  • Problem: get a parent and its children without a join.
  • How: give them the same partition key and different sort keys. One query returns the whole group.
  • Cost: you commit to the access pattern. A busy parent can become a hot key, and with an LSI the group is capped at 10 GB.

Hierarchical sort keys.

  • Problem: query at any level of a hierarchy.
  • How: join the levels into one sort key, such as country#region#city. Then begins_with on any prefix returns that level.
  • Cost: the depth is baked into the key, and you have to build and parse the strings.

Many-to-many with an adjacency list.

  • Problem: model links between two kinds of thing, such as users and groups.
  • How: store each link once as an item (key user, sort group). A GSI with the keys swapped (key group, sort user) answers the other direction.
  • Cost: every link is written twice, once to the table and once to the index. Multi-hop questions need extra work.
table: user, then groupkey: userthen: groupanachessanafilmrajchess swap index: group, then userkey: groupthen: userchessanachessrajfilmana query user = ana: her groupsquery group = chess: its members many-to-many: store each pair once, read it from both ends
An adjacency list: one table of links, and an index with the keys swapped to read it from the other end.

Sparse index.

  • Problem: find a rare subset, such as open orders, without scanning everything.
  • How: put the index key attribute only on the items that belong. Items without it are not in the index. Set it when an order opens and remove it when it ships.
  • Cost: your code must set and remove the attribute. The index is small and cheap to write.
table: all ordersindex on the open flag o-1 shippedo-2 openo-3 shippedo-4 openo-5 shipped only itemswith the flag o-2 openo-4 open small, cheap to readand cheap to keep set the flag when the order opens, remove it when it ships
A sparse index only holds the items that carry its key attribute.

GSI overloading.

  • Problem: you need more access paths than the index quota allows.
  • How: use generic key attribute names, and put different things in them for different item types. One index then serves lookups by name for one type and by warehouse for another.
  • Cost: the schema is harder to read and one index key space is shared by many uses.

Write sharding. See section 10.

Time series.

  • Problem: high-volume data with hot recent rows and cold old ones.
  • How: put a time bucket (a day or a month) in the partition key. Use TTL to expire old items for free. For very large volumes, AWS describes one table per period, so old tables can be scaled down or moved to the Standard-IA class.
  • Cost: reading a long range means several queries. TTL deletion is lazy.

Item versioning.

  • Problem: keep an audit trail and still read the latest cheaply.
  • How: store the current version under a fixed sort key (such as v0) and each old version under v1, v2. Reading the latest is a key lookup.
  • Cost: every update writes at least two items, so use a transaction to keep them in step.

Large attributes.

  • Problem: the 400 KB item limit, or big blobs that inflate every read.
  • How: compress the value, split it over several items under one partition key, or store it in S3 and keep the object key in the item.
  • Cost: you lose filtering on compressed data, and there is no transaction across S3 and DynamoDB, so orphaned objects need cleanup.

Uniqueness on something other than the key.

  • Problem: only the primary key is unique, but you want unique emails.
  • How: this is my suggestion, not an AWS recipe. Write a small lock item keyed by the email, in the same transaction that creates the user, with a condition that it does not exist.
  • Cost: the lock must be kept in step on update and delete, and a transaction costs double.

Counters and aggregates.

  • Problem: counts, sums and top-N lists.
  • How: use an atomic counter, or a pipeline where a stream feeds a function that adds to a summary item. A sparse index can expose a top-N.
  • Cost: counters over-count when requests are retried, a hot counter is a hot key, and a pipeline's result is a few seconds old.

Multi-tenancy.

  • Problem: isolate customers, and delete one cheaply.
  • How: put the tenant id first in the partition key. Closing an account is then a query of that tenant's items and a delete of each.
  • Cost: you still enforce who can see what. A very large tenant is a hot, large partition. Per-tenant encryption keys need separate tables or client-side encryption.

Archival and soft delete.

  • Problem: keep history without slowing the live table.
  • How: expire items with TTL and send the deleted records, which appear in the stream marked as service deletions, to S3 with a function. For a soft delete, set a flag attribute (my suggestion) and use a sparse index to hide them.
  • Cost: the archive arrives late, because TTL is lazy.

Single table or several?

A strongly argued debate sits behind every DynamoDB design.

The case for one table per service.

  • Items read together are stored together, so one query returns a parent and its children with no join.
  • One table has fewer alarms, fewer permissions and smoother traffic.
  • It forces you to model for the questions you ask.

The case for several tables.

  • Simpler to understand, especially while the product is changing.
  • Easier backups, encryption, streams and permissions per entity.
  • Easier GraphQL resolvers, and easier export for analytics when large, never-changing event data is kept apart from small, changing entity data.
  • A DynamoDB stream limits how many consumers can read it, and a single table with many entity types can run into that.

The positions differ. AWS's older guidance says to keep as few tables as possible, and its newer guidance says the choice depends on your requirements. Alex DeBrie, who popularised single-table design, argues its main benefit is fewer round trips, that the savings in operations are small, that it makes new access patterns harder to add and analytics harder, and that early-stage products and GraphQL backends may be better with several tables. Some practitioners now say single-table was oversold. I treat it as a trade-off: start with the access patterns, and let the shape of the data and the team decide, not the slogan. A reasonable middle: keep items that are read together in one item collection, and keep unrelated services in separate tables.

Common modelling mistakes

  • Normalising first, then discovering that every page needs several joins.
  • A low-cardinality partition key, such as a status, a boolean or a date, which puts most traffic on one partition.
  • Too many or rarely used indexes, and ALL projections as a default.
  • Using scans in an online path.
  • Treating an index as instantly up to date.
  • Forgetting that items grow: an ever-growing list inside one item will meet the 400 KB limit.
  • No schema in code. DynamoDB will store anything, so validate on the way in and keep a version attribute.
Remember: model for the questions you can name. Duplicate on purpose, and make the writes that keep the copies in step atomic.

Hot keys and scaling patterns

DynamoDB scales by spreading keys over partitions. It cannot spread one key. A hot key is the most common production problem, so learn to see it coming.

What makes a key hot

  • A low-cardinality partition key. AWS's own guidance rates a user id (many users) as good and a status code as bad, because only a few values share all the traffic.
  • A time-based key. Using today's date as the partition key sends all of today's writes to one partition.
  • One very popular item. A viral post, a global counter, a single configuration row. A split cannot help a single item.
  • A sequential sort key under one partition key. Writes that always go to the end of one key range stay on one partition, and the ceiling is 1,000 write units a second.
  • A hot index key, for the same reasons, which also throttles the base table (section 07).

How to find it

  • Contributor Insights shows the most accessed and the most throttled keys of a table or index.
  • Throttle metrics (ThrottledRequests, ReadThrottleEvents, WriteThrottleEvents) tell you that it happens and the throttling reason says which limit.
  • Consumed units against provisioned units looks fine for the table while one partition is overloaded. That mismatch is the signature of a hot key.

The fixes, from cheapest to most work

- A better key. Spread by something with many values, such as a user id or a device id. Put the time in the sort key, not the partition key. - Smaller, rarer writes. Batch counter updates in the application and flush them every few seconds, instead of one write per event. - Cache the reads. A hot read is cheaper from a cache (section 14) or from the edge, and eventually consistent reads cost half. - Write sharding. Split one hot key into several by adding a suffix. - Random suffix: write to key#3 where the number is random between 0 and N-1. Spread is perfect, but a read has to ask every shard and merge. - Calculated suffix: derive the number from the item, for example a hash of the order id modulo N. A single-item lookup still works, because you can recompute the suffix. - How many shards: enough that each stays under its ceiling. AWS's example splits 5,000 writes a second into 5 shards of about 1,000. - Move the heat out of transactions. A hot item inside a transaction slows everything that touches it.

writers suffix order#day#0order#day#1order#day#2order#day#3order#day#4 readeradds up pick one, 0 toN-1, per write ask everyshard, merge
One hot key becomes several by adding a suffix. Writes spread out, and a read asks every shard and adds the answers.

The cost of sharding is real: reads of the whole become scatter-gather, and a counter split into shards can no longer be checked in one atomic step. That matters for things like "never sell more than N": sharded stock needs a different design, such as handing each shard a fixed quota.

Other scaling habits

  • Exports, not scans. For analytics and full-table jobs, export to S3 (section 13). It does not use read capacity and does not touch the table's performance. If you must scan, use a parallel scan with segments and a limit, so one worker cannot eat all the throughput.
  • Bulk loads. Importing from S3 into a new table does not consume write capacity. Loading a live table with BatchWriteItem does.
  • Let it warm up before launch. A new on-demand table handles a few thousand requests a second at once, and doubles from its previous peak. For a launch that will be much bigger, ask about warm throughput, which lets you raise what a table can support instantly ahead of time.
Remember: the table can scale without limit, but a key cannot. Check how traffic spreads over key values, not just how much traffic there is.

For the general write-scaling toolbox see Scaling writes and Dealing with contention.

Streams, TTL and event-driven patterns

A table that you can only read and write is a store. A table that tells you what changed is a building block.

DynamoDB Streams

A stream records every change to items in a table, in order for each item.

  • One record per item change. Records say what changed. You choose how much they show with the view type: only the keys, the new item, the old item, or both. The view type cannot be edited later. To change it you must turn the stream off and create a new one.
  • 24 hours. Records live for 24 hours at most and are then trimmed. A consumer that falls behind by more than a day loses data.
  • Order is per item. Changes to one item arrive in order. There is no order across items, partitions or the table.
  • No duplicates. Each change appears once in the stream. A write that changes nothing writes no record.
  • Shards. A stream is a set of shards that come and go. A parent shard must be fully read before its children. Each open shard maps to one table partition.
  • No effect on the table. Streams work asynchronously, so enabling one does not slow it down.

Consuming a stream

  • Lambda is the usual consumer. The service polls the stream several times a second and calls your function with a batch of records. If the function fails, the same batch is retried until it succeeds or expires, so one bad record can block a shard. Plan a way to skip or park poison records. By default there is one function instance per shard, and you can raise parallelism up to 10 per shard while keeping per-item order.
  • Too many readers. More than two Lambda functions on one stream can cause read throttling, because a shard allows only two simultaneous readers.
  • Kinesis Data Streams for DynamoDB is the alternative. It keeps records up to a year, supports more consumers (and enhanced fan-out), but it can deliver duplicates, and the order of changes is found from a timestamp attribute. You can turn on both kinds of stream for one table.

Patterns that use a stream

  • Keep a search index or cache in step. A function reads changes and updates OpenSearch or a cache. AWS also offers managed zero-ETL integrations to OpenSearch and to Redshift (the Redshift one syncs incrementally every 15 to 30 minutes).
  • Maintain aggregates. A function adds each change to a summary item, for example totals per day.
  • The outbox pattern. To change data and publish an event reliably, write the data and an "outbox" item in one transaction. A stream function publishes the outbox item to a queue or topic. This avoids the classic bug where the data was saved but the event was lost.
  • Audit and archival. Send changes, or TTL deletes, to S3 for long-term storage.
  • Consumers must be idempotent. A retried batch runs again, so handling a record twice must be safe.

Time to live (TTL)

TTL deletes items for you when they expire.

  • How: name an attribute as the TTL attribute. It must be a number holding epoch time in seconds. An item whose TTL attribute is any other type is simply ignored, and a value in milliseconds is a classic mistake that makes an item look like it expires in the far future.
  • No write cost. The background deletes do not use your write capacity.
  • It is lazy. Items are deleted "typically within a few days" of expiry. Until then they still appear in reads and queries and still count for storage and reads. Filter them out yourself with a filter expression, or a condition on writes. Never use TTL for precise timing, such as a lock or a session that must end now.
  • Indexes and streams. Expired items disappear from every index like any delete, and in the stream they appear as deletions made by the service, which lets a function tell them from deletions made by your code.
  • You can rescue an item by updating its TTL to a future time before it is deleted.
  • Global tables. The delete happens in one region and is replicated, which costs write units on the replicas. Multi-region strongly consistent tables do not support TTL.
table TTL delete streamkept 24 hoursordered per item update search index update cache summary or archive consumers must be safe to run twice on the same record
A table feeds a stream. Functions turn changes into search updates, cache updates, summaries and archives.
Remember: streams make a table an event source, and TTL makes it clean up after itself. Both are asynchronous, so build consumers that are idempotent and tolerate delay.

See also Kafka for event logs in general, and Elasticsearch for a typical stream target.

Global tables and multi-region

A global table keeps copies of one table in several AWS regions. Applications read and write in the nearest region, and DynamoDB replicates the changes. You use it for low latency for users spread across the world, and for surviving the loss of a region.

How it works

  • One replica per region, all with the same schema. Any replica serves reads and writes (multi-active).
  • A write in one region is replicated to the others asynchronously, typically within a second or less. This is multi-region eventual consistency (MREC), the default mode.
  • Conflicts are resolved by last writer wins. If two regions change the same item at nearly the same time, the change with the later internal timestamp survives and the other is lost, item by item.
  • Replication uses a stream internally, and streams are on and cannot be turned off for these tables.
  • Replication uses write capacity on every replica, so a global table's write cost is multiplied by the number of regions.
  • Global tables are designed for 99.999 percent availability, compared to 99.99 percent for a single-region table.
region Areads + writes region Breads + writes region Creads + writes async copy,usually under a second same item in two regions at once: the later write wins
Each region takes reads and writes. Changes flow to the other regions, and the later write wins a conflict.

The two consistency modes

  • MREC (eventual). Asynchronous replication. After a regional failure, some recent writes may not have left the failed region, so the recovery point is more than zero. A strongly consistent read in one region may be stale for an item last written in another.
  • MRSC (strong). A newer mode. A write is replicated synchronously to at least one other region before it succeeds, so the recovery point is zero. It has rules: exactly three regions (or two plus a witness that stores data but takes no reads or writes), the table must be empty at creation, replicas cannot be added later, and it does not support TTL, transactions or LSIs. The mode cannot be changed after the table is created.

Designing for it

  • Avoid writing the same item in two regions. Give each item a home region, and route its writes there. Last writer wins is then rarely needed.
  • Make updates conflict-friendly. Replacing a whole value is safe. Increments in two regions at once are not, because a lost update silently loses a count.
  • Know that transactions stop at the border. A transaction is atomic only in the region that ran it. Other regions can briefly see parts of it.
  • Plan failover at the application layer. Your service decides when to send traffic to another region, and must tolerate stale reads while it does.
  • Know the housekeeping rules. After you create a replica, the table you copied from cannot be deleted for 24 hours, and a region you disable is turned into a single-region copy after about 20 hours.
  • Watch ReplicationLatency (for MREC) to see how far behind the replicas are.
Remember: a global table is multi-active replication with last writer wins. Choose a home region per item unless losing a concurrent write is acceptable.

See CAP theorem for the trade-off behind the two modes.

Backup, restore and recovery

Three-way replication protects you from hardware failure. It does not protect you from your own mistakes: a bad deploy or a bad delete is copied to every replica. Backups protect against that. The DynamoDB team's paper says so directly: backups guard against logical corruption caused by a bug in an application.

Point-in-time recovery (PITR)

  • Continuous backup, with restore to any second in a window you choose, from 1 to 35 days. It is off by default.
  • The latest restorable time is typically about five minutes before now.
  • A restore always creates a new table. You then switch traffic to it, or copy items back. Restore to the same or to another region. The source table stays available during the restore.
  • A restore recreates the data, the indexes, the capacity settings and the encryption settings. It does not bring back auto scaling settings, IAM policies, alarms, tags, stream settings, TTL settings or the PITR setting. Reapply those, ideally from infrastructure as code.
  • No fixed completion time is guaranteed, so test a restore before you rely on its duration.
  • When you delete a table that has PITR on, DynamoDB keeps a system backup for 35 days.

On-demand backups

  • A full copy of a table at a moment, with no limit on how many you keep. Taking or restoring one consumes no throughput and does not affect the table.
  • They are not causally consistent across items: they are a copy of the table at about that moment, not a transaction snapshot.
  • AWS Backup can schedule them, copy them across accounts and regions, and move them to cold storage, with its own encryption key. It must be switched on per account and region.

Export and import

  • Export to S3 writes the table (or just the changes, incrementally) to S3 in DynamoDB JSON or Ion format. It needs PITR on, uses no read capacity and does not touch the table's performance. It is the right way to feed analytics (Athena, Glue, EMR and others).
  • Import from S3 creates a new table from CSV, DynamoDB JSON or Ion files, optionally compressed, and does not consume write capacity.

Preventing the accident

  • Deletion protection stops anyone from deleting a table. It is off by default, including for restored tables and global replicas. Turn it on for production.
  • Least privilege. Most data loss is a human or a script with too many permissions (section 15).

A recovery plan

  • Decide the RPO (how much data you can lose) and RTO (how long you can be down). PITR gives a few minutes of RPO. A global table with MRSC gives zero.
  • Write down the steps: restore, reapply settings, switch the application, verify.
  • Practise it. A restore you have never run is a hope, not a plan.
Remember: replication copies mistakes. Turn on PITR and deletion protection, test a restore, and keep the settings that a restore does not bring back in code.

Caching with DAX

Reads from DynamoDB are already fast, in single-digit milliseconds. DynamoDB Accelerator (DAX) is a managed in-memory cache that brings them to microseconds for repeated reads, and absorbs read spikes on hot keys.

How it works

  • DAX is a cluster that sits between your application and the table. Your code uses the DAX client, which has the same API.
  • It has two caches. The item cache holds results of GetItem and BatchGetItem. The query cache holds results of Query and Scan. By default both keep entries for 5 minutes, which you can change, and old entries leave in least-recently-used order.
  • Writes are write-through. DAX writes to the table first, then updates its item cache. A failed write is not cached.
  • A cluster has one primary and up to 10 read replicas (11 nodes). A production cluster needs at least three nodes across availability zones to survive a failure.

What it does not do

  • Strongly consistent reads and transactional reads bypass it. They go to the table and are never cached. If your reads must be strongly consistent, DAX does not help.
  • The query cache is not invalidated by writes. An update to an item changes the item cache but not stored query results, so a cached query can show old data until its time expires.
  • Writes that bypass DAX (a function writing straight to the table) leave DAX stale until the entry expires.
  • Missing items are cached too, so a "not found" can persist for the TTL.
  • Nodes of a cluster are eventually consistent with each other, so two callers can briefly see different values.
  • A scan does not "warm up" the item cache.
  • It does not support table management calls, and an overloaded cache returns a throttling error.

The lesson from the paper

The DynamoDB team's rule is worth borrowing: a cache can improve performance, but do not let it hide work that the table must still be able to do. If the cache vanishes, the table must still survive the full read load, because a cold or failed cache sends everything to the table at once. Size the table for the miss case, not the hit case.

When to cache, and with what

  • DAX fits read-heavy, repeated reads of the same keys that can tolerate a few seconds of staleness, with little code change.
  • A general cache such as Redis fits when you need more control: your own invalidation, other data structures or caching computed results. See Caching for the patterns.
  • Often you need neither. Fix a hot key first, and use eventually consistent reads, which cost half.
Remember: DAX is a read-through, write-through cache with a TTL. It is wrong for strongly consistent reads and for anything written around it.

Security

Security in DynamoDB is mostly identity, encryption and network paths.

Encryption

  • At rest: always on and it cannot be disabled. It covers the table, its indexes, streams, backups and global table copies. You choose who holds the key: an AWS owned key (the default), an AWS managed key or a customer managed key in KMS that you control, and you can switch at any time. Customer keys must be symmetric.
  • A detail to remember: some sort key values that mark range boundaries are kept in plaintext in the table's metadata. Do not put secrets in sort keys.
  • In transit: clients must support TLS 1.2, and AWS recommends 1.3.
  • Sensitive attributes that even administrators of the account should not read need encryption in your application before they are stored. Encryption at rest protects the storage, not the people with permission to call the API.

Identity and access

  • IAM policies decide who can call which action on which table, index or stream. Give each service a role limited to its own tables, with only the actions it needs.
  • Resource-based policies can be attached to tables, indexes and streams, which allows cross-account access without role juggling. For example, they let a function in another account read a stream.
  • Fine-grained access control limits access to certain items or attributes. A condition on dynamodb:LeadingKeys restricts a caller to items whose partition key matches a value, which is the standard way to isolate tenants or users in a shared table. A condition on attributes limits which attributes can be read or written. For reads without a projection, remember that all attributes are returned regardless of attribute restrictions, so use the right condition keys together.
  • Tags can drive attribute-based access control for tables, but not for streams or backups.

Network

  • VPC endpoints. Reach DynamoDB from a private network through a gateway endpoint (free, but not reachable from on-premises networks, peered VPCs in other regions or through a transit gateway) or an interface endpoint, which does work across those paths.

Audit

  • CloudTrail records control-plane calls (create, update, delete a table) by default. Data events (reads and writes of items) must be switched on, and TTL deletes are not logged there.
  • Deletion protection and backups (section 13) are part of security, because they limit the damage of a mistake.
Remember: encryption protects storage. Access is controlled by IAM, so keep roles narrow, use leading-key conditions for tenants, and turn on data-event logging where you need an audit trail.

Operating DynamoDB in production

What to watch

Set alarms on these CloudWatch metrics.

  • Throttling: ThrottledRequests, ReadThrottleEvents, WriteThrottleEvents. Any sustained non-zero value deserves a look. A batch counts as throttled in ThrottledRequests only if every request in it was, but ReadThrottleEvents counts each underlying read, so use the second to see partial throttling.
  • Consumed units: ConsumedReadCapacityUnits and ConsumedWriteCapacityUnits. Use the Sum statistic and divide by 60 for a per-second figure. One-minute data can hide very short spikes.
  • Errors: SystemErrors (HTTP 500, the service) and UserErrors (HTTP 400, your requests). A failed condition is counted in ConditionalCheckFailedRequests, not in UserErrors, so it needs its own alarm if it matters.
  • Latency: SuccessfulRequestLatency measures the service time only. It does not include the network or your client, so compare it with what the application sees.
  • Transactions: TransactionConflict counts rejected requests due to conflicts. It rises when too many requests touch the same items.
  • Indexes: use the index-name dimension for GSI metrics, and watch an index's own write throttling.
  • Contributor Insights for the hottest and most throttled keys.

Retries and errors

  • Retry server errors (5xx) and throttling (ProvisionedThroughputExceededException, ThrottlingException), with exponential backoff and jitter. The AWS SDKs do this for you. Stop after about a minute.
  • Do not retry the other client errors until you fix the request. A ConditionalCheckFailedException is an answer, not a fault.
  • Batches: retry the unprocessed items, with backoff.
  • SDK retries hide errors and increase latency, so a rise in the client's latency can be the first sign of throttling.

Handling a throttling incident

  • 1. Confirm it with the throttle metrics, and find the reason in the error.
  • 2. Locate the key or index with Contributor Insights.
  • 3. Decide the cause: a hot key (fix the key or shard it), exhausted table capacity (raise it, or move to on-demand), a quota (request an increase), or a configured maximum (raise it).
  • 4. Mitigate now, for example by cutting retries and shedding load, then fix the cause.
  • 5. Verify that throttling stopped and that latency is normal.

Cost control

The bill is driven by item size, request volume, storage and the extras you switch on. Levers:

  • Smaller items and shorter attribute names on large tables.
  • Eventually consistent reads where a slightly old answer is fine.
  • No scans in request paths, and exports for analytics.
  • Fewer indexes and smaller projections.
  • TTL to remove data you no longer need.
  • The right capacity mode, reserved capacity for steady provisioned load, and the Standard-IA class for rarely read data.
  • Use ReturnConsumedCapacity in tests to see what each request costs, before it reaches production.

Changing a model that is already in use

  • Adding attributes is free. Add a version attribute so code can handle old and new shapes.
  • A new access pattern usually means a new GSI. You can add one to a live table. It backfills from existing items. Watch for throttling if its key is skewed.
  • A changed primary key is not possible. You create a new table and migrate: export and import, or a parallel scan that copies items in batches with a rate limit, and dual-write during the move, then switch reads, then retire the old table.
  • Backfill carefully. Use a segment count that matches your workers and a limit so the job does not consume the table's capacity.

Testing

  • DynamoDB Local is good for functional tests. It ignores provisioned throughput, does not run parallel scans, does not track item collection sizes and has no PITR, so it cannot find throttling, cost or index-size problems.
  • Load-test on a real table with realistic key distributions and a spike, and watch the throttling metrics.
  • NoSQL Workbench helps to design and try queries.

Housekeeping

  • Infrastructure as code for tables, indexes, alarms, scaling and backups, so restores and new environments are repeatable.
  • Quotas: a default of 2,500 tables per region (it can be raised), 20 GSIs per table and 5 LSIs, and limits on how many table-management requests can run at once. Request increases before you need them.
  • Deletion protection on in production.
Remember: operating DynamoDB is mostly watching throttling and key heat, retrying politely, and having a tested way back from a mistake.

When to use it, trade-offs, numbers

The short version

  • What: a managed key-value and document database, partitioned by key, with predictable latency at any size.
  • Use when: your questions are known and key-based, you need steady low latency at large or unpredictable scale, you want no cluster to run, and you can accept a model shaped around the access patterns.
  • Know: partition and sort key design, item collections, GSIs and their lag, units and rounding, the per-partition ceiling, hot keys, eventual versus strong reads, conditional writes, transactions, TTL, streams, PITR.
  • Careful: it is not for ad hoc queries, joins or exploratory analytics. A model built for the wrong questions is expensive to fix, because a new question can mean new keys or a migration.

Good fits

  • Key lookups at scale: user profiles, sessions, shopping carts, device state, feature flags, idempotency records.
  • Spiky or unpredictable traffic, especially with on-demand mode and serverless compute.
  • Event-driven designs, where a stream triggers work when data changes.
  • Time-series style data with a time bucket in the key and TTL for expiry.
  • Low-ops teams that want a database with no servers to patch.
  • Multi-region active-active where last writer wins is acceptable.

Poor fits

  • Analytics and reporting that need joins, aggregates and ad hoc filters. Export to S3 and use an analytics engine.
  • Questions you cannot list yet, such as an early product that changes weekly. The modelling work is wasted if the questions keep moving.
  • Heavy relational integrity, such as foreign keys, multi-row constraints and complex transactions across many items. See PostgreSQL.
  • Full-text and fuzzy search. Use Elasticsearch fed from a stream.
  • A single counter or row hit by huge write rates, where you cannot shard.
  • A cost model that grows with every request, at a very high and steady write volume. At that point compare the bill with running your own cluster.

The nearest alternatives

  • PostgreSQL: choose it when you need joins, constraints and flexible queries, and one primary can carry the writes. You get flexibility and give up automatic scale.
  • Cassandra: a similar partitioned model that you run yourself, with tunable consistency. Choose it to avoid cloud lock-in or when you need that tuning, and accept running the cluster.
  • MongoDB: richer queries on documents and secondary indexes, with its own sharding trade-offs.
  • Redis: an in-memory store for caches, counters and fast structures. It is usually a companion to DynamoDB, not a replacement.
  • Distributed SQL databases: SQL with scale-out writes, for when you want relational features and global scale. I have not compared them in detail here.

The honest dimensions to compare are: data model, consistency and transaction scope, query flexibility, how much you operate, how cost grows, and how locked in you become. DynamoDB scores best on operations and steady latency, and worst on query flexibility and portability.

Costs and lock-in

  • Cost grows with requests, item size, storage and extras such as indexes, global replicas, streams, backups and DAX. There are no prices here on purpose. Work out the units for your workload (section 06) and use the AWS pricing page.
  • Lock-in is in the data model. Keys, item collections and the single-table style are shaped for DynamoDB. The migration cost is in rethinking them, not only in moving bytes.

Numbers to know

Checked against the AWS documentation on 2 November 2026. They change over time.

  • Item: at most 400 KB, including attribute names. A partition key value is up to 2,048 bytes. A sort key value is up to 1,024 bytes.
  • Query and Scan: 1 MB per call, then paginate.
  • Batches: BatchGetItem up to 100 items (16 MB). BatchWriteItem up to 25 puts or deletes (16 MB).
  • Transactions: up to 100 actions and 4 MB. Cost is double. The idempotency token lasts 10 minutes.
  • Units: a read unit is 4 KB strongly consistent (half for eventual, double for transactional). A write unit is 1 KB (double for transactional). Sizes round up.
  • Partition ceiling: designed for 3,000 read units and 1,000 write units a second.
  • Default quotas: 40,000 read and 40,000 write units per table, 80,000 each per account (provisioned), 2,500 tables per region. These can be raised.
  • Indexes: 20 GSIs per table by default, 5 LSIs, 100 projected attributes per table, and a 10 GB limit per partition key value when an LSI exists.
  • On-demand: a new table sustains 4,000 writes and 12,000 reads a second, and a table doubles instantly from its previous peak.
  • Burst: up to 300 seconds of saved capacity.
  • Streams: 24-hour retention, two readers per shard.
  • PITR: 1 to 35 days, about five minutes behind. Restore creates a new table.
  • DAX: 5 minutes default cache time, up to 11 nodes.
  • Availability design targets: 99.99 percent for a regional table, 99.999 percent for a global table.
problemtoolthe catchfind items by keypartition key + sort keyquestions fixed up fronta second way inGSI (LSI at creation)lag, extra writesa rare subsetsparse indexyou set and clear the keythe last itemconditional updateloser re-reads, retrieschanges togethertransaction2x cost, 100 actionsa safe retryconditional put or tokentoken lasts 10 minutesa hot keybetter key, shards, cachereads become scatter-gatherread costeventual reads, small itemsa read may be staletraffic spikeson-demand or auto scalinglimits still applyexpire dataTTLlazy, filter on readreact to changesstreams24 h, ordered per itemanother regionglobal tablelast writer winsundo a mistakepoint-in-time recoverynew table, off by defaultfaster readsDAXnot for strong readsanalyticsexport to S3not live data
The decisions in this lesson in one place: the problem, the tool, and its catch.

DynamoDB in an interview or a design review

Reach for DynamoDB in a design when the data is key-based, the scale is large or unpredictable, and you want no database servers to run. Say why, state the cost of the choice, and walk through the model in a fixed order.

The order that works

  • 1. Entities and access patterns. Name the nouns and the questions. Rank the questions by traffic.
  • 2. Keys. For each question, the partition key and sort key that make it one query. Show a sample item.
  • 3. Indexes. One GSI for the second most important question. State its lag.
  • 4. Consistency and writes. Where you need strong reads, conditions or a transaction. Say how a retried request stays safe.
  • 5. Hot keys. Ask how traffic spreads over key values, and name the hottest key and your answer to it.
  • 6. Capacity. Units per second, with the rounding, against the per-partition ceiling.
  • 7. Extras. TTL for expiry, a stream for derived data, a cache for hot reads, PITR for recovery.
  • 8. The trade-off. "I chose DynamoDB over X because Y, at the cost of Z."

Assumptions, labelled as such: 1,000 new links a second at peak, 100 redirects for every new link, so 100,000 redirects a second, and an item of about 0.5 KB.

Questions: redirect from a short code to a long URL (the hot path), create a link, list a user's links, and expire old links.

Keys:

pk = short code        item
abc123                 long_url, owner, created_at, expires_at

The short code is the partition key. A random, well-spread code gives an even spread of load. Creating a link is a conditional put with attribute_not_exists(pk), so a code that already exists makes the write fail, and the service picks a new code and retries. expires_at is the TTL attribute, so expiry costs no writes. A GSI keyed by owner and sorted by created_at answers "list my links", and it is eventually consistent, which is fine here.

Capacity:

  • Writes: 1,000 per second, each under 1 KB, so 1,000 write units a second, plus about 1,000 a second for the GSI.
  • Reads: 100,000 redirects a second, eventually consistent, each under 4 KB, so half a unit each, 50,000 read units a second. That is above the default table quota of 40,000, so you would ask for an increase, or put a cache in front.
  • A viral link: one partition can deliver 3,000 read units a second. At half a unit per eventually consistent read, that is 6,000 reads a second for one hot link. A link that is read more than that needs a cache or a CDN in front, not more table capacity.

Trade-off: DynamoDB gives steady latency at this scale with no servers. In return, "list links by any filter" is not possible unless an index was planned for it.

Chat, in one paragraph

For a chat, use chat_id as the partition key and a time-ordered unique id as the sort key, so one query returns the latest messages of a chat. Do not use a raw timestamp as the sort key: two messages in the same millisecond collide, and one overwrites the other. A time-ordered id such as a ULID, or a timestamp plus a counter, sorts by time and stays unique.

Questions an interviewer often asks next

  • What if one partition key gets most of the traffic? Shard the key, cache the reads, and check the ceiling.
  • Is the GSI consistent? No. It is eventually consistent, so do not read your own write from it.
  • How do you stop two users taking the last item? A conditional update that checks the stock, so the database rejects the second one.
  • How do you make a retry safe? A conditional put on a caller-supplied id, or a transaction with a token.
  • What happens when a node fails? The leader of each affected partition is replaced and a new one takes over after a short wait, and the data is still on the other two copies.
  • How do you recover from a bad deploy? Point-in-time restore to a new table.
  • Why not SQL? Say what you need that SQL does better, or that you do not need it, and say what you gave up.

Common mistakes

  • Designing tables like a relational schema, then needing joins.
  • Using a low-cardinality partition key such as status, a boolean or a date, so one partition takes all the traffic.
  • Querying on non-key attributes and relying on scans.
  • Believing that a filter or a projection makes a read cheaper. Neither does.
  • Treating a GSI as instantly consistent, or asking it for a strongly consistent read.
  • Assuming that more table capacity fixes a hot key. The partition ceiling does not move.
  • Forgetting the 400 KB limit, and growing a list inside one item for ever.
  • Storing large blobs in items instead of in object storage.
  • Using a raw timestamp as a sort key, so simultaneous writes overwrite each other.
  • Planning to add a local secondary index later. It must exist when the table is created.
  • Adding many indexes with ALL projections, and not noticing that each one multiplies writes.
  • Using ADD for something that must be exact, then retrying it. It is not idempotent.
  • Reading, deciding in code, and writing back, with no condition.
  • Expecting TTL to delete on time. It is lazy, and expired items still show in reads.
  • Putting TTL values in milliseconds, or in a non-number type, so items never expire.
  • Expecting a cache in front (DAX) to see writes that bypass it, or to cache strongly consistent reads.
  • Assuming a global table merges concurrent writes. Last writer wins, and the other write is lost.
  • Assuming replication is a backup. It copies mistakes. Turn on point-in-time recovery.
  • Retrying everything. A failed condition is an answer, not a fault.
  • Relying on DynamoDB Local to find throttling or cost problems.
  • Ignoring lock-in and cost: at very high, steady write volume the bill may favour running your own cluster.

Check yourself

  • What do the partition key and the sort key each control?
  • What is an item collection, and why does it matter for modelling?
  • Why does a hot partition throttle writes when the whole table has spare capacity?
  • What are the three replicas of a partition for, and which one serves a strongly consistent read?
  • Why does a strongly consistent read cost twice an eventually consistent one, and why can an index not serve it?
  • How many units does a 6 KB item cost to read strongly, to read eventually and to write?
  • A table takes 8,000 writes per second of 1.5 KB items. How many write units is that? Is it near any limit?
  • Why does a projection or a filter not reduce read cost?
  • When would you choose an LSI over a GSI, and what is the catch?
  • Why can a GSI throttle writes to its own table?
  • What is a sparse index, and when does it help?
  • How would you give an email address uniqueness in DynamoDB?
  • How do you stop two buyers taking the last item?
  • Why is ADD risky on a retry, and what is the safer option?
  • What is the difference between BatchWriteItem and TransactWriteItems?
  • What does a transaction cost, and what can it not do?
  • Why is a raw timestamp a poor sort key, and what would you use?
  • Describe write sharding, and what it costs on the read side.
  • What does TTL promise about timing, and how do you hide expired items?
  • When does a DynamoDB stream lose data, and why must consumers be idempotent?
  • In a global table, what happens when two regions change the same item at once?
  • What does point-in-time recovery not restore?
  • Give one case where you would pick PostgreSQL over DynamoDB.
  • Sketch a design for a link shortener: keys, one index, and the capacity for the read path.

Further reading

The structure and ideas of this lesson follow the sources below. The text is my own wording, and where a claim comes from a paper or from AWS, it was checked against it.

From the DynamoDB team

AWS documentation

Practitioners