Denshin / Blog / Engineering
DynamoDB single-table design at small scale: what actually mattered
After a few years running production DynamoDB single-table designs for small SaaS apps, the parts that earned their keep, and the parts that bit us.
Denshin Engineering · Engineering Team · 18 April 2026 · 2 min read
DynamoDB single-table design has a reputation for being either miraculous or miserable, with not a lot of middle ground. After running a few production systems on it, my honest read is that the people who hate it are usually building at a scale where they didn't need it, and the people who love it built something that was always going to be hard either way.
Here's where it actually paid off for us.
One bill, no surprises
A single on-demand table costs basically nothing at the volume our admin-heavy SaaS apps run. We have client systems billing less than three dollars a month for the database, including all writes. Compare that to an RDS instance you have to size, an Aurora cluster that costs whether you use it or not, or a managed Mongo with a per-cluster floor.
Access patterns first, schema later
This is the part that took us the longest to internalise. With a relational database, you start with the entities. With a single-table DynamoDB, you start with the queries. "Show me all published blogs in this category, newest first" becomes a sparse GSI before it becomes any code.
The win is that once you've named the access pattern, the implementation is mostly mechanical. The cost is that adding an unplanned access pattern later is genuinely annoying. So we spend more time at the start sketching, and less time later regretting.
Where it bites
Search. Real text search. Don't try. We've tried filtering server-side using contains, we've tried client-side filtering for small entities, we've tried OpenSearch. The honest answer for most of our admin panels is: load the entity list once, filter in memory, accept the bound. When someone has more than ten thousand entities, give them OpenSearch. Not before.
The other place it bites is anything that wants a complex transactional invariant across entities. DynamoDB has transactions, they're real, and they have hard limits. If you find yourself constantly bumping into them, your domain might genuinely want a relational store.
The pattern we've settled on
For our typical project: one table, two GSIs, occasionally a third sparse one. pk and sk spell the entity. gsi1 is "list everything of this entity, sorted". gsi2 is for slug or unique-identifier lookups. gsi3 shows up when we have a "give me published items in this category, newest first" query and we don't want to filter the whole entity list.
That's it. We don't optimise further until something actually feels slow. So far, nothing has.
One more thing nobody tells you: the entity-type prefix in the partition key (BLOG#, SERVICE#) is doing more work than it looks like it is. It's the thing that lets you debug a stray item by eyeballing it in the AWS console without having to remember which GSI it came from. Keep the prefixes. Future-you will thank you.
Tags: DynamoDB, AWS, Architecture, Database, Single-Table
All posts · Work with Denshin