Cloud Intelligence™
DynamoDB: What Most Teams Get Wrong Before They Write a Line of Code
This page is also available in Deutsch, Español, Français, Italiano, 日本語, and Português.
About Joseph Allam
Databases have been my craft for a long time — long before the cloud, and deep into it across AWS and GCP. I know what breaks, what costs too much, and what needs rethinking before it hurts. I also know how to bring AI into the work: into how databases are designed, operated, and accessed by the intelligent systems being built on top of them.
My personal pageOn Prime Day 2025, Amazon DynamoDB peaked at 151 million requests per second. It's the database powering some of the most demanding workloads on the planet, and it has been for nearly two decades. Most engineers working on AWS have heard of it. Many have used it. But in the DynamoDB Immersion Day I recently ran with the AWS team, the same problem surfaced in almost every session: teams that had picked DynamoDB for the right reasons were still struggling because they were designing schemas the wrong way.
This post is about that. Not DynamoDB basics (there is plenty of that elsewhere), but the design thinking that separates the teams who get it working well from those who end up with runaway costs and tables that don't scale.
Is DynamoDB the right choice?
Before getting into design, it is worth being direct about fit. DynamoDB is a good choice when you need consistent, single-digit millisecond latency at virtually any scale, your access patterns are known and predictable upfront, and your workload is read/write-heavy rather than analytically complex. Good examples: user-facing APIs, session stores, leaderboards, shopping carts, IoT ingestion, event sourcing.
It is a poor choice when you need ad-hoc queries across many dimensions, your access patterns are still evolving, or you are dealing with complex relational joins. If you are replacing a reporting database or building something where the query requirements will change month to month, you will fight DynamoDB the whole way.
Item size matters more than most teams expect
Before committing to DynamoDB, model the cost properly. DynamoDB charges write capacity in 1 KB increments, rounded up. In on-demand mode a 20 KB item consumes 20 Write Request Units (WRUs); in provisioned mode the same write consumes 20 Write Capacity Units (WCUs). Reads work similarly, billed in 4 KB increments per Read Capacity Unit (RCU), so item size drives both sides of your capacity cost.
This sounds simple, but item size is one of the most commonly overlooked variables when teams model DynamoDB costs. We regularly see customers migrating from relational databases like PostgreSQL where the concept of per-byte billing on writes does not exist, and their initial cost estimates are significantly off because they have not accounted for item size properly. Getting that number right before you commit changes the economics entirely.
The point is not that DynamoDB is expensive. The point is that the cost model is different from what most engineers are used to, and the variables that drive it (item size, access pattern efficiency, on-demand vs provisioned capacity) need to be understood before you commit, not after.
The mental model shift that changes everything
Here is the thing most engineers bring to DynamoDB from a relational background: they design the schema first, then figure out the queries.
With a relational database, that mostly works. Normalize the data, build indexes later, write queries that join across tables. The database engine handles much of the query complexity.
DynamoDB does not work that way. Unlike relational databases, it does not optimize arbitrary queries through a query planner. Instead, it routes each request directly to the partition that owns the data using the partition key hash, which is one of the reasons DynamoDB can consistently deliver single-digit millisecond latency regardless of table size. The trade-off is that the database is optimized for one thing: retrieving items efficiently using key-based access patterns, through the primary key or secondary indexes. Everything else (filtering, sorting by non-key attributes, querying across entity relationships) is either expensive or requires additional indexes designed in advance.
The shift is this: you need to know your access patterns before you design your table. Not roughly. Specifically. "Get all orders for a user, sorted by date" is a design input. "Get the 10 most recent escalated support tickets for a given account" is a design input. Your schema is built backwards from those questions, not from the shape of the data.
In practice this means a single DynamoDB table often stores multiple entity types side by side, with partition keys and sort keys constructed to answer specific questions efficiently. That looks wrong to anyone coming from a normalized SQL background. It is not. It is the whole point.
Composite sort keys
Take a common example: a user's orders, where you need to query by status and date. The naive approach stores status and date as separate attributes, then filters at read time. You end up reading every order for that user and discarding the ones that don't match.
The access-pattern-first approach encodes the query directly into the sort key:
| PK | SK |
|---|---|
| user#123 | ACTIVE#2024-07-15 |
| user#123 | ACTIVE#2024-07-01 |
| user#123 | COMPLETED#2024-06-20 |
| user#123 | COMPLETED#2024-06-10 |
Now a single query with BEGINS_WITH("ACTIVE#") returns only the active orders, sorted by date, without reading a single completed order. You pay for exactly what you retrieve.
The reason this works is that BEGINS_WITH is a prefix query on the sort key. The broadest grouping must come first so DynamoDB can use it to narrow the range. A common approach is to place the broadest condition on the left and progressively more specific components to the right, so that prefix queries can efficiently target the subset you care about.
Sparse Global Secondary Indexes
Another pattern that falls out of access-pattern-first thinking: if only a small fraction of your items ever meet a condition (escalated tickets, flagged records, items awaiting review), model that condition as an attribute that only exists on those items. A GSI built on that attribute only indexes the items where it is present.
The key behaviour here is that DynamoDB does not include an item in a GSI when the indexed attribute is absent (not just null, but entirely missing from the item). This means you can make a GSI sparse by design simply by omitting the attribute on items you don't want indexed.
If 2% of your tickets are escalated, the GSI contains 2% of the table. Querying it is vastly cheaper than scanning the full table and filtering. The GSI is sparse by design, not by accident.
Your cloud bill shouldn't be a mystery
Optimization, automation, expertise. In one platform.
The mistakes that show up on your bill
These are worth naming specifically because they are common, they are invisible until you look at your cost reports, and they are usually introduced by engineers who understand DynamoDB reasonably well.
Filter expressions
DynamoDB does support filtering query results after retrieval. The problem is that a FilterExpression does not reduce the capacity units consumed; it only reduces what is returned to the caller. If a query reads 500 items before the filter runs, you are charged for all the data read up to that point, regardless of how many items are returned to the caller.
This becomes expensive fast when it is the primary strategy for narrowing results. It feels like a SQL WHERE clause, so it is easy to reach for without thinking about the cost. The fix is almost always to redesign the key structure so the database retrieves only what you need. Filter expressions have their place, but they should not be load-bearing in your query strategy.
Hot partitions from time-based keys
A common pattern when building time-series data: using the current timestamp or current hour as the partition key. The reasoning seems intuitive: partition by time bucket. The problem is that all writes target the same logical partition key, preventing DynamoDB from distributing write traffic evenly across partitions. At any meaningful write volume this leads to throttling.
DynamoDB does have Adaptive Capacity, which automatically redistributes throughput toward partitions that are receiving more traffic. For moderate skew, it often handles the problem transparently. But no amount of adaptive capacity compensates for a single partition key receiving sustained extreme traffic, and a time-bucket key under high write load is exactly that situation.
The fix is write sharding: append a random suffix to the partition key (for example, EVENTS#4 where the suffix is a number between 0 and N) to spread writes across multiple logical partitions. At read time, query each shard in parallel and merge the results. It requires more application code, but it is what keeps a high-throughput write workload from bottlenecking on a single partition.
Table scans
The third one worth calling out: full table scans. DynamoDB supports Scan operations, and engineers sometimes reach for them when they run out of GSIs or need to query by an attribute that is not indexed. The problem is that a scan reads every item in the table regardless of how many match. On a large table, this is both slow and expensive.
Scans are perfectly reasonable for migrations, exports, admin jobs, or background processing where latency does not matter. The problem is using them in user-facing request paths. If a scan is in your application's critical flow, it is almost always a signal that the access patterns were not fully accounted for in the schema design. The fix is usually a new GSI or a redesigned sort key, not a faster scan.
Closing thoughts
DynamoDB is genuinely powerful, and for the right workloads it is one of the best databases AWS offers. But the gap between "picked the right database" and "designed it correctly" is wide, and that gap shows up in your AWS bill before it shows up anywhere else. The cost model is different, the design process is different, and the mistakes are different from what most engineers have run into before.
If you are evaluating DynamoDB for a new workload, or you already have tables in production and the costs are harder to explain than you would like, this is exactly the kind of work DoiT does with AWS customers every week. Our team of 100+ cloud experts can help you validate access patterns, spot design issues early, and get the most out of the services you are already paying for. Book a demo.