DynamoDB Interview Questions · 2026

DynamoDB Interview Questions (2026): 51 Q&As

A backend candidate at a mid-size logistics company got asked to design a schema for tracking package scans, one write per scan event, tens of thousands of packages moving through a handful of regional hubs at once. He reached for a relational instinct: a packages table, a scans table, a foreign key between them. The interviewer's follow-up killed it in one sentence: what happens when every package scanned at the same hub in the same hour writes to the same partition. That's the DynamoDB interview in miniature. The service looks like a key-value store with a friendly API, and most of the actual difficulty lives in key design decisions you make before you write a single query. AWS's own developer guide frames the whole service around exactly this tradeoff, since a table's partition and sort keys determine how data physically spreads across storage nodes (AWS DynamoDB Developer Guide).

Here's a take that might be unpopular: most DynamoDB prep spends too much time on the CRUD API surface, PutItem, GetItem, UpdateItem, and not enough on capacity planning and access-pattern modeling, which is where interviewers actually differentiate candidates. You can memorize every API call in an afternoon. Understanding why a hot partition throttles your whole table even though your account-level throughput looks fine takes actual hands-on scar tissue, or at minimum a careful read of how DynamoDB's quotas work under the hood.

This page covers eight areas: the core data model (tables, items, attributes, primary keys), partition keys and secondary indexes, capacity modes and throughput, read consistency and Query versus Scan, writes and transactions, Streams and TTL and global tables, single-table design and hot partition mitigation, and day-two operations like DAX, backups, and throttling. Questions run from the kind asked in a phone screen through the kind that shows up in a staff-level system design loop.

51Questions
400 KBMax Item Size
20 per tableDefault GSI Limit
5 per tableDefault LSI Limit

Tables, items, and the primary key

Everything else on this page builds on how DynamoDB names and stores a single row. Get the vocabulary wrong here and the rest of the interview gets harder than it needs to be.

Easy questions

13

A table holds items, and each item is a collection of attributes, roughly a row and its columns in relational terms, except DynamoDB items in the same table don't need the same set of attributes. One item might have twelve fields, another item in the same table might have three. The only thing every item in a table must share is the primary key, since that's what DynamoDB uses to physically locate and retrieve it.

A simple primary key is just a partition key, one attribute, and it has to be unique across the whole table since it's the only thing identifying an item. A composite primary key adds a sort key on top of the partition key, and now uniqueness is enforced on the pair together, not the partition key alone. That means you can have many items sharing the same partition key as long as each has a different sort key.

json
// Composite key example: an orders table
{
  "customerId": "cust_9182",   // partition key
  "orderDate": "2026-03-14",   // sort key
  "total": 89.50
}
// Same customerId, different orderDate, both valid, both items

No, not in place. The primary key is immutable for a given item, since it determines physical placement. To change it you delete the old item and write a new one with the new key values. This trips people who come from relational databases, where updating a primary key column, while rarely a good idea, is at least mechanically possible.

Scalar types cover string, number, binary, boolean, and null. Document types cover list and map, which nest arbitrarily, a map inside a list inside a map is fine. Set types cover string set, number set, and binary set, which are unordered collections of unique values and behave differently from a list in that duplicates are silently rejected.

A GSI is an index with its own partition key and optional sort key, different from the base table's, letting you query the data by an access pattern the base table's primary key doesn't support. You reach for one whenever a real query in your application needs to filter or sort by an attribute that isn't part of the base table key. A default table supports up to 20 GSIs, per the current AWS service quotas (AWS DynamoDB quotas documentation).

On-demand is pay-per-request, no capacity planning, DynamoDB scales automatically to whatever the workload actually asks for and you're billed for exactly what you use. Provisioned mode has you specify a fixed number of read and write capacity units up front, and you're billed for what's provisioned, whether you use all of it or not. AWS positions on-demand as the default and recommended mode for most workloads today, reserving provisioned mode for teams with steady, predictable traffic they can forecast accurately (AWS DynamoDB capacity modes documentation).

One RCU covers one strongly consistent read per second of an item up to 4 KB, or two eventually consistent reads per second of the same size. Reading a 10 KB item strongly consistent rounds up to 3 RCUs, since DynamoDB charges in whole 4 KB increments, not fractional ones.

An eventually consistent read might return slightly stale data if it hits a replica that hasn't caught up with the latest write yet, though in practice that window is usually well under a second. A strongly consistent read always returns the most recent successful write, at the cost of higher latency and double the RCU cost of the equivalent eventually consistent read. GetItem, Query, and Scan all default to eventually consistent unless you explicitly request strong consistency.

