DynamoDB Interview Guide: Data Modeling and Trade-offs
Quick Overview
This article explains how to discuss DynamoDB in system design interviews, including when to choose it, key design, capacity estimates, and index trade-offs. It also covers migrations, retries, idempotency, hot partitions, Global Tables, backups, and reliability practices.
DynamoDB Interview Guide: Data Modeling, Migrations, Reliability, and System Design Trade-offs
DynamoDB is a managed key-value/document database for operational workloads where the access patterns are known up front. In interviews, don't sell it as "NoSQL that scales." Show the interviewer how you size the workload, design keys, estimate capacity, handle retries, migrate safely, and recover when things fail.
A strong DynamoDB answer calls out:
- Access patterns, item sizes, RPS, read/write ratio, p95/p99 latency target, cardinality, consistency needs, retention, and cost ceiling.
- Base table PK/SK, GSIs/LSIs, projections, TTL, streams, backups, and alarms.
- Hot-key controls, write amplification, idempotency, transactions, pagination, and rollback.
1. DynamoDB in Interview System Design: When to Choose It and When Not To
Use a decision frame before picking DynamoDB:
| Question | DynamoDB-friendly answer | Warning sign |
|---|---|---|
| Queries | Known GetItem/Query patterns | Unknown filters, joins, arbitrary reporting |
| Item size | Usually small, below 400 KB | Large documents/blobs/manifests |
| Traffic | High-volume key access with distributed keys | One key gets most traffic |
| Latency | Single-digit millisecond service-side target for simple key access | p99 depends on retries, throttling, hot partitions, large items, transactions |
| Consistency | Per-item conditional writes; regional strong reads where needed | Strict cross-item relational constraints everywhere |
| Cost | Capacity units and indexes can be estimated | Many GSIs, scans, large items, global replication without budget |
Choose DynamoDB for order state, user settings, idempotency records, workflow state, metadata indexes, dedup indexes, counters, and precomputed views. Don't choose it for full-text search, high-cardinality analytical aggregations, full table scans, large documents over 400 KB, ad hoc filtering, unknown query patterns, or complex relational constraints.
Comparison:
| Store | Use it for | Caveat |
|---|---|---|
| DynamoDB | Durable key-value/document serving | Access-pattern-first; no joins; GSIs eventual |
| PostgreSQL | relational workflows, constraints, SQL | harder to scale writes horizontally |
| Redis | cache, rate limits, ephemeral counters | networked Redis is commonly sub-millisecond, not a durable system of record by default |
| DAX | DynamoDB API-compatible read-through cache for hot eventually consistent GetItem/Query | does not help writes, broad scans, ad hoc queries, rate-limit structures, or non-DynamoDB caching |
| Search index | text search, faceting, ranking | secondary, eventually consistent |
| S3/object storage | blobs, exports, backups, lake data | not a low-latency metadata query engine alone |
For global systems, bring up Global Tables: multi-region active-active replication, asynchronous cross-region propagation, regional failover, and last-writer-wins-style conflict resolution. Strong reads are regional. Cross-region reads can be stale. If conflicts matter, store application versions, request IDs, or state-transition rules.
2. DynamoDB Fundamentals Interviewers Expect You to Know
A table item is a map of attributes and must include the primary key. The max item size is 400 KB, including attribute names. Use vertical partitioning or object storage for large payloads.
Operations:
| Operation | What it does | Trap |
|---|---|---|
GetItem | fetch exact primary key | fastest path |
Query | requires partition key equality; optional sort key condition such as between, <, begins_with | key condition narrows reads |
Scan | reads table/index pages | expensive; acceptable for controlled backfills/admin jobs |
| Filter expression | filters after reading | does not reduce consumed read capacity |
| Pagination | Query/Scan return up to 1 MB before filters | use LastEvaluatedKey cursor |
Capacity arithmetic:
- 1 WCU = one write/sec up to 1 KB; round up by item size.
- 1 RCU = one strongly consistent read/sec up to 4 KB; two eventually consistent reads/sec up to 4 KB.
- Transactional reads/writes consume more capacity; model roughly double.
- GSIs add storage and write capacity for projected index entries.
BatchWriteItem: 25 writes, 16 MB request, no conditions; retry unprocessed items.BatchGetItem: 100 items, 16 MB response; retry unprocessed keys.- Transactions: up to 100 actions, 4 MB aggregate, same account/region, no two operations on the same item, cancellation reasons must be inspected.
Worked capacity example: 10,000 writes/sec of 2 KB order items = ceil(2/1) * 10,000 = 20,000 WCU on the base table. If two GSIs each project a 2 KB entry, add 20,000 + 20,000, for 60,000 WCU total. If the write is transactional, budget roughly 120,000 WCU. Global Tables multiply replicated write cost per replica region.
Data type caveats: numbers are sent as strings and have finite decimal precision. Maps/lists are convenient for document-style modeling, but they can grow past 400 KB and are hard to query. Empty string/binary values are allowed for non-key attributes; key attributes cannot be empty. Empty sets are not allowed. NULL is different from a missing attribute. Sparse indexes require the GSI key attributes to be absent when the item should not appear.
Expression pitfalls: use expression attribute names for reserved words, nested paths for maps, SET for assignment, ADD for atomic counters/sets`, and conditions for invariants.
Idempotent create in TypeScript:
try {
await doc.send(new PutCommand({
TableName: "App",
Item: {
PK: `IDEMP#${key}`, SK: "CREATE_ORDER",
requestHash, status: "COMPLETED",
response: { orderId }, expiresAt
},
ConditionExpression: "attribute_not_exists(PK)"
}));
return { orderId };
} catch (e: any) {
if (e.name !== "ConditionalCheckFailedException") throw e;
const old = await doc.send(new GetCommand({
TableName: "App", Key: { PK: `IDEMP#${key}`, SK: "CREATE_ORDER" }
}));
if (old.Item?.requestHash !== requestHash) throw new Error("conflicting idempotency key");
return old.Item.response;
}
LSIs use the same partition key with an alternate sort key. They must be created with the table, can be strongly read, and impose a 10 GB item collection limit per partition key. They are less common than GSIs because they are inflexible. GSIs have their own PK/SK, projections (KEYS_ONLY, INCLUDE, ALL), eventual consistency, backfill when added, hot partitions, and write amplification. In provisioned mode, a throttled GSI can throttle base-table writes.
PartiQL is a SQL-like API, not a relational engine. It does not remove key-design constraints.
3. Access-Pattern-First Data Modeling
Example requirements:
- Get order by ID.
- List user orders by time, newest first.
- List active restaurant orders without hot partitions.
- Lookup by payment ID.
- Reserve inventory atomically.
- Paginate all lists.
Table design:
Table: App
PK: S
SK: S
BillingMode: PAY_PER_REQUEST
GSIs:
GSI1: { PK: GSI1PK, SK: GSI1SK, Projection: INCLUDE(status,total,eta,userId) }
GSI2: { PK: GSI2PK, SK: GSI2SK, Projection: KEYS_ONLY }
TTL: expiresAt
Stream: NEW_AND_OLD_IMAGES
PITR: enabled
Encryption: KMS-managed
DeletionProtection: true
Items:
PK=ORDER#o123 SK=META
type=Order, userId=u9, restaurantId=r7, status=CREATED, version=1,
GSI1PK=USER#u9, GSI1SK=ORDER#2026-07-06T10:05:00.000Z#o123,
GSI2PK=PAYMENT#p555, GSI2SK=ORDER#o123
PK=ORDER#o123 SK=EVENT#2026-07-06T10:05:00.000Z#e01
type=OrderEvent, payloadRef=s3Key
PK=ORDER#o123 SK=DETAILS
deliveryAddress=..., notes=... # vertical partitioning
PK=REST#r7#BUCKET#20260706T10#SHARD#03 SK=ETA#2026-07-06T10:30:00.000Z#o123
type=ActiveRestaurantOrder, status=ACTIVE
Use UTC ISO-8601 fixed-width timestamps for lexical ordering; add IDs as tie-breakers. For reverse order, query ascending keys with inverted timestamps or use ScanIndexForward=false. Bucket and shard monotonically increasing time-series writes so all traffic does not land on the newest key.
Concrete reads:
// user orders, newest first, paginated
const out = await doc.send(new QueryCommand({
TableName: "App", IndexName: "GSI1",
KeyConditionExpression: "GSI1PK = :u AND begins_with(GSI1SK, :p)",
ExpressionAttributeValues: { ":u": "USER#u9", ":p": "ORDER#" },
Limit: 25, ScanIndexForward: false,
ExclusiveStartKey: cursor
}));
return { items: out.Items, cursor: out.LastEvaluatedKey };
For restaurant active queues, query all shards for the current buckets, merge by ETA, and keep per-shard cursors. Choose the shard count from peak writes for one restaurant divided by the safe per-partition budget, then add headroom. High cardinality alone is not enough; traffic distribution is what matters.
Uniqueness beyond primary key uses sentinel items in a transaction:
await doc.send(new TransactWriteCommand({ TransactItems: [
{ Put: { TableName:"App", Item:{PK:"EMAIL#a@x",SK:"UNIQUE",userId},
ConditionExpression:"attribute_not_exists(PK)" } },
{ Put: { TableName:"App", Item:{PK:`USER#${userId}`,SK:"META",email:"a@x"},
ConditionExpression:"attribute_not_exists(PK)" } }
]}));
Inventory reservation:
await doc.send(new UpdateCommand({
TableName: "App",
Key: { PK: "SKU#sku1", SK: "STOCK" },
UpdateExpression: "SET reserved = reserved + :n",
ConditionExpression: "available - reserved >= :n",
ExpressionAttributeValues: { ":n": 2 }
}));
Materialized views are duplicated data. If you update them in a transaction, reads are consistent but writes cost more. If Streams update them, views lag. Store the source version, reconcile missing projections, and rebuild with export/backfill plus stream catch-up.
4. Relational-to-DynamoDB Migration Design
Interview-ready migration plan:
- Inventory SQL queries, cardinality, item sizes, RPS, joins, consistency needs, and which reports remain in OLAP/search.
- Design versioned DynamoDB items and dual readers that tolerate old/new attributes.
- Snapshot with isolation and record the source LSN/binlog position.
- Transform via DMS, export to S3, Glue/EMR, or custom workers.
- Backfill with parallel scan/range workers,
BatchWriteItem, checkpoint items, adaptive rate limits, exponential backoff, and unprocessed-item retries. - Apply CDC after the snapshot position, ordered per source key. Deduplicate snapshot and CDC writes by source version/LSN.
- Validate deterministic per-entity checksums, query-result diffs, lag thresholds, mismatch budgets, and business invariants. Quarantine mismatches.
- Shadow reads, canary, ramp traffic.
- Roll back only if the old system still receives writes or reverse CDC/dual-write reconciliation is proven.
Deletes need an explicit design: soft-delete markers for APIs, tombstones for CDC ordering, deletion of duplicate index items, TTL for eventual cleanup, and referential cleanup workers. TTL is asynchronous, may take days, can emit stream records, and is not a scheduler.
Dual writes are where migrations usually get messy. Prefer one writer plus CDC/outbox. If dual writing cannot be avoided, make writes idempotent, record operation IDs, reconcile partial failures, define ordering, and document which store wins during failover.
Backfills can get expensive fast. Shape the write rate, temporarily use on-demand or provisioned warm capacity, account for GSI backfill, monitor throttling, and avoid starving production.
5. High-Scale Metadata Stores: Object Storage and Deduplication Use Cases
DynamoDB stores metadata, not object bytes. Keep manifests small or split them:
PK=OBJECT#obj1 SK=VERSION#000042 state=FINAL, manifestId=m1
PK=MANIFEST#m1 SK=PART#000000 chunk hashes 0..999
PK=MANIFEST#m1 SK=PART#000001 chunk hashes 1000..1999
PK=TENANT#t1#HASH#h SK=CHUNK refCount=7, state=LIVE
If a manifest may exceed 400 KB, store the manifest in object storage and keep only pointer, checksum, size, and page count in DynamoDB.
Upload lifecycle: PENDING -> FINALIZING -> FINAL or ABORTED; commit is idempotent by upload ID/request hash. Overwrites create new versions. Deletes create delete markers before garbage collection. Reads define whether they see only the latest committed version.
Safe reference counting protocol: finalize upload in a transaction that creates the object version, links chunks with idempotency keys, and increments counters conditionally. Unlink writes tombstones and decrements once. GC only deletes chunks with refCount=0, no active finalization lease, and grace period elapsed. Workers use leases, retry counts, and crash recovery; object-storage delete failures leave retryable tombstones.
Dedup requires cryptographic hashes, tenant privacy rules, collision handling by verifying size/secondary hash or byte comparison, and encryption implications. Cross-tenant dedup can leak existence unless explicitly allowed. Hot hashes use routing records:
PK=HASH#h SK=ROUTE shardCount=16
PK=HASH#h#S#07 SK=CHUNK refDelta=...
Reads fetch the route, then fan out or read the selected shard. Writes choose the shard by object ID to avoid double counting.
6. Capacity, Partitioning, Hot Keys, and SLA Protection
A common single-physical-partition design ceiling is about 1,000 WCU or 3,000 strongly consistent RCU. Adaptive capacity and split-for-heat help, but not for a single hot item, bad LSI choices, or monotonic latest-bucket writes. On-demand has quotas, maximum throughput settings, previous-peak behavior, and may need warm throughput/pre-warming for launches. Provisioned mode has auto-scaling lag but can use reserved capacity.
SLA control loop:
- API layer: deadlines, request budgets, idempotency keys.
- Queue layer: bounded queues and backpressure for non-critical work.
- DynamoDB client: SDK standard/adaptive retries, exponential backoff with jitter.
- Capacity guard: shed low-priority writes when throttling or p99 crosses threshold.
- Cache: Redis/DAX/in-process for hot read-only data with invalidation.
- Circuit breakers: stop retry storms from degraded upstreams.
Retryable: ProvisionedThroughputExceededException, ThrottlingException, transient 5xx, unprocessed batch items. Usually non-retryable without changing input: ConditionalCheckFailedException, validation errors. TransactionCanceledException requires checking cancellation reasons; some causes are conditional conflicts, others are throttling.
Observability: CloudWatch consumed capacity, throttled requests, latency, system errors, account/table quotas, GSI metrics, stream iterator age, Lambda failures, DLQ depth, PITR/backups, and Contributor Insights for hot keys. Log request IDs, consumed capacity on sampled calls, key prefixes, throttling reason codes, and page counts.
Cost model includes table storage, GSI storage, reads/writes, transactions, streams, Lambda consumers, PITR, backups, exports, restores, global-table replicated writes, data transfer, KMS, and every denormalized item.
7. Consistency, Transactions, Idempotency, and Async Workflows
Strongly consistent reads apply to base tables and LSIs in a region. GSIs and Global Tables are eventually consistent. Use conditions for correctness, not "read then write."
Optimistic locking:
try {
await doc.send(new UpdateCommand({
TableName: "App",
Key: { PK: "ORDER#o123", SK: "META" },
UpdateExpression: "SET #s=:s, version=version+:one",
ConditionExpression: "version=:expected",
ExpressionAttributeNames: { "#s": "status" },
ExpressionAttributeValues: { ":s": "PICKED_UP", ":one": 1, ":expected": 7 }
}));
} catch (e:any) {
if (e.name === "ConditionalCheckFailedException") {/* re-read and decide */}
else throw e;
}
Exactly-once is not a DynamoDB feature. Build idempotent consumers. Store eventId, source version, status, retry count, and poison reason. Valid state transitions are conditional: QUEUED -> RUNNING -> SUCCEEDED/FAILED. Workers claim jobs with a lease:
Condition: status='QUEUED' OR leaseUntil < now
SET status='RUNNING', workerId=:w, leaseUntil=:t, attempts=attempts+1
Terminal states are idempotent; duplicate workers cannot overwrite them.
Outbox keys should avoid hot dates:
PK=OUTBOX#SHARD#07 SK=2026-07-06T10:05:00Z#eventId
state=READY, leaseUntil, attempts, publishKey, payloadRef
Choose Streams when table changes themselves are events and 24-hour replay is enough. Choose an explicit outbox when you need publish intent, payload shaping, leases, retries, cleanup, and no double-publish. Use queues/event buses for buffering and fan-out.
8. Data Ingestion, Streams, CDC, and Analytics Pipelines
DynamoDB Streams retain records for 24 hours. They preserve order for modifications to the same item key, not global order. Parallel consumers can reorder across keys, so design per-entity idempotency.
Lambda consumers need partial batch response, bisect-on-error, DLQ/on-failure destination, idempotent downstream writes, checkpoints, and stream-lag alarms.
Derived systems need snapshot plus catch-up: export table/PITR snapshot to S3, build search/lakehouse output, then apply stream records after the export point, validate counts/checksums/query diffs, and switch readers. Analytics options: Export to S3, Glue/Athena-style querying, Kinesis/Firehose pipelines, OpenSearch indexing, Redshift/lakehouse sinks.
Scans are fine for controlled backfills, exports, small admin tables, or offline jobs. Use parallel scan, pagination, rate limits, consumed-capacity monitoring, and remember scans lack snapshot isolation unless using export/PITR.
9. Common DynamoDB Interview Pitfalls and How to Answer Follow-ups
| Pitfall | Fix |
|---|---|
| "Filter by status/date" without an index | make status/date part of PK/SK or GSI |
| forgetting pagination | return cursor from LastEvaluatedKey |
| assuming GSI strong consistency | say GSIs are eventual |
| ignoring 400 KB | vertical partition or object storage |
| single global counter/key | use sharded counters or per-entity counters |
| overusing GSIs | choose: GSI for critical alternate query; duplicate lookup item for unique/direct lookup; separate table for different lifecycle/owner; search index for text/facets; OLAP for aggregations |
| missing null/absent distinction | sparse index membership needs absent valid key attrs |
| treating TTL as exact | use workflow scheduler/queue for exact timing |
| no security | IAM least privilege, KMS encryption, VPC endpoints, audit logs, tenant key prefixes, per-tenant quotas, PII minimization, deletion workflows |
| no operations | alarms, PITR, restore tests, deletion protection, blue/green tables, GSI backfill monitoring, runbooks |
Final interview checklist:
- State access patterns with RPS, item size, latency, consistency, retention.
- Draw PK/SK and GSIs with projections.
- Show one write path, read path, pagination, and capacity math.
- Explain hot-key mitigation and retry/backpressure.
- Use conditional writes for invariants and idempotency.
- Separate operational serving from search/analytics/blob storage.
- Cover migration: snapshot, CDC, validation, cutover, rollback.
- Cover reliability/security: PITR, backups, restores, Global Tables if needed, IAM, encryption, tenant isolation, observability.
Related Articles
From Non-CS Major to Software Engineer: A Practical Guide to Cracking the Technical Interview
Prepare for technical interviews with a practical guide to DSA practice, live coding, mock interviews, communication, and interview mindset.
From Non-CS Major to Software Engineer: A Practical Guide to Cracking the Technical Interview
Prepare for technical interviews with a practical guide to DSA practice, live coding, mock interviews, communication, and interview mindset.
Design WhatsApp: the presence and receipt problems most candidates ignore
Design WhatsApp-style chat with WebSockets, offline inboxes, Kafka partitions, presence TTLs, receipts, and reliable delivery.
I Pinned Our Autoscaler for a Month to See What Would Break. Nothing Did.
Learn when Kubernetes autoscaling helps, when CPU-based HPA wastes money, and how capacity planning can cut cloud costs safely.
Comments (0)