Part 7 of 814 min read · 10 diagramsAI-assisted

DynamoDB In Depth

Table of Contents#

  1. Why DynamoDB Gets Its Own Dedicated Part
  2. DynamoDB's Core Data Model
  3. Partition Keys and Sort Keys
  4. How DynamoDB Actually Partitions Data
  5. The Hot Partition Problem, Made Concrete
  6. Read/Write Capacity: Provisioned vs On-Demand
  7. Consistency Choices: Eventually vs Strongly Consistent Reads
  8. Global Tables — Multi-Region, Multi-Active
  9. Secondary Indexes: GSI vs LSI
  10. DynamoDB Streams
  11. Single-Table Design — The Controversial Best Practice
  12. Essential Operational Commands
  13. Backup and Point-in-Time Recovery
  14. When DynamoDB Is the Right Choice (and When It Isn't)
  15. Common Mistakes
  16. Worked Practice Problems
  17. Summary and What's Next

Why DynamoDB Gets Its Own Dedicated Part#

Part 5 introduced DynamoDB as a real-world example of a key-value/document NoSQL database, tracing its design lineage directly to Amazon's own Dynamo paper. Because it's genuinely one of the most widely deployed managed databases in the industry — and because it behaves in ways meaningfully different from both traditional relational databases and other NoSQL systems — it earns the same dedicated, hands-on treatment MySQL received in Part 6.


DynamoDB's Core Data Model#

DynamoDB is a fully managed key-value/document database — there's no server to patch, no storage engine to choose, no replication to configure by hand. Every table's structure is built around exactly one required concept: the primary key.

Diagram

A genuinely important terminology mapping worth knowing, directly connecting to Part 4's SQL vocabulary: a DynamoDB "table" is roughly a SQL table; an "item" is roughly a row; an "attribute" is roughly a column. But unlike a SQL table (Part 4), items in the SAME DynamoDB table can have completely different attributes — exactly the schema-on-read flexibility discussed for document databases in Part 5.


Partition Keys and Sort Keys#

The single most important design decision in any DynamoDB table — directly extending the shard-key discussion from Part 2, since DynamoDB's partition key IS its shard key.

Diagram
# A composite-key table: get ALL orders for user 'alice',
# sorted by order_date, WITHOUT touching any other user's data
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "user_id = :uid" \
  --expression-attribute-values '{":uid": {"S": "alice"}}'

Why this composite pattern is so powerful, worth stating explicitly: it directly gives you the "keep the common query pattern to a single partition" property from Part 2's sharding discussion, for free, as the natural default way of modeling one-to-many relationships in DynamoDB — all of one user's orders live together, physically, in the same partition, making "get this user's orders" a single, fast, cheap operation instead of the cross-shard fan-out Part 2 warned about.


How DynamoDB Actually Partitions Data#

Diagram

This is, mechanically, exactly the hash-based sharding strategy already covered in depth in Part 2 — DynamoDB doesn't expose this as a choice; it's baked into the service itself, automatically, using the same consistent-hashing-style approach (from the Reliability & Architecture Patterns series) to minimize disruption as partitions split and grow. The practical implication: your partition key choice is your sharding strategy, whether you think about it that way or not.


The Hot Partition Problem, Made Concrete#

Directly, precisely the "hot shard" problem from Part 2 — but DynamoDB makes the consequence unusually visible and immediate, since capacity is allocated per-partition.

Diagram

Why this is such a commonly-tested, concrete DynamoDB interview scenario, worth having a specific answer ready: "choosing a low-cardinality partition key (like a status field with only 3-4 possible values) is one of the most common real DynamoDB design mistakes — it directly recreates the hot-shard problem from general sharding theory, but DynamoDB's per-partition throughput allocation means you'll hit hard, visible throttling (ProvisionedThroughputExceededException) specifically on that one overloaded partition, even while the table's AGGREGATE capacity looks completely fine."


Read/Write Capacity: Provisioned vs On-Demand#

Diagram
# Enable auto-scaling for a provisioned-capacity table
# (a middle ground between the two extremes)
aws application-autoscaling register-scalable-target \
  --service-namespace dynamodb \
  --resource-id "table/Orders" \
  --scalable-dimension "dynamodb:table:ReadCapacityUnits" \
  --min-capacity 5 --max-capacity 100

Consistency Choices: Eventually vs Strongly Consistent Reads#

Directly, concretely extending the PACELC discussion from the Reliability & Architecture Patterns series — DynamoDB is one of the clearest real-world examples of a system exposing this exact tradeoff per individual query.

# Default: EVENTUALLY consistent read — faster, cheaper
# (half the read capacity cost), might return slightly stale data
aws dynamodb get-item --table-name Orders --key '{"order_id": {"S": "101"}}'

# STRONGLY consistent read — guaranteed to reflect the most
# recent successful write, at higher cost and slightly higher latency
aws dynamodb get-item --table-name Orders \
  --key '{"order_id": {"S": "101"}}' \
  --consistent-read

Why this is such a strong, concrete answer to "explain PACELC with a real example," worth citing directly: "DynamoDB literally exposes the Latency-vs-Consistency choice from PACELC as a single boolean flag on every read request — --consistent-read. Choosing eventually consistent reads (the default) means lower latency and half the read cost; choosing strongly consistent reads means guaranteed freshness at a real, measurable cost. It's the cleanest, most direct real-world illustration of that theoretical tradeoff I know of."


Global Tables — Multi-Region, Multi-Active#

DynamoDB's answer to multi-region replication, directly extending the multi-region discussion from Part 1 of this series and the Disaster Recovery series' Multi-Site Active-Active strategy.

Diagram

Why the conflict-resolution mechanism matters, directly connecting to the CAP theorem and multi-region replication discussions from Parts 1 and elsewhere: Global Tables use "last writer wins" (based on timestamps) to resolve conflicting concurrent writes to the SAME item in different regions — a deliberate, explicit AP-style choice (Reliability & Architecture Patterns series), prioritizing availability and low regional latency over strict consistency, exactly the same tradeoff the Disaster Recovery series flagged as the real, hard complexity behind any multi-site active-active architecture.


Secondary Indexes: GSI vs LSI#

Directly extending the indexing discussion from Part 4 — DynamoDB's version, with genuinely important, DynamoDB-specific constraints.

Diagram

Why the "GSI can be added anytime, LSI cannot" distinction is such a commonly-tested, practical gotcha: a team that didn't anticipate a specific query pattern at table-creation time can still add a GSI later with zero downtime — but if that pattern would have been better served by an LSI, they're out of luck; LSIs are a one-time, table-creation-only decision, making them a real, permanent design commitment in a way GSIs simply aren't.


DynamoDB Streams#

A genuinely powerful, event-driven feature worth knowing — directly connects to the binlog/WAL-based change-capture concepts from Parts 1 and 6.

Diagram

Why this is conceptually the exact same idea as MySQL's binlog (Part 6) or PostgreSQL's WAL (Part 1), just exposed as a first-class, directly-consumable event stream rather than an internal replication mechanism: it's DynamoDB's own "every change, in order" log, deliberately made available for building reactive, event-driven architectures — directly connecting to the CI/CD and GitOps series' reconciliation-loop theme, applied to data changes instead of infrastructure state.


Single-Table Design — The Controversial Best Practice#

A genuinely distinctive, frequently-debated DynamoDB modeling philosophy worth knowing about, even if just to discuss its tradeoffs intelligently.

Diagram

Why this is worth citing as a real, debated tradeoff rather than a universal rule, worth stating explicitly in an interview: "Single-table design is a genuinely powerful technique for minimizing the number of round trips DynamoDB needs for a specific set of KNOWN access patterns, but it requires modeling those access patterns completely upfront — a much less flexible starting point than a relational schema (Part 4), where new query patterns can usually be served with a new JOIN. I'd reach for it when access patterns are well-understood and read-latency at scale genuinely matters, not as a default for every DynamoDB table."


Essential Operational Commands#

# Describe a table's configuration (capacity mode, indexes, keys)
aws dynamodb describe-table --table-name Orders

# Check current consumed capacity (helps diagnose throttling)
aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ConsumedReadCapacityUnits \
  --dimensions Name=TableName,Value=Orders \
  --start-time 2026-06-01T00:00:00Z --end-time 2026-06-01T01:00:00Z \
  --period 300 --statistics Sum

# Scan (reads EVERY item — expensive, avoid on large tables)
aws dynamodb scan --table-name Orders

# Query (uses the partition key — efficient, the normal access pattern)
aws dynamodb query --table-name Orders \
  --key-condition-expression "user_id = :uid" \
  --expression-attribute-values '{":uid": {"S": "alice"}}'

Why Scan vs Query deserves its own explicit callout, directly connecting to the full-table-scan discussion from Part 4: Scan reads every single item in the table, checking each against any filter AFTER reading it — exactly the DynamoDB equivalent of a SQL full table scan, and just as expensive at real scale. Query uses the partition key (and optionally sort key) to jump directly to relevant data, exactly the DynamoDB equivalent of an efficient, indexed SQL query. A Scan showing up in production code against a large table is a real, common, actionable performance red flag.


Backup and Point-in-Time Recovery#

Directly extending the backup/recovery discussion from Part 3 — DynamoDB's own concrete implementation of the same principles.

# Enable continuous backups, enabling point-in-time recovery
# (restore to any second within the last 35 days)
aws dynamodb update-continuous-backups \
  --table-name Orders \
  --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true

# Restore to a specific point in time (creates a NEW table —
# doesn't overwrite the original, exactly the same safety
# principle as the point-in-time recovery discussion in Part 3)
aws dynamodb restore-table-to-point-in-time \
  --source-table-name Orders \
  --target-table-name Orders-Restored \
  --restore-date-time 2026-06-01T14:44:59Z

Why restoring to a NEW table (never overwriting the original in place) matters, worth stating explicitly, and directly reinforcing Part 3's core lesson: this is exactly the same safe-recovery discipline as restoring a relational database to a separate instance before deciding how to merge data back — it guarantees the recovery process itself can never accidentally make a bad situation worse by destroying the current (possibly still partially valid) state.


When DynamoDB Is the Right Choice (and When It Isn't)#

Directly closing the loop with Part 5's SQL-vs-NoSQL decision framework, now made concrete for this specific database.

Diagram

A strong, senior-level closing interview line: "DynamoDB is an excellent choice when I can identify the real access patterns up front and need genuinely massive, predictable scale with low, consistent latency — its whole design, from partition keys to single-table modeling, assumes you know your queries in advance. When requirements are still evolving, or ad hoc analytical queries matter more than raw operational scale, I'd lean toward a relational database instead, where JOINs (Part 4) let new query patterns emerge without a full data-model redesign."


Common Mistakes#

MistakeWhy It's WrongFix
Choosing a low-cardinality partition key (like a status field)Recreates the hot-shard problem from Part 2 in its most extreme, visible form — one partition absorbs all traffic for that valueChoose a high-cardinality partition key that naturally spreads load evenly
Using Scan in production code against a large tableReads every item in the table, checking filters after the fact — the DynamoDB equivalent of a full table scanUse Query with a proper partition key condition instead
Assuming an LSI can be added to a table after creationLSIs must be defined at table-creation time and can never be added laterPlan LSI needs upfront, or use a GSI (addable anytime) if the requirement emerges later
Adopting single-table design without fully modeling access patterns firstProduces a genuinely hard-to-reason-about schema without delivering its main benefit (fewer round trips for known patterns)Only use single-table design when access patterns are well understood in advance
Using strongly consistent reads everywhere "to be safe"Pays real, unnecessary latency and cost for freshness guarantees most reads don't actually needDefault to eventually consistent reads; reserve strongly consistent reads for the specific cases that genuinely require them
Assuming Global Tables provide strong consistency across regionsThey use last-writer-wins conflict resolution — an explicit AP-style, eventually-consistent design choiceUnderstand and design around the real conflict-resolution behavior before relying on cross-region strong consistency

Worked Practice Problems#

Problem 1: A table using order_status (values: "pending", "shipped", "delivered", "cancelled") as its partition key starts throwing ProvisionedThroughputExceededException errors during a sale event, even though the table's overall provisioned capacity looks sufficient in CloudWatch. What's happening, and what's the fix?

Answer: This is a textbook hot partition — with only 4 possible partition key values, every single "pending" order (likely the vast majority of traffic during an active sale) lands on the same physical partition, which has its own fixed slice of the table's total capacity, regardless of how much AGGREGATE capacity is provisioned overall. The fix is redesigning the key schema around a high-cardinality partition key (like order_id), moving order_status to a regular attribute (queryable via a GSI if "find all pending orders" is still a needed access pattern) — spreading write load evenly across many partitions instead of concentrating it on one.

Problem 2: An application performs a strongly consistent read immediately after every write, "to be safe," across a table with heavy read traffic. A cost review flags DynamoDB read costs as unexpectedly high. What would you investigate, and what's the likely fix?

Answer: Strongly consistent reads cost roughly double the read capacity of eventually consistent reads, and add measurable latency — if most of those reads don't actually need guaranteed up-to-the-millisecond freshness (e.g., displaying a user's own recently-placed order, where a few hundred milliseconds of eventual consistency would be imperceptible), this blanket policy is paying a real, ongoing cost for a guarantee that's rarely actually needed. I'd audit which specific read paths genuinely require strong consistency (e.g., a read immediately followed by a conditional write depending on the exact current value) versus which are just defensive habit, and switch the latter to the default eventually consistent reads.

Problem 3: A team designs a DynamoDB table with a composite key of user_id (partition) and created_at (sort key), planning to query "all orders for a user, most recent first." Midway through development, product asks for a new feature: "show all orders across all users placed in the last hour, for fraud monitoring." What's the problem with the current design for this new requirement, and what would you recommend?

Answer: The current key schema is well-designed for the original per-user access pattern, but it provides no efficient way to query across ALL users by a time range — that would require a full table Scan (checking every item's created_at against the time window), which is exactly the expensive, discouraged pattern this tutorial warns against. The fix: add a Global Secondary Index (GSI) with a different, purpose-built key — for example, a constant or coarse-grained partition key (like a truncated hour-bucket) paired with created_at as the sort key — giving the fraud-monitoring feature its own efficient Query-based access pattern without disturbing the base table's existing, working design for the original per-user use case.


Summary and What's Next#

  • DynamoDB is a fully managed key-value/document database where the partition key choice is, mechanically, your sharding strategy — directly reusing the hash-based sharding concepts from Part 2.
  • Composite primary keys (partition key + sort key) let many related items share a partition, naturally keeping common one-to-many query patterns (like "all of this user's orders") efficient and single-partition.
  • Hot partitions are the concrete, highly visible DynamoDB manifestation of Part 2's hot-shard problem — a low-cardinality partition key throttles even when aggregate table capacity looks fine.
  • Provisioned vs. on-demand capacity is a direct cost-vs-operational-simplicity tradeoff, and eventually vs. strongly consistent reads is one of the cleanest real-world illustrations of the PACELC latency-vs-consistency tradeoff from the Reliability & Architecture Patterns series.
  • Global Tables provide multi-region, multi-active replication using last-writer-wins conflict resolution — an explicit, deliberate AP-style choice, not accidental.
  • GSIs can be added anytime; LSIs are a permanent, table-creation-time-only commitment — a real, commonly-tested distinction.
  • DynamoDB Streams provide the same "ordered change log" capability as MySQL's binlog or PostgreSQL's WAL, deliberately exposed as a first-class, consumable event source for reactive architectures.
  • Single-table design trades upfront modeling complexity for fewer round trips on well-known access patterns — a deliberate, situational technique, not a universal default.

Continue to Part 8 (08-mongodb-in-depth.md) for the same focused, hands-on treatment applied to MongoDB — its document model in practice, replica sets, and sharding.