Query requires a partition key and reads only the items in that one partition (optionally narrowed by a sort key condition), which makes it fast and cheap regardless of table size. Scan reads every item in the entire table (or index), then optionally filters after the fact, which means its cost scales with total table size, not with how many results you actually want back.

DynamoDB caps a single Query or Scan response at 1 MB of data read, and if there's more, the response includes a LastEvaluatedKey. You pass that value back in as ExclusiveStartKey on the next request to continue from exactly where the last one stopped. No LastEvaluatedKey in the response means you've reached the end.

It attaches a condition that DynamoDB evaluates atomically against the item's current state before applying the write. If the condition fails, the write is rejected and nothing changes, and the whole check-then-act happens as one atomic server-side operation, not two round trips from the client that could race against each other.

UpdateExpression lets you modify specific attributes on an item, SET a value, ADD to a number or a set, REMOVE an attribute entirely, without touching or even reading the rest of the item's data. PutItem replaces the entire item, so if two processes PutItem the same key around the same time, whichever writes last silently wins and discards the other's unrelated changes to different fields. UpdateExpression avoids that entirely by only touching the fields you name.

Streams captures a time-ordered log of item-level changes, inserts, updates, deletes, on a table, retained for 24 hours, that other services can read and react to. A common pattern: a Lambda function subscribed to a table's stream that fans out a notification, updates a search index, or replicates the change into an analytics pipeline, all without the original write path knowing or caring that any of that downstream work exists.

Medium questions

25

Because the primary key is the thing DynamoDB actually hashes to decide which physical partition an item lives on. Without a mandatory, consistently-typed key, the service has no deterministic way to route a read or write request to the right storage node in constant time. Every other attribute can vary freely between items precisely because the engine never needs them to locate anything, it only needs the key.

400 KB per item, and that figure includes both attribute names and attribute values, not just the values. A table with long, descriptive attribute names on every item burns more of that budget than the same data with terse names, which is one reason some teams shorten attribute names in high-volume tables even though it hurts readability.

Split the item across multiple items using a sort key range, storing the large payload as a separate item linked by the same partition key. If the oversized field is a blob rather than structured data, store it in S3 and keep only the S3 object key in DynamoDB. Compressing large text or JSON blobs before writing them also buys real headroom, since DynamoDB counts the stored bytes, not the pre-compression size.

It runs the partition key value through an internal hash function, and the output determines which physical partition stores the item. Two items with the same partition key always hash to the same partition, which is exactly why sort keys exist, to let you group related items together on one partition while still giving each a unique identity.

An LSI shares the base table's partition key but defines a different sort key, so it only lets you re-sort items within the same partition, not reroute them to a different one. A GSI can have a completely different partition key, effectively letting you query the table through a whole different access pattern. LSIs have to be created at table creation time and can't be added later; GSIs can be added or removed at any point in a table's life.

Projection controls which attributes get copied into the index alongside the key attributes. ALL copies every attribute from the base table item, KEYS_ONLY copies just the key attributes plus the index's own key, and INCLUDE lets you name a specific list of extra attributes. KEYS_ONLY keeps the index small and cheap to write to, which matters if the index only exists to support existence checks or a follow-up GetItem, since a smaller index costs less storage and less write capacity per update to the base table.

No, that's expected behavior, not a bug. GSIs are updated asynchronously after the base table write completes, so there's a small propagation window, usually well under a second, where a read against the index might not reflect the very latest write. GSIs also only support eventually consistent reads, there's no strongly consistent read option for a GSI the way there is for the base table or an LSI.

No. LSIs have to be defined at table creation and DynamoDB can never add one after the fact, precisely because they share the base table's partitions and adding one retroactively would require re-partitioning existing data in a way the service doesn't support. If you discover a new sort-order requirement after the table already exists and has real data, a GSI is the only option, since GSIs can be created, modified, and deleted on a live table at any time.

One WCU covers one write per second of an item up to 1 KB, a quarter the size threshold a read gets for the same unit cost. Writes are simply more expensive work for the storage engine, since every write has to be durably persisted and replicated across multiple availability zones before it's acknowledged, where an eventually consistent read can be served from a replica that's slightly behind.

Both modes default to 40,000 read units and 40,000 write units per table, per current AWS quotas, though provisioned mode also has an account-level ceiling of 80,000 combined read and write capacity units across all tables in a region, while on-demand mode has no such account-level cap (AWS DynamoDB quotas documentation). All of these are soft limits that AWS will raise on request, not hard architectural ceilings.

