Design with DynamoDB — Staff-Level Technology Guide
The 60-Second Pitch
Amazon DynamoDB is a fully managed NoSQL key-value and document database that delivers single-digit millisecond latency at any scale. No servers to provision, no disks to manage, no replication to configure, no compaction to tune. You define a table, choose a partition key, and DynamoDB handles the rest — automatic partitioning, three-way replication within an AWS region, continuous backups, and encryption at rest. In system design interviews, DynamoDB is the answer when you want Cassandra-like horizontal scaling without the operational overhead of managing a cluster.
The Staff-level insight: DynamoDB is not a database you model entities in — it is a database you model access patterns in. The entire game is single-table design: cramming multiple entity types into one table, using carefully constructed partition keys and sort keys so that every query is a single, efficient request. If you design DynamoDB tables the way you design PostgreSQL tables (one table per entity, multiple tables joined at read time), you will get a system that is slow, expensive, and impossible to optimize. The Staff move is to present your access patterns first, then derive the table schema — not the other way around.
Architecture & Internals
Fully Managed Partitioning
DynamoDB automatically partitions your data across an internal fleet of storage nodes. You never see these partitions, you never configure them, and you never manage them. This is the fundamental architectural difference from Cassandra — where you choose replication factors, compaction strategies, and node counts, DynamoDB abstracts all of that away. You interact only with tables, items, and indexes.
Internally, each partition is a B-tree stored on SSD, replicated three ways across Availability Zones within a region. A partition holds up to 10GB of data and serves up to 3,000 Read Capacity Units (RCUs) or 1,000 Write Capacity Units (WCUs) per second. When a partition exceeds either limit, DynamoDB automatically splits it — transparently, without downtime, without any action from you. This auto-splitting is the mechanism that enables "infinite" scaling, but it also means partition key design directly determines performance characteristics.
The partition key is hashed (using an internal hash function) to determine which partition stores the item. All items with the same partition key are stored in the same partition, sorted by the sort key. This is identical to Cassandra's partition key / clustering key model, but with one crucial difference: you cannot choose the hashing algorithm, you cannot inspect partition boundaries, and you cannot manually rebalance. DynamoDB manages the ring for you — the tradeoff is less control for zero operational burden.
Request Routing
Every DynamoDB API call goes through a request router — a stateless fleet of servers that authenticates the request, determines which storage partition holds the data, and forwards the request. The router uses a metadata service to map partition keys to storage nodes. This metadata is cached aggressively, so routing adds sub-millisecond overhead. For strongly consistent reads, the router always contacts the leader replica of the partition's Paxos group. For eventually consistent reads (the default), the router can contact any of the three replicas, reducing latency and improving availability.
The router is also responsible for admission control. If a partition receives more requests than its allocated throughput, the router returns a ProvisionedThroughputExceededException (throttling). The AWS SDK automatically retries with exponential backoff (up to a configurable maximum), but sustained throttling means your partition key design needs attention — not your capacity settings.
Understanding the router architecture matters for interviews because it explains why DynamoDB latency is so consistent: the router is stateless and horizontally scaled, the metadata lookup is cached, and the storage node performs a single B-tree lookup. There is no query planning, no lock acquisition, no transaction log consultation. The path from API call to data is deterministic and short — which is why DynamoDB guarantees single-digit millisecond latency at the p99 level.
Storage & Replication
Each item is stored on three replicas across three Availability Zones. Writes are acknowledged after two of three replicas confirm (similar to Cassandra QUORUM), giving you durability against a single AZ failure. The third replica is updated asynchronously and typically converges within milliseconds. This is why DynamoDB offers two read consistency modes:
- Eventually consistent reads (default, 0.5 RCU per 4KB) — may return a slightly stale value if the read hits the lagging replica. In practice, staleness is rare (sub-100ms convergence), but your application must tolerate it.
- Strongly consistent reads (1 RCU per 4KB) — always returns the latest committed value by reading from the leader replica. Costs 2x the RCU of eventually consistent reads and has slightly higher latency.
Data Modeling — Single-Table Design
The Single-Table Philosophy
Single-table design is the most controversial and most powerful DynamoDB concept. Instead of creating separate tables for Users, Orders, Products, and Reviews (the relational approach), you store all entity types in one table using carefully constructed partition keys and sort keys that encode the entity type and relationships.
Why one table? DynamoDB charges per request and per table. Multiple tables mean multiple queries to assemble a response, and DynamoDB has no JOIN operation. If you need a user's profile and their last 10 orders in one API call, you need both in the same table so a single Query operation can retrieve them. This is the access-pattern-first principle: your table schema is derived from your UI screens and API responses, not from your entity-relationship diagram.
The schema pattern:
| Partition Key (PK) | Sort Key (SK) | Attributes |
|---|---|---|
USER#alice | PROFILE | name, email, created_at |
USER#alice | ORDER#2024-01-15#ord-123 | total, status, items |
USER#alice | ORDER#2024-01-20#ord-456 | total, status, items |
USER#alice | REVIEW#prod-789 | rating, text, date |
PRODUCT#prod-789 | METADATA | name, price, category |
PRODUCT#prod-789 | REVIEW#alice | rating, text, date |
ORDER#ord-123 | METADATA | user_id, total, status |
ORDER#ord-123 | ITEM#prod-789 | quantity, price |
This single table supports all these queries with a single Query operation each:
- Get user profile:
PK = USER#alice, SK = PROFILE - Get user's orders:
PK = USER#alice, SK begins_with ORDER# - Get user's reviews:
PK = USER#alice, SK begins_with REVIEW# - Get product reviews:
PK = PRODUCT#prod-789, SK begins_with REVIEW# - Get order details with items:
PK = ORDER#ord-123
Partition Key & Sort Key Design
The primary key in DynamoDB has two forms:
- Simple primary key — partition key only. Each item is uniquely identified by its partition key. Use when you only need key-value lookups (e.g., session store, feature flags).
- Composite primary key — partition key + sort key. Multiple items can share a partition key, distinguished by their sort key. Use for all entity-relationship modeling (which is most applications).
Partition key design rules:
- High cardinality — the partition key should have many distinct values (user IDs, device IDs, order IDs). Low-cardinality keys (status codes, country codes, boolean flags) create hot partitions.
- Uniform distribution — traffic should be spread evenly across partition key values. If 80% of requests target 1% of partition keys, those partitions will throttle regardless of provisioned capacity.
- Query identity — the partition key must be known at query time. You cannot scan for it efficiently. Every query starts with
PK = <exact value>.
Sort key design patterns:
- Hierarchical prefixes —
ORDER#2024-01-15#ord-123enables both exact lookup and range queries (begins_with ORDER#2024-01). - Composite sort keys —
STATUS#active#2024-01-15enables filtering by status and date range in a single query. The order of components determines the query flexibility: you can query any prefix, but not skip a middle component. - Version tracking —
v0(latest),v1,v2... for item versioning withSK begins_with vto retrieve history.
Overloaded Keys & Entity Packing
Single-table design requires "overloading" the partition key and sort key — using generic attribute names (PK, SK) that hold different semantic values depending on the entity type. The PK for a user item is USER#alice, for an order item it is ORDER#ord-123, for a product item it is PRODUCT#prod-789. A type attribute is typically added for application-level filtering.
This pattern makes the schema opaque when viewed in the DynamoDB console — you see a wall of PK/SK values that are meaningless without documentation. This is the cost of single-table design. Mitigate it with: an access pattern document that maps every query to its PK/SK pattern, a shared constants module for key prefixes, and integration tests that verify each access pattern.
The access pattern document is non-negotiable. It should look like this:
| # | Access Pattern | Operation | PK | SK | GSI? |
|---|---|---|---|---|---|
| 1 | Get user by ID | GetItem | USER#{id} | PROFILE | — |
| 2 | List user orders | Query | USER#{id} | begins_with ORDER# | — |
| 3 | Get order details | Query | ORDER#{id} | — | — |
| 4 | Get user by email | Query | {email} | — | GSI1 |
| 5 | List orders by status | Query | STATUS#{status} | {created_at} | GSI2 |
This document is your source of truth. Every new access pattern must be added here before schema changes are made. If a pattern cannot be served by the existing PK/SK/GSI combination, you either add a GSI (limited to 20) or accept multiple round-trips.
GSI vs LSI — Secondary Access Patterns
Global Secondary Indexes (GSI)
A GSI is a fully independent copy of the table with a different partition key and sort key. When you write to the base table, DynamoDB asynchronously replicates the item to each GSI that projects it. GSIs are the mechanism for answering queries that the base table's primary key cannot serve.
Key characteristics:
- Eventually consistent only — GSIs are always eventually consistent, even if you request strongly consistent reads on the base table. The replication lag is typically <100ms but can spike under heavy write loads.
- Separate throughput — each GSI has its own provisioned capacity (or shares the table's on-demand capacity). If a GSI throttles, it backpressures the base table writes (more on this in Failure Modes).
- Sparse indexes — items are only projected to a GSI if they have the GSI's partition key attribute. Omit the attribute to exclude items from the index. This is a powerful cost optimization: a GSI on
emailonly includes items that have anemailattribute, not every item in the table. - Up to 20 GSIs per table — a hard limit. Each GSI adds write amplification and storage cost, so design them carefully.
Projection types:
| Type | What's Projected | Storage Cost | When to Use |
|---|---|---|---|
KEYS_ONLY | Base table PK + SK + GSI PK + SK | Minimal | When you only need item identifiers |
INCLUDE | Keys + specified attributes | Moderate | When you need a subset of attributes |
ALL | All attributes from the base item | Full copy | When the GSI serves as a complete alternate view |
Local Secondary Indexes (LSI)
An LSI uses the same partition key as the base table but a different sort key. It must be created at table creation time (cannot be added later) and shares the base table's throughput capacity. LSIs support strongly consistent reads — the only DynamoDB index type that does.
The critical constraint: LSIs enforce a 10GB per partition key limit on the base table. Without an LSI, a partition key can hold unlimited items (bounded only by per-item and per-partition throughput). With an LSI, the combined data for a partition key across the base table and all LSIs cannot exceed 10GB. This limit has bitten many teams at scale and is the primary reason most DynamoDB experts recommend GSIs over LSIs.
When to use LSIs: Only when you need strongly consistent reads on an alternate sort key AND your partition sizes will never approach 10GB. In practice, this is rare. Default to GSIs.
GSI Design Patterns
Inverted index: Swap PK and SK. If the base table has PK=USER#id, SK=ORDER#date, a GSI with PK=ORDER#id, SK=USER#id lets you look up which user placed an order. This is the most common GSI pattern.
Sparse index for status filtering: Add a GSI_PK attribute only to items in a specific state. For example, set active_order_pk = ORDER#status#active only on active orders. The GSI only contains active orders — no need to filter through millions of completed orders. When an order completes, remove the active_order_pk attribute, and it disappears from the GSI automatically.
Overloaded GSI: Use generic GSI1PK and GSI1SK attribute names that hold different values depending on entity type. User items set GSI1PK = EMAIL#alice@example.com, Order items set GSI1PK = STATUS#active. One GSI serves multiple access patterns across entity types. This maximizes the utility of each GSI slot (remember, you only get 20).
Capacity Modes
On-Demand vs. Provisioned
DynamoDB offers two capacity modes, and choosing the wrong one for your workload is one of the most expensive mistakes in AWS.
On-demand mode: Pay per request. No capacity planning, no throttling (up to account limits), no provisioned numbers to guess. DynamoDB instantly accommodates whatever traffic arrives. The cost is ~5x higher per request than provisioned mode at steady-state utilization. On-demand is ideal for: unpredictable traffic (new applications, spiky workloads), development/staging environments, and tables where the cost of throttling exceeds the cost of over-paying per request.
Provisioned mode: You specify RCUs and WCUs. Reads and writes beyond your provisioned capacity are throttled. The cost per request is much lower than on-demand, but you must predict your traffic and risk throttling if you under-provision. Provisioned mode is ideal for: predictable, steady-state workloads where you can forecast capacity accurately.
Auto-scaling bridges the gap: you set a target utilization (e.g., 70%), min/max capacity, and DynamoDB adjusts provisioned capacity based on CloudWatch metrics. Auto-scaling reacts in minutes, not seconds — it will not absorb sudden spikes. For traffic patterns with predictable peaks (daily cycles, weekly patterns), use scheduled scaling to pre-provision capacity before the spike.
Burst Capacity & Adaptive Capacity
DynamoDB reserves a portion of unused partition throughput as "burst capacity" — if a partition has been underutilized, it can temporarily exceed its per-partition limit (up to 300 seconds of accumulated unused capacity). This smooths out short spikes but is not a substitute for proper capacity planning.
Adaptive capacity (enabled by default) redistributes unused throughput from cold partitions to hot ones. If partition A is using 200 WCU and partition B is using 800 WCU out of a table-level 1,000 WCU, adaptive capacity allows partition B to borrow from partition A. This significantly reduces throttling for workloads with moderate skew, but does not help when a single partition key truly exceeds 1,000 WCU sustained.
DynamoDB Streams & Change Data Capture
How Streams Work
DynamoDB Streams captures a time-ordered log of every item-level change (insert, update, delete) in a table. Each stream record contains the item's primary key and, depending on the stream view type, the old values, new values, or both. Stream records are retained for 24 hours and are guaranteed to arrive in order per partition key.
Stream view types:
| View Type | Contains | Use Case |
|---|---|---|
KEYS_ONLY | PK + SK only | Trigger downstream lookups |
NEW_IMAGE | Item after change | Replicate to another store |
OLD_IMAGE | Item before change | Audit trails, undo |
NEW_AND_OLD_IMAGES | Both | Differential processing, CDC |
Lambda Triggers & Event-Driven Architecture
The most common integration is DynamoDB Streams → Lambda function. When an item changes, the stream record triggers a Lambda function that processes the change — replicating to Elasticsearch for search, publishing to SNS for notifications, updating a cache, or maintaining a materialized view in another table.
Operational considerations:
- Batch size — Lambda receives stream records in batches (up to 1,000). Process the entire batch atomically or implement per-record error handling with a dead-letter queue.
- Iterator age — the
IteratorAgeCloudWatch metric shows how far behind the stream processor is. If it grows, your Lambda is processing slower than the write rate — scale up concurrency or optimize processing logic. - Exactly-once is your responsibility — DynamoDB Streams guarantees at-least-once delivery. If your Lambda fails mid-batch and retries, it will re-process records. Make your handler idempotent.
- Fan-out limit — a DynamoDB stream can have at most 2 concurrent consumers. If you need more, pipe the stream through Kinesis Data Streams (up to 5 consumers) or EventBridge Pipes.
Advanced Features
DynamoDB Transactions
TransactWriteItems and TransactGetItems provide ACID transactions across up to 100 items in one or more tables within the same region. Transactions use optimistic concurrency control — they succeed if no conflicting writes occurred during the transaction, and fail with TransactionCanceledException otherwise.
TransactWriteItems:
- Put: {PK: ORDER#ord-789, SK: METADATA, status: confirmed}
- Update: {PK: USER#alice, SK: PROFILE, SET order_count = order_count + 1}
- Update: {PK: PRODUCT#prod-123, SK: METADATA, SET stock = stock - 1}
- ConditionCheck: {PK: PRODUCT#prod-123, SK: METADATA, stock > 0}
All four operations succeed or all four fail — no partial writes. Transactions cost 2x the WCU of individual operations (each item is written twice: once for the prepare phase, once for the commit). Use transactions for: order placement (deduct inventory + create order), account transfers (debit + credit), and any multi-item operation where partial failure would corrupt state.
Transaction limits to know: 100 items max per transaction, 4MB total request size, items must be in the same region, and no two operations in the same transaction can target the same item.
The Staff-level nuance: DynamoDB transactions are not PostgreSQL transactions. They do not support isolation levels, read-modify-write in a single transaction, or rollback to savepoints. They are atomic batch writes with optional condition checks. If your application needs multi-step transactions with read-dependent logic (read balance → check sufficient → debit → credit), you must use ConditionCheck within TransactWriteItems or implement saga patterns. If your transaction logic requires JOINs, subqueries, or cross-entity reads within the transaction boundary, DynamoDB is the wrong tool — use PostgreSQL for that data.
TTL (Time To Live)
DynamoDB TTL automatically deletes items whose TTL attribute (a Unix epoch timestamp) has passed. Deletions are eventually consistent — items may persist for up to 48 hours after expiry, but they are filtered from query results immediately. TTL deletes do not consume WCU, making this the most cost-effective way to expire data.
Common TTL patterns:
- Session management — set TTL to
login_time + session_duration. Sessions expire automatically. - Cache entries — set TTL to
cache_time + freshness_window. Stale cache entries clean themselves up. - Temporary data — invitations, OTPs, password reset tokens with natural expiry.
DAX (DynamoDB Accelerator)
DAX is an in-memory write-through cache for DynamoDB that reduces read latency from single-digit milliseconds to microseconds. DAX sits between your application and DynamoDB — you change the SDK endpoint, and reads are served from the cache if the item exists. Cache misses fall through to DynamoDB transparently.
When DAX makes sense: Read-heavy workloads (>10:1 read-to-write ratio) where microsecond latency matters. Gaming leaderboards, session stores, product catalogs with hot items. DAX costs ~$0.27/hour per node (minimum 3 nodes for HA), so it must save enough RCU cost to justify the infrastructure spend.
When DAX is wrong: Write-heavy workloads (DAX adds a write-through hop), strongly consistent read requirements (DAX only supports eventually consistent reads), or workloads where a simple ElastiCache/Redis layer already provides the caching.
DAX vs ElastiCache/Redis: DAX is tightly coupled to DynamoDB — it understands DynamoDB's data model and automatically handles cache invalidation on writes. Redis requires application-layer cache management (write-through or write-behind logic). If your only cache use case is accelerating DynamoDB reads, DAX is simpler. If you also cache computed values, external API responses, or session data, a general-purpose Redis cache is more versatile. In interviews, propose DAX for DynamoDB-specific caching and Redis for cross-service caching — they serve different architectural purposes.
Failure Modes & Recovery
1. Hot Partition Throttling
Symptoms: ProvisionedThroughputExceededException on specific operations, CloudWatch showing throttled requests concentrated on specific partition keys, overall table utilization well below provisioned capacity.
Root cause: A small number of partition keys receive disproportionate traffic. Classic examples: using date as partition key (today's date gets all writes), using status as partition key (most items are active), a viral product/user that concentrates reads.
Fix: Redesign the partition key to distribute traffic. Add a write-sharding suffix: PK = USER#alice#shard-{hash(request_id) % 10}. This spreads writes across 10 partitions but requires fan-out on reads (query all 10 shards and merge). For read hotspots, add DAX or an application-level cache. Adaptive capacity helps with moderate skew but not extreme hotspots.
2. GSI Backpressure & Write Throttling
Symptoms: Base table writes throttled even though base table has capacity, GSI WriteThrottleEvents metric elevated, increasing replication lag between base table and GSI.
Root cause: Every base table write that includes a GSI key attribute triggers an asynchronous write to the GSI. If the GSI has insufficient write capacity (provisioned mode) or hits per-partition throughput limits, the backpressure propagates to the base table. DynamoDB throttles base table writes to prevent the GSI from falling irrecoverably behind.
Fix: Ensure GSI provisioned capacity matches the base table write rate (GSI writes = base table writes for that GSI). Use on-demand mode for GSIs with unpredictable write patterns. Remove unused GSIs — each one multiplies write costs and throttling risk. Consider sparse indexes: only project items that need the GSI by conditionally including the GSI key attribute.
3. Item Size Limit (400KB)
Symptoms: ValidationException: Item size has exceeded the maximum allowed size on PutItem/UpdateItem.
Root cause: Each DynamoDB item has a hard 400KB size limit, including attribute names and values. Items with large JSON blobs, base64-encoded files, or deeply nested maps can exceed this limit.
Fix: Store large values in S3 and keep a reference (S3 URI) in DynamoDB. Compress large attributes before storing. Split large items into multiple items with the same PK and sequential SK (CHUNK#1, CHUNK#2) and reassemble on read. For documents >400KB, DynamoDB is the wrong storage layer.
4. Scan Anti-Pattern at Scale
Symptoms: High RCU consumption, read latency spikes during analytics queries, ConsumedCapacity far exceeding expected values.
Root cause: Using Scan operations to iterate over the entire table for analytics, reporting, or data export. A Scan reads every item in the table, consuming 1 RCU per 4KB of scanned data regardless of whether items match your filter expression. A 100GB table scan costs 25 million RCUs.
Fix: Never Scan for application queries — design GSIs so every query is a single-partition Query operation. For analytics, export to S3 via DynamoDB Export (zero RCU cost, reads from snapshots) and query with Athena. For real-time analytics on specific dimensions, create a GSI with the analytics dimension as the partition key.
When to Use vs. Alternatives
| Dimension | DynamoDB | PostgreSQL | Cassandra | MongoDB |
|---|---|---|---|---|
| Best for | Serverless, AWS-native, predictable access patterns | Complex queries, transactions, evolving schemas | Multi-DC, massive write throughput, self-hosted | Flexible schemas, document queries, moderate scale |
| Scaling model | Automatic, pay-per-request | Vertical + read replicas; Citus for sharding | Linear, self-managed nodes | Sharded clusters, manual balancing |
| Consistency | Eventually consistent (default) or strongly consistent per-item | ACID transactions, serializable isolation | Tunable per-query (ONE to ALL) | Tunable (w:1 to w:majority) |
| Data model | Key-value + sort key, single-table design | Relational, normalized, JOIN-capable | Wide-column, query-first tables | Document (JSON), flexible schema |
| Operational cost | Zero (fully managed) | Medium (VACUUM, replication, backups) | High (JVM tuning, repair, compaction) | Medium (balancer, oplog, elections) |
| Query flexibility | Low — PK required, limited filter expressions | High — arbitrary SQL, aggregations, CTEs | Low — partition key required | Medium — rich query language on documents |
| Multi-region | Global tables (limited, expensive) | BDR/Citus (complex) | First-class, active-active | Atlas global clusters |
| Vendor lock-in | Complete (AWS only) | None (open source) | None (open source) | Moderate (Atlas vs self-hosted) |
| When to avoid | Complex queries, multi-cloud, >400KB items | >10TB, >100K writes/sec | Evolving access patterns, small scale | Massive write throughput, strict consistency |
Decision framework for interviews:
- "We're on AWS and want zero ops" → DynamoDB. No contest for serverless-first teams.
- "We need complex queries and might change access patterns" → PostgreSQL. DynamoDB punishes schema changes.
- "We need multi-region active-active" → Cassandra (first-class) or DynamoDB Global Tables (simpler, more limited).
- "We need flexible document queries" → MongoDB. Better query language than DynamoDB for ad-hoc access.
- "We need transactions across entities" → PostgreSQL for complex transactions. DynamoDB transactions work for up to 100 items.
- "We need to avoid vendor lock-in" → Cassandra or PostgreSQL. DynamoDB is AWS-only with no portable alternative.
Operational Concerns
Cost Optimization
DynamoDB costs catch teams off guard because they scale with request volume, not server count. Key optimizations:
- Eventually consistent reads by default — they cost half the RCU. Only use strong consistency where correctness demands it.
- Projection types on GSIs —
KEYS_ONLYorINCLUDErather thanALL. Every projected attribute multiplies storage and write cost. - TTL for data lifecycle — TTL deletes are free. Explicit
DeleteItemoperations consume WCU. - Batch operations —
BatchWriteItem(up to 25 items) andBatchGetItem(up to 100 items) reduce network overhead and request count. - Reserved capacity — for predictable provisioned workloads, reserved capacity offers 50-75% savings over on-demand pricing.
Monitoring Essentials
| Metric | Healthy Range | Alert Threshold |
|---|---|---|
| Read/Write throttle events | 0 | Any sustained >0 |
| SuccessfulRequestLatency p99 | <10ms | >25ms |
| ConsumedRead/WriteCapacity | <80% provisioned | >90% provisioned |
| SystemErrors | 0 | Any >0 |
| AccountProvisionedReadCapacity | Well below account limit | >80% account limit |
| GSI IteratorAge (Streams) | <1000ms | >60000ms |
Backup & Recovery
- Point-in-time recovery (PITR) — continuous backups with 35-day retention. Restore to any second within the window. Costs ~$0.20/GB/month. Enable on every production table.
- On-demand backups — manual snapshots retained until deleted. Use for pre-migration safety nets.
- S3 Export — export full table to S3 in DynamoDB JSON or Apache Ion format. Zero RCU consumed (reads from continuous backup). Use for analytics, data lake integration, and disaster recovery to non-AWS.
Global Tables — Multi-Region
DynamoDB Global Tables replicate a table across multiple AWS regions with active-active semantics. Writes in any region are replicated to all other regions with typical latency under one second. Conflict resolution uses last-writer-wins based on timestamps — there is no merge strategy or conflict detection callback.
Limitations to know: Global Tables add ~50% write cost (cross-region replication), strongly consistent reads only apply to the local region's data (cross-region reads are eventually consistent), and DynamoDB Streams behavior changes (stream records appear in the region where the write originated, not in replica regions). For most use cases, Global Tables are sufficient. For workloads requiring cross-region strong consistency or custom conflict resolution, consider a different architecture (e.g., single-region primary with read replicas).
Interview Application — Staff-Level Plays
Which Playbooks Use DynamoDB
| Playbook | How DynamoDB Is Used | Key Pattern |
|---|---|---|
| Database Sharding | Managed partitioning without manual sharding | Automatic partition splitting at 10GB/3K RCU/1K WCU thresholds |
| Data Consistency | Eventually consistent by default, strongly consistent reads optional | Single-region strong consistency, cross-region eventual (Global Tables) |
| Database Selection | NoSQL option for known access patterns at scale | Single-table design with PK/SK, GSI for secondary access patterns |
L5 vs L6 DynamoDB Responses
| Scenario | L5 Answer | L6/Staff Answer |
|---|---|---|
| "Design a session store" | "Use DynamoDB with session_id as partition key" | "DynamoDB on-demand with session_id as PK. TTL attribute set to created_at + session_duration for automatic expiry at zero WCU cost. DAX in front for microsecond reads. GSI on user_id for 'list active sessions' admin query with KEYS_ONLY projection to minimize cost." |
| "How do you handle hot keys?" | "Add more capacity" | "Write-sharding: append #shard-{hash % N} to the partition key. Distribute N=10 for predictable fan-out. DAX absorbs read hotspots. Adaptive capacity handles moderate skew, but a truly viral key needs architectural sharding." |
| "DynamoDB or PostgreSQL?" | "DynamoDB for NoSQL, PostgreSQL for SQL" | "DynamoDB when access patterns are known, stable, and read/write-heavy at scale — session stores, IoT telemetry, gaming leaderboards. PostgreSQL when access patterns are evolving, queries are complex, or you need cross-entity transactions. The deciding question: can you enumerate every query your application will ever run?" |
| "How do you model relationships?" | "Use multiple tables" | "Single-table design with overloaded PK/SK. Put related entities under the same partition key for single-query retrieval. GSIs for inverse lookups. Document every access pattern in a shared contract. If the relationship requires JOIN semantics, use PostgreSQL for that entity group." |
The Staff DynamoDB Checklist
When proposing DynamoDB in a system design interview:
- Justify the choice: "Our access patterns are known, read-heavy, and latency-sensitive. DynamoDB's single-digit-ms guarantee and zero operational overhead make it ideal."
- List access patterns first: "We have five access patterns: get user by ID, list orders by user, get order by ID, search products by category, and list reviews by product."
- Design the single table live: Write out PK/SK for each entity type. Show which queries each pattern serves.
- Identify GSIs needed: "We need a GSI with
emailas PK for login-by-email, and a GSI withcategoryas PK for product browsing." - Choose capacity mode: "On-demand to start (unknown traffic), migrate to provisioned + auto-scaling once traffic patterns stabilize (3-6 months)."
- Address failure modes: Hot partitions (write sharding), GSI backpressure (match capacity), item size (S3 for large objects).
- Name the complement: "DynamoDB for the hot path and session data. PostgreSQL for the user profile and billing data that needs transactions and flexible queries."
Quick Reference Card
Primary key: PK (partition key) + optional SK (sort key)
Consistency: Eventually consistent (default, 0.5 RCU) or strongly consistent (1 RCU)
Item limit: 400KB per item
Throughput: 3,000 RCU / 1,000 WCU per partition
GSI limit: 20 per table, eventually consistent only
LSI limit: 5 per table, 10GB partition limit, must create at table creation
Transactions: Up to 100 items, 2x WCU cost, same-region only
TTL: Free deletes, eventually consistent (up to 48h lag)
Streams: 24-hour retention, ordered per partition key
Backup: PITR (35 days), on-demand snapshots, S3 export (zero RCU)
Capacity modes: On-demand (~5x cost, zero throttling) vs Provisioned (cheaper, risk of throttling)
Anti-patterns: Scan for queries, low-cardinality PK, multi-table design, >400KB items