Top 50 DynamoDB Interview Questions 2026
DynamoDB comes up in three kinds of interview: backend and serverless roles at companies that run on AWS, cloud and solutions architect loops, and system design rounds where "use DynamoDB" is a reasonable answer that the interviewer will immediately push on. Data engineers and technical PMs who own a DynamoDB-backed product get a lighter version of the same questions. If AWS is on the job description, expect at least a few of these.
What the interviewer is really testing is not whether you remember the API names. It is whether you understand that DynamoDB inverts relational design: you list the queries first and shape the keys to answer them, because there are no joins and a Scan is almost always the wrong answer. The strong candidate talks about access patterns, partition key cardinality, and which hard limit (400 KB items, 1 MB pages, per-partition throughput) the design will hit first. The questions below escalate from fundamentals to the modelling scenarios that live rounds actually use.
Practice alongside this post. The AWS question bank and the databases question bank drill this material as timed multiple-choice, and the system design trade-offs cheat sheet is worth keeping open for the consistency and partitioning questions. The free daily challenge gives you one question a day with no account needed. For the theory underneath, read SQL vs NoSQL in a system design interview and database sharding explained for interviews.
Part 1: Fundamentals (1-10)
1. What is DynamoDB, and when would you choose it?
AWS's fully managed, serverless key-value and document database. You create a table and pick a capacity mode; it partitions and replicates data across Availability Zones for you, with single-digit-millisecond reads and writes by key at almost any scale.
Choose it when access patterns are known and key-based, when you need predictable latency at spiky scale, and when you want no operational burden. It suits serverless stacks because it is HTTP-based and needs no connection pooling. Avoid it, or pair it with something else, for ad-hoc queries, joins, heavy aggregation or full-text search.
The follow-up: "what do you do when product asks for a query nobody planned for?" A new GSI or an export to an analytics store - not a Scan.
2. What is the difference between a partition key and a sort key?
The primary key is either a simple key (partition key only) or a composite key (partition key plus sort key).
- The partition key is hashed to decide which physical partition stores the item. All items with the same partition key form an item collection.
- The sort key orders items inside an item collection and enables range queries:
begins_with, between, <, >.
The key (or key pair) must be unique. The design consequence: a Query specifies exactly one partition key value and narrows by sort key, so the sort key is where you encode hierarchy and time (ORDER#2026-10-13#8812) - one Query then returns "this customer's latest 20 orders" with no filtering.
3. What is the maximum item size, and what do you do when data is bigger?
400 KB, including attribute names as well as values - which is why some teams use short names like pk, sk, t.
When data is bigger: store the blob in S3 and keep its key plus metadata in DynamoDB (the standard answer), split the item into parts in one item collection, or compress text into a Binary attribute.
The follow-up: "why does size matter below the limit?" Capacity is billed by size - a write unit covers 1 KB, so updating one field on a 40 KB item costs 40 write units.
4. What data types does DynamoDB support?
- Scalar: String, Number, Binary, Boolean, Null.
- Document: List and Map, nestable up to 32 levels deep.
- Set: String Set, Number Set, Binary Set (unique values of one type).
Gotchas: Numbers have up to 38 digits of precision and travel as strings on the wire, so Python's SDK hands you a Decimal. Empty strings are allowed on non-key attributes, but an empty Set is not. Key attributes must be String, Number or Binary. Beyond the key there is no schema, so two items in one table can have completely different attributes - which is what makes single-table design possible.
5. How are read and write capacity units calculated?
- 1 RCU = one strongly consistent read per second of an item up to 4 KB, or two eventually consistent reads per second.
- 1 WCU = one write per second of an item up to 1 KB.
- Transactional reads and writes cost double: 2 RCUs per 4 KB, 2 WCUs per 1 KB.
Sizes round up: a 4.5 KB strong read costs 2 RCUs. On-demand uses the same arithmetic. For Query and Scan, the size is the total of everything read, before any filter is applied - which is why filters do not save money (question 14).
6. What is the difference between eventually consistent and strongly consistent reads?
Every write is stored durably across multiple Availability Zones. An eventually consistent read (the default) may hit a replica that has not yet applied the most recent write, so it can return slightly stale data; consistency is typically reached within a second. A strongly consistent read (ConsistentRead: true) returns the latest acknowledged write.
| Eventually consistent | Strongly consistent |
|---|
| Cost | 0.5 RCU per 4 KB | 1 RCU per 4 KB |
| Default | Yes | No, opt in |
| Supported on GSIs | Yes | No |
| Supported on LSIs | Yes | Yes |
| Via DAX | Cached | Passed through, not cached |
Use eventual reads by default and strong reads for read-after-write flows, and note that GSIs cannot do strong reads.
7. What are the core data-plane operations?
- Single item:
GetItem, PutItem, UpdateItem, DeleteItem.
- Multi-item reads:
Query (one partition key, optional sort key condition) and Scan (whole table or index).
- Batch:
BatchGetItem (up to 100 items, 16 MB) and BatchWriteItem (up to 25 put or delete requests, 16 MB).
- Transactions:
TransactGetItems and TransactWriteItems (up to 100 unique items, 4 MB).
- PartiQL:
ExecuteStatement, BatchExecuteStatement, ExecuteTransaction - a SQL-compatible syntax over the same engine.
Nuance: PutItem replaces the whole item; UpdateItem modifies named attributes (and upserts). Using PutItem for a partial update and wiping attributes is a classic bug.
8. What is the difference between Query and Scan?
Query goes straight to one item collection by partition key and reads items in sort key order, optionally narrowed by a sort key condition. Its cost is proportional to what it returns.
Scan reads every item in the table or index, in no useful order, and charges you for everything it reads. On a large table it is slow, expensive, and burns capacity that live traffic needs.
table.query(
KeyConditionExpression=Key("pk").eq("CUSTOMER#42") & Key("sk").begins_with("ORDER#"),
ScanIndexForward=False,
Limit=20,
)
Scan is fine for one-off backfills and small tables, ideally as a rate-limited parallel scan (Segment/TotalSegments), or replaced by an export to S3. If your design needs a Scan on the request path, the design is wrong.
9. Does DynamoDB support SQL?
Partly, through PartiQL: SELECT * FROM "Orders" WHERE pk = 'CUSTOMER#42', plus INSERT, UPDATE and DELETE. But it is syntax, not a query planner. There are no joins or GROUP BY, and a SELECT without an equality condition on the partition key becomes a full table Scan. That is the gotcha: PartiQL makes an accidental Scan look like an innocent WHERE clause. Model exactly as you would with the native API.
10. How is DynamoDB priced at a high level?
You pay for throughput (per request on on-demand, per capacity unit-hour on provisioned), storage (per GB-month), and features (backups, PITR, replicated writes for global tables, data transfer out, DAX nodes, exports). The Standard-Infrequent Access table class has cheaper storage and pricier requests, so it suits large, rarely touched tables.
Do not quote prices from memory. Say what drives the bill: item size times request rate, the number of GSIs (each one is another copy of every write), and whether storage or throughput dominates. A table with five GSIs projecting ALL pays for roughly six writes on every update.
Part 2: Indexes and querying (11-20)
11. What is a Global Secondary Index (GSI)?
A GSI is a separately partitioned copy of selected table data with a different partition key and optional sort key. It lets you query the same items by another access path - orders by status, users by email.
Key facts:
- Default quota of 20 GSIs per table, and you can add or delete them at any time.
- The GSI key attributes do not need to be unique, and items missing the GSI key attribute are simply not in the index (see sparse indexes, question 17).
- GSIs are updated asynchronously, so reads are eventually consistent only.
- On provisioned tables, a GSI has its own capacity. If a GSI cannot keep up with writes, DynamoDB throttles writes to the base table (GSI back-pressure), so an under-provisioned GSI breaks writes that never mention it. On-demand tables avoid manual sizing but the same principle applies to hot GSI keys.
Follow-up: "you wrote an item and the GSI query does not show it - why?" Asynchronous index propagation.
12. What is a Local Secondary Index (LSI), and how does it differ from a GSI?
An LSI keeps the same partition key as the table but uses a different sort key, so you can sort or range-query one item collection a second way (orders by date, and also orders by total).
| LSI | GSI |
|---|
| Partition key | Same as table | Any attribute |
| When created | Only at table creation | Any time |
| Max per table | 5 | 20 (default, adjustable) |
| Strongly consistent reads | Yes | No |
| Capacity | Shares the table's | Its own (provisioned) |
| Item collection size limit | 10 GB per partition key value | None |
| Fetch non-projected attributes | Yes, at extra cost | No |
The real gotcha: with an LSI, each item collection (plus its LSI entries) must fit in 10 GB and cannot be split across partitions. Default to GSIs; use an LSI only for a strongly consistent alternate sort order.
13. What are projections, and how do you choose one?
A projection decides which attributes are copied into an index:
- KEYS_ONLY - table and index keys only. Cheapest storage and writes.
- INCLUDE - keys plus a named list of attributes (up to 100 projected attributes across all indexes of a table).
- ALL - every attribute. Simplest, most expensive.
Project what the query behind the index returns; KEYS_ONLY forces a follow-up BatchGetItem on the table.
Two follow-ups: a GSI query cannot fetch attributes that are not projected (an LSI can, at extra read cost), and every projected attribute is written again on every change, so ALL on a write-heavy table can double or triple write cost. Projection type cannot be changed after the index is created; you create a new index instead.
14. Why does a FilterExpression not reduce cost?
A filter runs after items are read from the partition and before they are returned. You are charged read capacity for everything read, not what survives the filter.
It also breaks pagination: Limit: 20 caps items evaluated, so a filtered Query can return 0 items plus a LastEvaluatedKey. Code that treats an empty page as "done" silently drops data.
Filters are fine for trimming a small result set. If a filter discards most of what it reads, that pattern needs its own key or GSI: "push the predicate into the key, not the filter."
15. What is a KeyConditionExpression, and what operators can you use on the sort key?
It is the part of a Query that DynamoDB uses to locate items. It must contain an equality condition on the partition key, and optionally one condition on the sort key: =, <, <=, >, >=, BETWEEN, or begins_with.
You cannot use contains, <>, OR, or IN in a key condition, and you cannot put non-key attributes there - those go in a FilterExpression.
KeyConditionExpression: "pk = :pk AND sk BETWEEN :from AND :to"
With a sort key of ORDER#<ISO date>#<orderId>, "orders in October" is a BETWEEN. With a random UUID, it is a filter over everything.
16. How do you paginate results?
Query and Scan return at most 1 MB of data per call (before filtering), or fewer if you set Limit. If there is more, the response includes LastEvaluatedKey; you pass it back as ExclusiveStartKey on the next call. When LastEvaluatedKey is absent, you are done.
kwargs = {"KeyConditionExpression": Key("pk").eq("USER#7")}
while True:
resp = table.query(**kwargs)
yield from resp["Items"]
if "LastEvaluatedKey" not in resp:
break
kwargs["ExclusiveStartKey"] = resp["LastEvaluatedKey"]
Gotchas:
- Loop on
LastEvaluatedKey, never on "the page was empty" (filters, question 14).
- For a public API, do not hand clients the raw key - encode it (and ideally sign or encrypt it), since it exposes your key structure.
- There is no "jump to page 50". DynamoDB pagination is cursor-based; offset pagination needs a different design.
17. What is a sparse index, and why is it useful?
A GSI only contains items that have the index's key attributes. If only 1% of items have the attribute, the index holds only that 1%.
Use it as a cheap materialized filter: set openTicketPk only on open tickets and remove it on close, and a Query on that GSI lists open tickets without touching closed ones. Writes to items without the attribute never touch the index.
The follow-up: "isn't one value like OPEN a hot partition?" At high volume, yes - shard it (OPEN#0 to OPEN#9) or key it by tenantId.
18. What is index overloading?
Using one GSI for several unrelated access patterns by storing different kinds of values in generic attributes like gsi1pk / gsi1sk.
Item type gsi1pk gsi1sk
Order STATUS#SHIPPED 2026-10-13#ORD#881
Employee DEPT#ENG NAME#Ana Silva
Ticket ASSIGNEE#u17 PRIORITY#1#T#552
One index answers three unrelated queries, which keeps single-table designs inside the GSI quota and cuts write costs. The price is readability: without a written access-pattern table, nobody knows which entity writes which prefix.
19. How do expression attribute names and values work, and why are they needed?
DynamoDB has a long list of reserved words - name, status, date, data, timestamp, count, size and many more. You can store attributes with those names, but you cannot reference them directly in an expression. You alias them with #:
table.update_item(
Key={"pk": "ORDER#881", "sk": "META"},
UpdateExpression="SET #s = :s, #u = :now",
ConditionExpression="#s = :expected",
ExpressionAttributeNames={"#s": "status", "#u": "updatedAt"},
ExpressionAttributeValues={":s": "SHIPPED", ":expected": "PAID", ":now": "2026-10-13T10:00:00Z"},
)
#name placeholders go in ExpressionAttributeNames. Also needed for names with dots or dashes, so a top-level attribute literally named a.b is not read as a nested path.
:value placeholders go in ExpressionAttributeValues. Values can never be inlined.
The classic interview bug: ValidationException: Attribute name is a reserved keyword; reserved keyword: status. Many teams alias every name by default so they never think about it.
20. What can an UpdateExpression do?
Four clauses, combinable in one expression:
- SET - assign attributes, do arithmetic (
SET views = views + :one), append to lists (list_append), or set only if missing (if_not_exists(createdAt, :now)).
- REMOVE - delete attributes or list elements.
- ADD - increment a number or add elements to a set (creates the attribute if absent).
- DELETE - remove elements from a set.
SET #c = if_not_exists(#c, :zero) + :one, lastSeen = :now
REMOVE tempToken
The point interviewers listen for: arithmetic in UpdateItem is atomic on the server, so no read-modify-write cycle. But it is not idempotent - a retried request increments twice (question 40).
Part 3: Data modelling (21-28)
21. What does "access-pattern-first" modelling mean?
In a relational database you normalize the entities and then write whatever queries you need. In DynamoDB you list the queries first and design keys so each one is a GetItem or a single Query.
The process: sketch the entity model to understand the domain; list every access pattern with parameters and frequency ("list a customer's orders, newest first"); design table and GSI keys so each pattern is one key lookup; write the mapping down (pattern, index, key condition); then check volumes - which keys are hot, how big collections grow.
The trade-off to say aloud: known queries become fast and cheap, unknown queries become hard. That is why ad-hoc analytics gets shipped elsewhere.
22. What is single-table design?
Storing multiple entity types in one table with generic key names (pk, sk) and type prefixes, so that related items share an item collection and can be fetched together in one Query.
pk sk attributes
CUSTOMER#42 PROFILE name, email
CUSTOMER#42 ORDER#2026-10-01#7 total, status
CUSTOMER#42 ORDER#2026-10-12#9 total, status
ORDER#9 ITEM#1 sku, qty
ORDER#9 ITEM#2 sku, qty
One Query(pk = CUSTOMER#42) returns the profile and recent orders - effectively a pre-joined result. Benefits: fewer round trips, one set of capacity and alarms, related data co-located.
Costs: harder to read, evolve and export, since every consumer must understand the prefixes. A pragmatic middle path is one table per service, not one for the whole company.
23. When would you NOT use single-table design?
- Access patterns are still changing, so you would keep reshaping the key scheme.
- Entities are never fetched together, so co-location buys nothing.
- Entities need different settings - one needs global tables, another is ephemeral; or a Streams consumer only cares about one type.
- The team is new to DynamoDB and readability beats shaving a round trip.
- Analytics is heavy, and a clean table per entity exports more simply.
Interviewers are checking you are not dogmatic in either direction.
24. How do you model a one-to-many relationship?
Several options, in rough order of preference:
- Item collection - parent and children share the partition key, children have prefixed sort keys (
CUSTOMER#42 / ORDER#...). One Query gets the parent and its children. Best when children are unbounded or updated independently.
- Denormalized list or map in the parent item - fine when the "many" is small, bounded, and read together, such as a few shipping addresses. Watch the 400 KB limit.
- GSI on the child's foreign key - the child is its own item and a GSI keyed on
customerId lists them. Useful when children are mainly accessed by their own id.
Ask aloud: "is the many side bounded?" If not, never embed it.
25. How do you model a many-to-many relationship?
The adjacency list pattern: store each edge as an item, and add a GSI that inverts the key.
pk sk (GSI1: pk=sk, sk=pk)
STUDENT#1 CLASS#math
STUDENT#1 CLASS#art
STUDENT#2 CLASS#math
- "Classes for student 1": Query table where
pk = STUDENT#1.
- "Students in math": Query GSI1 where
sk = CLASS#math (the inverted index).
Copy display attributes onto the edge item so neither query needs a second lookup. The cost: renaming a class updates every edge - the denormalization trade-off interviewers want you to raise.
26. How do you design a sort key for hierarchical or time-based data?
Encode the hierarchy left to right, most general first, so begins_with can query any level:
sk = COUNTRY#US#STATE#CA#CITY#SF#STORE#112
begins_with(sk, "COUNTRY#US#STATE#CA") returns every California store.
For time, use a sortable format: ISO-8601 strings (2026-10-13T10:00:00Z) or zero-padded numbers, and put the time before a unique suffix (MSG#2026-10-13T10:00:00Z#<id>) so items sort chronologically and stay unique. ScanIndexForward: false gives newest first.
Gotchas: sort is lexicographic, so ORDER#9 sorts after ORDER#10 unless padded, and key attributes cannot be updated in place.
27. How do you handle a "get by unique attribute" like email, and enforce uniqueness?
Two separate concerns:
- Lookup by email: a GSI keyed on
email. Simple, but eventually consistent and it does not enforce uniqueness.
- Uniqueness: write a second item whose key is the unique value, inside a transaction with the user item:
TransactWriteItems:
Put pk=USER#u17 sk=PROFILE condition attribute_not_exists(pk)
Put pk=EMAIL#a@ex.com sk=EMAIL condition attribute_not_exists(pk)
If either condition fails, nothing is written; changing an email is a transaction swapping the EMAIL# items. It shows you know primary keys are the only uniqueness DynamoDB enforces.
28. How do you handle schema changes and migrations?
DynamoDB has no schema beyond keys, so "migration" means changing the data your code writes and reads:
- Additive changes need no migration; code tolerates the attribute being absent.
- New access pattern: add a GSI, and backfill its key attribute onto existing items with a throttled parallel Scan.
- Key changes: key attributes cannot be updated, so copy items to new keys (often a new table), dual-write, and switch reads once the backfill is verified.
- Versioned items: store
schemaVersion and upgrade lazily on read.
The senior signal: backfills consume capacity and must be rate-limited, and dual-write plus verification beats a big-bang cutover.
29. How does DynamoDB partition data?
DynamoDB hashes the partition key to choose a physical partition, and splits partitions as data grows or as throughput demands rise. You never manage partitions directly, but you live with their limits: each partition serves at most about 3,000 read units and 1,000 write units per second, and a partition holds up to 10 GB.
The consequence is that total table capacity does not help a single key that receives all the traffic. A table with 100,000 WCUs of headroom will still throttle writes to one partition key that exceeds 1,000 writes per second. That is why partition key cardinality - many distinct, evenly accessed values - is the first property of a good key.
30. What is a hot partition, and how do you fix one?
A hot partition (or hot key) receives a disproportionate share of traffic and throttles while the rest of the table idles. Common causes: a low-cardinality key (status, date), a celebrity item, or a time-ordered key where everyone writes "today".
Fixes:
- Better key choice - high cardinality, like
userId instead of country.
- Write sharding - append a suffix (
DATE#2026-10-13#7, with a random or hashed 0-9) to spread writes, then read with 10 parallel Queries and merge.
- Cache hot reads - DAX or ElastiCache in front of a celebrity item.
- Aggregate in memory before writing, for counters that update thousands of times per second.
To find one, CloudWatch Contributor Insights shows the most accessed and most throttled keys.
31. What are burst capacity and adaptive capacity?
- Burst capacity: DynamoDB retains up to about five minutes of unused provisioned capacity and can spend it on short spikes. AWS documents this as subject to change, so do not design around it.
- Adaptive capacity: when access is uneven, DynamoDB automatically gives a hot partition more of the table's total capacity, and it can split partitions so frequently accessed items are isolated. It is on for every table at no cost.
The limit interviewers want you to say: adaptive capacity cannot exceed the per-partition maximum (about 3,000 RCU / 1,000 WCU), so a single item beyond that still throttles. It also cannot split an item collection on a table with an LSI, and splitting by sort key does not help if traffic follows a monotonically increasing sort key (everyone writing to "now"). Adaptive capacity makes uneven access survivable; it does not make a bad key good.
32. On-demand vs provisioned capacity - how do you choose?
| On-demand | Provisioned |
|---|
| Billing | Per request | Per capacity unit-hour, whether used or not |
| Scaling | Automatic; instantly handles up to double the previous peak | You set RCU/WCU, optionally with auto scaling |
| Best for | Unknown, spiky, new or low-traffic workloads | Steady, predictable traffic at high utilization |
| Cost control | Optional per-table maximum throughput | Fixed ceiling by design; reserved capacity discounts |
| Default | Recommended default in AWS docs | |
On-demand is the AWS-recommended default. Provisioned plus auto scaling (and reserved capacity) wins on cost when traffic is steady and you can keep utilization high. You can switch provisioned-to-on-demand up to four times in a rolling 24 hours, and back at any time.
The follow-up: "can on-demand still throttle?" Yes - if traffic more than doubles the previous peak within 30 minutes, if you hit the per-table quota or a configured max, or on a hot partition.
33. What is warm throughput, and when would you pre-warm a table?
Warm throughput is the read and write throughput a table or GSI can handle instantly without throttling, based on the capacity it has been scaled to. A new on-demand table starts able to sustain around 4,000 writes and 12,000 reads per second, and grows as traffic grows.
Pre-warm before a known step change - a launch, a bulk load, a ticket sale - or the first minutes can throttle while DynamoDB scales out. The signal is knowing "serverless" does not mean "instantly infinite".
34. How do you handle throttling in application code?
- Exponential backoff with jitter - the SDKs do this by default; tune retry counts to your latency budget.
- Read the error:
ProvisionedThroughputExceededException and ThrottlingException are retriable; a ValidationException is a bug.
- Check unprocessed items on batch calls (question 39), and alarm on throttle metrics.
- Smooth bursts with SQS in front of heavy write paths, so a spike becomes a backlog instead of errors.
The design answer matters more: persistent throttling is usually a key problem (question 30) or an under-provisioned GSI (question 11), and retries only hide it.
35. What is DAX, and when would you use it?
DynamoDB Accelerator is a managed, write-through, in-memory cache that is API-compatible with DynamoDB, giving microsecond-latency reads for cache hits. A cluster has one primary and up to ten read replicas and runs inside your VPC.
It has an item cache (for GetItem / BatchGetItem) and a query cache (for Query / Scan results). Gotchas:
- Strongly consistent reads bypass the cache - DAX passes them to DynamoDB and does not cache the result.
- The query cache is not invalidated by writes to the items it contains; it expires by TTL, so stale query results are expected.
- It helps read-heavy, repeated-key workloads, not write-heavy ones.
Skip it when you need strong consistency or reads rarely repeat. The natural follow-up is Redis: DAX is a transparent drop-in; Redis is a general data-structure server you cache into explicitly.
Part 5: Writes, consistency and transactions (36-42)
36. What is a conditional write?
A write (PutItem, UpdateItem, DeleteItem) with a ConditionExpression that DynamoDB evaluates against the current item atomically before applying the write. If the condition is false, the write fails with ConditionalCheckFailedException and nothing changes.
PutItem ... ConditionExpression: attribute_not_exists(pk) # create only
UpdateItem ... ConditionExpression: balance >= :amount # no overdraft
DeleteItem ... ConditionExpression: #s = :draft # only delete drafts
This is how you enforce invariants without locks. A failed condition still consumes write capacity; ReturnValuesOnConditionCheckFailure: ALL_OLD returns the current item and saves a read.
37. How do you implement optimistic locking?
Keep a version number on the item. Read it, compute the change, and write back with a condition that the version has not moved:
table.update_item(
Key={"pk": "DOC#5", "sk": "META"},
UpdateExpression="SET body = :b, version = version + :one",
ConditionExpression="version = :expected",
ExpressionAttributeValues={":b": new_body, ":one": 1, ":expected": read_version},
)
If another writer got there first, the condition fails; you re-read, re-apply, and retry, or surface a conflict to the user. Some SDK mappers (for example the Java enhanced client's version attribute) do this for you.
"Why not pessimistic locking?" There is no row lock; you can emulate one with a lease item, but optimistic locking is simpler when conflicts are rare. Under high contention, redesign so writers do not share an item.
38. How do DynamoDB transactions work, and what are their limits?
TransactWriteItems applies up to 100 unique items (and 4 MB) of Put, Update, Delete and ConditionCheck actions all-or-nothing, across one or more tables in the same account and Region. TransactGetItems reads up to 100 items as a consistent snapshot.
Limits and gotchas:
- Two actions cannot target the same item in one transaction.
- Transactional operations cost twice the normal capacity.
- Transactions conflict with concurrent writes to the same items and fail with
TransactionCanceledException, which lists a reason per action; your code must inspect it.
- Pass a
ClientRequestToken to make retries idempotent.
- In global tables, ACID guarantees apply only in the Region where the write was made; other Regions can observe partially replicated transactions.
Follow-up: "a transaction for every write?" No - only for real multi-item invariants like money movement, uniqueness or inventory.
39. What are batch operations, and what are unprocessed items?
BatchWriteItem sends up to 25 put or delete requests (16 MB total); BatchGetItem fetches up to 100 items (16 MB total). They save round trips, but they are not atomic and do not support conditions or updates.
The gotcha is partial success. If some requests are throttled or exceed limits, the call still returns HTTP 200 with the leftovers in UnprocessedItems (writes) or UnprocessedKeys (reads). Code that ignores those fields silently loses data.
while request_items:
resp = client.batch_write_item(RequestItems=request_items)
request_items = resp.get("UnprocessedItems", {})
if request_items:
sleep_with_backoff()
Helpers such as boto3's table.batch_writer() retry unprocessed items for you.
40. How do you make writes idempotent?
SDKs, Lambda and SQS all retry, so writes must be safe to repeat:
- Natural keys -
PutItem of the same complete item twice yields the same result.
- Condition on creation -
attribute_not_exists(pk) so a duplicate create fails harmlessly.
- Idempotency record - write
IDEMPOTENCY#<requestId> in the same transaction as the business change, with a TTL to clean it up later.
- Transaction client token -
ClientRequestToken makes a retried TransactWriteItems not apply twice within the token's window.
Call out the non-idempotent ones: ADD, list_append, SET x = x + :n.
41. How would you implement a distributed counter that receives very heavy writes?
One item with ADD count :1 works until it passes the per-partition write limit. Options at higher scale:
- Sharded counter - N items, each with its own partition key (
COUNTER#views#0 to #9). Each write picks a random shard; a read fetches all N with one BatchGetItem and sums them. Do not put the shards under one shared partition key with different sort keys - that brings the hot key straight back.
- Aggregate upstream - buffer increments in memory, Kinesis or SQS and write a sum every few seconds.
- Stream-driven rollups - write raw events, and a Streams consumer maintains the totals.
The trade-off is freshness versus throughput: a like-count a few seconds behind is fine; a bank balance is not.
42. What happens if two clients update the same item at the same time?
Without conditions, last writer wins for any attribute both set. UpdateItem calls touching different attributes merge cleanly; a PutItem replaces the whole item and can erase the other writer's change. Atomic operations (ADD, SET x = x + :n) are safe because the server serializes them.
When a write depends on a value read earlier, use a conditional write or version attribute (question 37), or a transaction if the invariant spans items. Global tables in the default mode resolve cross-Region conflicts last-writer-wins too.
Part 6: Streams, TTL, global tables and operations (43-46)
43. What are DynamoDB Streams, and what are they used for?
A Stream is a time-ordered change log of item-level modifications (insert, modify, remove). You choose what each record contains: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, or NEW_AND_OLD_IMAGES. Records are retained for 24 hours, and ordering is guaranteed per item, not across the table.
Uses: Lambda triggers, aggregates and materialized views, fan-out to search or analytics, auditing, and the outbox pattern.
Gotchas: consumers must be idempotent because retries redeliver records; a failing Lambda batch blocks its shard until it succeeds or expires, so set retry limits, bisect-on-error and an on-failure destination. Kinesis Data Streams for DynamoDB is the alternative for longer retention or more consumers.
44. How does TTL work, and what are its gotchas?
You enable TTL on a table and name an attribute holding an expiry timestamp as a Number in Unix epoch seconds. After that time, DynamoDB deletes the item in the background at no write cost.
Gotchas:
- Deletion is not immediate - AWS documents it as typically within a few days of expiry. Expired items still show up in reads, so filter them out (
expiresAt > :now) if correctness matters.
- Milliseconds or ISO strings are silently ignored, because the attribute must be epoch seconds as a Number. This is the most common TTL bug.
- TTL deletes appear in Streams as service deletions, so consumers can distinguish them from user deletes - useful for archiving expired items to S3.
- In global tables, the replicated deletes consume write capacity in other Regions.
Typical uses: sessions, idempotency records, temporary tokens.
45. What are global tables, and what consistency do they offer?
Global tables replicate a table to multiple Regions, with every replica accepting reads and writes (active-active). There are two consistency modes, chosen at creation:
| MREC (default) | MRSC |
|---|
| Replication | Asynchronous, typically a second or less | Synchronous to at least one other Region |
| Conflicts | Last writer wins, per item | Conflicting write fails with ReplicatedWriteConflictException |
| Strong reads | Latest only if last written in the same Region | Latest version from any replica |
| RPO | Seconds (replication lag) | Zero |
| Shape | Any number of Regions | Exactly three Regions (three replicas, or two plus a witness) |
MRSC trades write latency for zero RPO, runs in a subset of Regions, and does not support TTL or LSIs. Transactions stay Region-local in both modes. A good answer routes each user to a "home" Region so last-writer-wins conflicts stay rare.
46. How do backups and point-in-time recovery work?
- On-demand backups - full snapshots you trigger (or schedule through AWS Backup), retained until deleted, with no performance impact on the table.
- Point-in-time recovery (PITR) - continuous backups that let you restore to any second within a recovery period you configure between 1 and 35 days.
Both restore into a new table, which does not inherit auto scaling policies, alarms, IAM policies, stream or TTL settings - so rehearse restores. Export to S3 (built on PITR) gets data to analytics without consuming table capacity.
The senior answer: PITR protects against bad deploys and accidental deletes; global tables protect against a Regional outage. Neither substitutes for the other.
Part 7: Modelling and debugging scenarios (47-50)
These are the questions live rounds actually use. Talk through access patterns first, then keys, then the limit you will hit.
47. Design a game leaderboard on DynamoDB.
Access patterns: record a score; get a player's score and rank; top 100 for a game; top 100 for a game this week.
Base table: pk = GAME#<id>#PLAYER#<id>, attributes score, updatedAt. Updates use SET score = :s with a condition score < :s so only a better score lands.
Top-N: a GSI with pk = GAME#<id>#WEEK#2026-41, sk = score (a Number). Query with ScanIndexForward: false, Limit: 100. Weekly boards become different partition keys automatically; TTL cleans old weeks.
Where it breaks: one popular game puts all writes on a single GSI partition key. Shard it (GAME#1#WEEK#41#SHARD#0..9), query each shard's top 100 in parallel, and merge in memory. Exact rank for an arbitrary player ("you are 48,213th") is something DynamoDB cannot answer cheaply - say so, and propose a Redis sorted set (ZREVRANK) fed by Streams for live rank, with DynamoDB as the durable store. That trade-off is the whole point of the question. Compare with the leaderboard at scale walkthrough.
48. Design the data model for an e-commerce order system.
Access patterns: get order by id; list a customer's orders newest first; get an order with its line items; list orders by status for fulfilment; look up an order by payment id.
pk sk notes
CUSTOMER#42 ORDER#2026-10-12#O9 summary copy: total, status
ORDER#O9 META full order, status, version
ORDER#O9 ITEM#001 sku, qty, price
GSI1: gsi1pk = STATUS#PAID#<shard>, gsi1sk = <createdAt>#O9 (sparse: removed once shipped)
GSI2: gsi2pk = PAYMENT#<paymentId> (lookup)
Placing an order is a TransactWriteItems: put ORDER#O9 / META with attribute_not_exists, put line items, and decrement inventory items with a condition stock >= :qty. If any inventory check fails, nothing is written.
Status changes use optimistic locking on version; the customer summary copy updates in the same transaction or via Streams - say which and why. An order with hundreds of lines exceeds the 100-item transaction limit and needs a staged workflow.
49. Design the storage for a chat application.
Access patterns: send a message; load the latest 50 messages in a conversation, then page backwards; list a user's conversations ordered by last activity; unread counts.
pk sk
CONV#c1 MSG#2026-10-13T10:00:00.123Z#m77 sender, body
USER#u1 CONV#c1 lastMessageAt, unread, title
GSI1 (user inbox): gsi1pk = USER#u1, gsi1sk = <lastMessageAt>#c1
- Latest messages: Query
CONV#c1, ScanIndexForward: false, Limit: 50, then page with LastEvaluatedKey.
- Inbox: Query GSI1 for the user, newest first.
- Sending updates each member's
USER#/CONV# item - fine for small groups via a transaction or a Streams fan-out; for large channels, do not fan out writes, compute the inbox on read instead.
Limits to raise: a busy channel is a hot CONV# partition, so bucket by day (CONV#c1#2026-10-13); attachments go to S3; retention uses TTL. Real-time delivery is a WebSocket concern - see the chat system design page.
50. Debug this: "Our DynamoDB bill tripled and some writes are throttled, but CloudWatch shows the table is at 30% of provisioned capacity."
- Hot partition - 30% of total capacity can still mean one key is far over the per-partition limit. Check Contributor Insights for the most throttled keys.
- GSI back-pressure - a GSI with less write capacity than the table, or a hot GSI key, throttles base-table writes. Check
WriteThrottleEvents per index and IndexWriteProvisionedThroughputExceeded throttle reasons.
- Bill growth - look for a new GSI projecting
ALL, items that grew (a list attribute appended forever, pushing every write to many WCUs), a new Scan on the request path or in a cron job, a filter-heavy Query reading far more than it returns, or a Streams consumer retrying a poison batch.
- Retries amplifying load - throttling triggers SDK retries, which add more requests.
Fixes map one-to-one: shard the hot key, size the GSI or go on-demand, cap item growth, replace Scans with indexed Queries, trim projections. The interviewer grades the order of investigation, so state each hypothesis before the metric that confirms it.
How to practice for a DynamoDB interview
- Write the access-pattern table before the keys, every time. For any practice prompt (a URL shortener, a booking system, a ride-share app), list the patterns with parameters first. In the interview, doing this out loud is half the score.
- Model the three scenarios above on paper without looking, then check which patterns needed a Scan or a filter. Every one that did is a key you should redesign.
- Run DynamoDB Local or a free-tier table and break things on purpose: write a reserved word without an alias, paginate with a filter and watch empty pages, store TTL in milliseconds and see nothing expire.
- Memorize the handful of numbers that drive designs: 400 KB items, 1 MB per Query/Scan page, 4 KB per read unit and 1 KB per write unit, 25/100 for batches, 100 items per transaction, 10 GB per LSI item collection, and roughly 3,000 reads / 1,000 writes per second per partition.
- Practise "what breaks at 100x?" For each design, name the hot key and how you would shard it.
- Know when to say "not DynamoDB". Ad-hoc analytics, full-text search and live ranking each have a better tool; naming it is a senior signal.
Keep going
#dynamodb #aws #nosql #systemdesign #interviews #career