FilterExpression runs after DynamoDB has already read every item and consumed the capacity for reading it, it only controls what gets returned to the client, not what gets read and billed. A Scan with a filter that only matches 1% of a 10 million item table still reads, and charges for, all 10 million items. The fix almost always isn't a smarter filter, it's replacing the Scan with a Query against a key (or a GSI key) that actually matches the access pattern.

Yes, and the mechanics are identical: the filter applies after DynamoDB reads the items matching the key condition, so it doesn't reduce the RCUs charged for that Query. The difference is scope. A Query's key condition already narrows the read to one partition (and optionally a sort key range) before the filter ever runs, so the wasted read work is bounded by partition size, not table size, which is a much smaller blast radius than an unfiltered Scan.

Limit caps the number of items DynamoDB reads and evaluates per request, before any FilterExpression is applied, not the number of items returned after filtering. Set Limit to 10 with a filter that only matches 2 of them, and you get 2 items back, plus a LastEvaluatedKey telling you there's more to page through, not automatically 10 filtered results. Candidates who assume Limit and "number of results" are the same thing usually get bitten by this exact gap the first time they add a filter to a paginated query.

Keep a version attribute on the item, and on every update, include a ConditionExpression that the version attribute still equals the value you read, then increment it as part of the same write. If another process updated the item in between your read and your write, the condition fails and your write is rejected, telling you to re-read and retry instead of silently overwriting someone else's change.

json
// Read: { "orderId": "o_5521", "status": "pending", "version": 3 }
// UpdateItem with:
// ConditionExpression: "version = :expectedVersion"
// UpdateExpression: "SET status = :newStatus, version = version + :incr"
// If version isn't 3 anymore, the write fails

TransactWriteItems is all-or-nothing across up to 100 actions: if any single Put, Update, Delete, or ConditionCheck in the group fails, every action in the group rolls back and nothing is written. BatchWriteItem has no such guarantee, it's just a convenience wrapper that sends multiple independent Put or Delete requests in one network call, and any individual request in the batch can succeed or fail on its own without affecting the others.

There's no separate fee to turn transactions on, but each item involved in a TransactWriteItems or TransactGetItems call is billed at roughly double the normal capacity, since DynamoDB performs two underlying operations per item, one to prepare the transaction and one to commit it (AWS DynamoDB transactions documentation). A five-item transactional write costs roughly what ten individual writes would.

NEW_IMAGE gives you the item as it looked immediately after the change, OLD_IMAGE gives you the item as it looked immediately before, and NEW_AND_OLD_IMAGES gives you both in the same stream record. You'd want both, for instance, if a downstream consumer needs to detect exactly which fields changed rather than just knowing the item changed at all.

You mark an attribute as the TTL attribute and store a Unix epoch timestamp in it per item. A background process scans for expired items and deletes them, but that deletion isn't instant, AWS's general guidance is that expired items are typically removed within 48 hours of the TTL timestamp, not the second it passes. Anything that assumes TTL deletion happens the moment an item expires, like using it as a precise scheduling mechanism, will produce surprising bugs.

A global table replicates a table across multiple AWS Regions and keeps them in sync automatically, giving applications in each Region low-latency local reads and writes without you building and operating your own cross-Region replication pipeline. The classic case is a globally distributed user base where a user in Tokyo and a user in Frankfurt both want fast local reads against what functions as the same logical table.

Single-table design means modeling multiple, differently-shaped entity types, users, orders, product reviews, all in one physical DynamoDB table, using generic key attribute names like PK and SK whose actual meaning shifts depending on the entity type stored in that item. It's driven by the fact that DynamoDB has no cheap join operation, so a relational instinct of "one table per entity type, join at query time" turns every multi-entity read into multiple round trips. Colocating related entities that get queried together, under the same partition key, turns what would be a join into a single Query call.

json
// One table, two entity types sharing a partition
{ "PK": "CUSTOMER#9182", "SK": "PROFILE",         "name": "Priya R." }
{ "PK": "CUSTOMER#9182", "SK": "ORDER#2026-03-14", "total": 89.50 }
// A single Query on PK = CUSTOMER#9182 returns the profile and every order together

Say every write for a viral post's like-counter hits the same partition key, postId. Instead of one key, shard it into a fixed number of buckets, postId#0 through postId#9, and have each write pick a bucket at random. Reading the total requires summing across all 10 buckets instead of one GetItem, which is a real added cost, but it's a small, bounded cost compared to a partition throttling every write during a traffic spike.

json
// Instead of: { "PK": "POST#5521", "likes": 48213 }
// Shard into:
{ "PK": "POST#5521#0", "likes": 4801 }
{ "PK": "POST#5521#1", "likes": 4790 }
// ...through POST#5521#9, summed on read

DynamoDB Accelerator is an in-memory, write-through cache that sits in front of a table and serves reads in microseconds instead of the single-digit milliseconds a direct table read typically takes. It solves latency for read-heavy, repeat-key access patterns, a product page hit thousands of times a minute, not throughput problems, since DAX still has to write through to the table on every write and still respects the table's own capacity limits underneath it.

Not really. Writes still go straight through DAX to the underlying table and pay the table's normal write cost and latency, DAX doesn't buffer or batch writes to reduce that cost. Its value is almost entirely on the read side, and a workload that's mostly writes with few repeated reads of the same items won't see much benefit from adding it.

An on-demand backup is a full, manually-triggered snapshot you keep as long as you want, useful before a risky schema migration or a major deploy. Point-in-time recovery, once enabled, continuously backs up changes and lets you restore the table to any second within a retention window, useful for the "someone deleted the wrong items 6 hours ago" scenario where you don't have a snapshot from exactly that moment.

Scan usage, first. A Scan added anywhere in the request path, a background job, a debugging endpoint left in production, an admin report that used to run against a small table, gets dramatically more expensive as the table grows, since its cost scales with total table size regardless of how few rows actually match. I'd also check for a new GSI that's projecting ALL attributes when it only needs a couple, since that inflates the write cost of every base table write, not just the queries against the index.

Hard questions

13

An LSI shares storage with the base table's partition, since it's just an alternate sort order within the same physical partition. That means every LSI you add eats into the same 10 GB per-partition storage ceiling and the same per-partition throughput as the base table data, so DynamoDB caps the count tightly to protect that shared budget. A GSI is a genuinely separate index with its own partitions and its own provisioned or on-demand capacity, so it doesn't compete with the base table's per-partition limits the same way, which is why AWS can afford a much higher default quota there.

Base table primary key: customerId as partition key, orderDate (or a composite orderDate#orderId to keep sort keys unique when a customer places two orders the same day) as sort key. That directly serves the customer-facing query, a Query on a single partition key, sorted by date, no index needed. For support's lookup-by-order-ID pattern, add a GSI with orderId as its partition key. Support's queries are rarer and lower-volume than the customer-facing path, which is exactly the kind of access pattern a GSI exists to support without forcing you to compromise the base table's key design.

json
// Base table item
{
  "customerId": "cust_9182",           // PK
  "orderDate#orderId": "2026-03-14#o_5521", // SK
  "orderId": "o_5521",                 // also projected into a GSI
  "status": "shipped"
}
// GSI: partition key = orderId, sort key = none needed for a unique lookup

Capacity in provisioned mode is allocated per partition, not just per table. DynamoDB spreads a table's total provisioned throughput evenly across its partitions, so if traffic concentrates on one partition key, that single partition can exhaust its slice of capacity and start throttling requests even though the table's aggregate provisioned capacity, summed across all partitions, is nowhere near used up. Adaptive capacity mitigates this by shifting throughput toward hot partitions automatically, but it isn't instantaneous, and a sudden spike can still throttle before adaptive capacity catches up.

You hit a rate limit on decreases specifically, not increases. AWS gives a table 4 available capacity decreases at the start of each UTC day, refilling one more every hour up to that cap of 4, which works out to up to 27 total decreases across a full 24-hour day. Increases have no equivalent daily cap, which makes sense given the asymmetry: AWS wants to make it easy to scale up under load and slightly harder to thrash the billing down and up repeatedly.

Through an optional ClientRequestToken you can attach to the request. If the same token shows up again within a roughly 10-minute idempotency window, DynamoDB recognizes the retry and returns the original result without re-applying the writes a second time. Without a token, a client that times out waiting for a response has no safe way to know whether the transaction actually committed before retrying, and a naive retry risks double-applying the writes.

ACID transactions require a single coordinator that can see and lock every item involved at the same moment before committing, and cross-region replication in DynamoDB (including global tables) is asynchronous by design, built for availability and low write latency in each region rather than for a synchronous cross-region lock. Multi-Region Strong Consistency global tables are the newer exception to this, letting reads and writes get strong consistency guarantees across replicas in specific configurations, but the classic transaction APIs remain single-Region operations.

DynamoDB rejects the whole request with a validation error before it even attempts the transaction. Every action within a single TransactWriteItems call must target a distinct item, you can't have two actions, whether both ConditionChecks, or a ConditionCheck and an Update, pointed at the same primary key in one transaction. If your logic genuinely needs to both check and update the same item, fold the condition into the Update action's own ConditionExpression instead of using a separate ConditionCheck.

Up to two, for a single-Region table. Beyond that, AWS's own documentation warns you'll start seeing request throttling on the shard, and for global tables specifically, AWS recommends limiting it to a single reader per shard to avoid throttling entirely (AWS DynamoDB quotas documentation). Teams that need more than two consumers of the same change feed usually fan the stream out through Lambda or Kinesis Data Streams for DynamoDB instead of attaching multiple readers directly to the same shard.

Yes, a TTL-triggered deletion produces a stream record just like any other delete, but it's tagged with a specific identifier marking it as a system delete rather than a user-initiated one, so downstream consumers can distinguish the two if the distinction matters to them. This is genuinely useful: a stream consumer archiving deleted records to S3 for compliance can treat TTL expirations differently from an accidental application-level delete.

Honestly, the biggest one is that it front-loads almost all the design cost. You have to know every access pattern your application will ever need before you finalize the key schema, since changing a partition or sort key structure after the table is populated with real production data is a genuinely painful migration, usually a full rewrite into a new table. A relational schema tolerates "we'll add an index for that later" far more gracefully than a single-table DynamoDB design does. I'd still reach for single-table design on a well-understood domain with stable access patterns. I'd hesitate on a genuinely exploratory product where the queries are still shifting month to month.

A hot partition happens when one partition key value receives a disproportionate share of read or write traffic relative to the others, since DynamoDB's per-partition capacity is a hard physical ceiling regardless of the table's total provisioned or on-demand capacity. Adaptive capacity helps by shifting some throughput toward the hot partition automatically, but it's a mitigation, not a fix, and it can't rescue a design where every single write genuinely targets the same key, a global counter incremented by every user, say. The actual fix is usually key sharding: append a random or hashed suffix to the hot key so writes spread across several logical partitions, then fan out reads across all the suffixes and merge results in the application.

Because every new write lands on whatever the "latest" key range is, which DynamoDB has already routed to a specific, small set of physical partitions. All your write traffic concentrates on that recent range instead of spreading across the full key space the table's partitions are distributed over. A random or well-distributed hash-based key, or a sharded key like the post-likes example, spreads new writes across partitions from the start instead of concentrating them on whatever's most recent.

Standard-IA charges roughly 60% less per GB of storage than the default Standard class, but charges more per read and write request unit in exchange. It's the right call for tables that hold a lot of data but see relatively light, infrequent read and write traffic against it, an audit log you're required to retain but rarely query, for instance, and the wrong call for a table with heavy request volume, where the higher per-request cost would outweigh whatever you saved on storage.

How to prepare for a DynamoDB interview

Skip the flashcard approach to the API surface. Build one small thing that forces you to actually hit these tradeoffs: a chat app's message history modeled with a composite key, a like-counter you deliberately overload with concurrent writes until you watch it throttle, then fix with sharding. Turn on DynamoDB's request metrics in CloudWatch while you do it and watch ConsumedReadCapacityUnits and ThrottledRequests move in response to your own traffic. Reading about a hot partition is not the same as watching one happen to code you wrote an hour ago.

Across backend mock interviews run through LastRoundAI, the GSI-versus-LSI question and the Query-versus-Scan cost question come up in roughly the same conversation more often than not, since they're really the same underlying idea, know your access pattern before you pick a tool, asked two different ways. Candidates who can explain one but not the other usually memorized rather than understood the distinction. We don't track a precise percentage split on that pattern, it's an observation from reviewing sessions, not a measured statistic.

One correction to something I said earlier in this piece: I called Scan-with-filter a design mistake, full stop. That's not quite fair. A Scan is the right, and only, tool for a genuinely rare full-table operation, a one-time data migration script, an ad hoc analytics export you run once a quarter. The actual mistake is a Scan sitting in a hot request path that runs on every page load.

Get the reps in before the real thing

LastRoundAI's mock interview mode runs live system design and coding rounds with real-time follow-up questions, the same kind of pressure-test a hot-partition question actually creates in a real loop. The free plan includes 15 credits a month that reset monthly rather than piling up unused, and Starter is $19/mo if a handful of sessions isn't enough runway.

Once your answers hold up under a follow-up, Auto-Apply queues tailored applications for backend and infrastructure roles for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

Leave a Reply

Your email address will not be published. Required fields are marked *