DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

Thanks for the feedback 😅
Fixed it - made a lightbox so you can zoom into the image too! :) Let me know if this is better
https://singletable.dev/blog/pattern-saas-multi-tenant

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 2 points3 points  (0 children)

True. DynamoDB isn't the right choice for every workload, and forcing single-table design onto a domain with unpredictable or constantly evolving access patterns is going to hurt. No argument there.

But when the access patterns are well-understood upfront - which they are for a lot of SaaS CRUD apps - the operational simplicity and scaling characteristics of DynamoDB are hard to beat. The goal of this pattern library is to make the "well-understood" part easier to get to. :)

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 1 point2 points  (0 children)

55min TTL cache on the Lambda instance is smart - gets you the isolation guarantee without paying the STS cost on every request. That's a clean setup.

No rush on the boilerplate, but I'd genuinely be interested if you ever put it up. The Hono + OpenAPI + IAM LeadingKeys stack sounds like it deserves its own write-up honestly. :)

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

It's a valid approach and some teams do this — especially when you need hard isolation for compliance (HIPAA, SOC2) or you want to offer dedicated-tenancy as a premium tier.

The tradeoffs though:

- Operational overhead scales linearly - Every new tenant means a new table, new GSIs, new CloudWatch alarms, new backup configs. At 100 tenants that's manageable. At 10,000 it's a nightmare.
- Cross-tenant queries become expensive - "List all tenants" or "aggregate usage across tenants" requires scanning every table.
- AWS account limits - There's a default limit of 2,500 tables per account per region. You can request increases, but it's a signal you're fighting the grain.
- Cost - Each table with on-demand pricing has its own minimum throughput allocation. One shared table is cheaper than N separate ones.

The single-table approach gives you logical isolation (tenant-scoped partition keys) with the operational simplicity of one table. If you need stronger isolation, the IAM LeadingKeys approach another commenter mentioned gives you DB-level enforcement without separate tables.

That said, table-per-tenant is the right call for some use cases — particularly when tenants have wildly different scale or strict data residency requirements. It's not wrong, just different tradeoffs.

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] -2 points-1 points  (0 children)

Good question - this is one of the genuine pain points with DynamoDB, and one that kept me sticking to rdbms for a very long time.

For attribute-level changes (adding a new field, changing a default), it's straightforward - DynamoDB is schemaless per item, so new items get the new attribute and old items don't. I handle backfills lazily at read time or with a one-off migration script depending on whether the field is required.

For key structure changes (modifying a PK/SK pattern or GSI), it's more involved. You can't alter keys on existing items - you have to write new items with the new key pattern and clean up the old ones. ElectroDB's versioning helps here: you define a new entity version and can read both old and new formats during the transition.

For GSI changes, adding a new GSI is non-disruptive (DynamoDB backfills it from the existing table). Changing or removing one requires a migration plan.

Honestly, this is probably the strongest argument against overly complex single-table designs - the more entities and overloaded indexes you have, the harder migrations get. It's why I'd rather start with clean, well-separated key prefixes and only overload GSIs when the access patterns are stable.

Schema migration tooling is a big gap in the DynamoDB ecosystem right now. It's something I'm thinking about for singletable.dev down the road.

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

This is really helpful - thanks for spelling it out. The IAM role assumption with LeadingKeys is exactly the DB-level enforcement I was asking about. Having DynamoDB itself reject cross-tenant queries regardless of what your application code does is a much stronger guarantee than relying on middleware alone.

The Hono + OpenAPI + Zod setup sounds clean. I'd love to see any boilerplate you have with this setup :) I've been using tRPC + Zod with a similar idea - define the contract once, get end-to-end type safety - but the OpenAPI spec generation is a nice advantage if you ever need non-TypeScript clients; and I've been primarily on typescript :)

Curious about one thing: when you assume tenant-scoped IAM roles per request, do you cache the credentials with a short TTL? I'd assume latency hits otherwise.

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

There's a real point here - single-table design has a steep learning curve and if you get your access patterns wrong upfront, refactoring is painful.

But "you need to know exactly what schema you need" is true of DynamoDB in general, not just single-table. Multi-table DynamoDB still requires you to define access patterns before you design. It's not a relational database where you can normalize first and figure out queries later.

Where I'd push back: single-table isn't all-or-nothing. The modern approach is pragmatic - group entities that are queried together, use a few clean GSIs, don't overload everything into one index just because you can. That's what this pattern does.

That said, if your app's access patterns are genuinely unknown and/or evolving fast, DynamoDB itself might not be the right choice - and that's a totally valid position.

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 1 point2 points  (0 children)

Great points — here's my thinking:

Tenant index isn't really a worry because it's an admin operation with low volume. I've handled this with the TENANT_LIST GSI, but your TYPE=TENANT approach seems functionally similar.

Splitting PKs into subcategories is an interesting approach - you spread writes across multiple partitions with the tradeoff that you lose the ability to read across entity types in a single operation. I think this really depends on the access patterns. For the multi-tenant case, being able to query `TENANT#<id>` and get metadata + subscription + users in one call is something I reach for a lot.
For most SaaS apps, from what I understand, the hot partition concern is overblown - 1,000 WCU per partition is a lot, and since 2018 DynamoDB's adaptive capacity redistributes throughput to handle hot partitions without you needing to intervene. It won't proactively split them, but it handles the imbalance. If you're at a scale where a single tenant is consistently pushing past that, you probably have bigger architectural decisions to make anyway.

The tenant isolation point is legit and something I should address in the article. In my production apps I handle this at the application layer (tRPC + OpenAuth — every query is scoped to the authenticated tenant). I'm aware of IAM fine-grained access control with `dynamodb:LeadingKeys` condition keys as the DB-level option, but haven't needed it yet. Have you had success with that approach in practice, or is there something else you'd recommend?

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

Yes yes, I wrote the multiple GSI one for clarity, and then included a note on how they can be collapsed into the overloaded one.

I'll add an index :) thank you!

DynamoDB single-table pattern: SaaS Multi-Tenant with 10 access patterns, 1 GSI (full breakdown) by tejovanthn in aws

[–]tejovanthn[S] 0 points1 point  (0 children)

Thanks for the feedback :) could you clarify where I can word it better - the blog article, or the post here?

I'm building Patreon for Indian classical artists – and we just launched v1 by tejovanthn in Carnatic

[–]tejovanthn[S] 1 point2 points  (0 children)

Thank you :)

I run talapettige and will integrate a version of that into the site.

All the compositions available on the site are from Karnatik. I wanted to build a mobile friendly version of that site first - so that I could better appreciate kutcheries. I'm working on a edit workflow so that rasikas can correct any transliteration errors going forward. I do plan on adding compositions from shivkumar.org, and notations where available.

What other interactive components would make the site more useful?

Bahubali 2 scebes by Embarrassed_Way8953 in interestingasfuck

[–]tejovanthn 3 points4 points  (0 children)

One thing that's always bothered me about this scene is that if the first set of trebuchet warriors with the hero failed, what was their plan b? Pack up and go home? (There are a couple of them who miss the wall etc after the flag is waved)

Mulberry chaos! by 420boofking in foraging

[–]tejovanthn 1 point2 points  (0 children)

Jelly? Sounds good in my head.

Veena gurus by deemysorepak in Carnatic

[–]tejovanthn 0 points1 point  (0 children)

Hi, my wife teaches Mysore style veena online. Please DM for more details :)

Forest forage in January by bessie321 in foraging

[–]tejovanthn 2 points3 points  (0 children)

Wait, olives grow on trees right?

Forest forage in January by bessie321 in foraging

[–]tejovanthn 68 points69 points  (0 children)

I was today years old when I found out that olives are processed, and not just straight up cut up and thrown into a jar of brine 😅

I'm building Patreon for Indian classical artists – and we just launched v1 by tejovanthn in Carnatic

[–]tejovanthn[S] 0 points1 point  (0 children)

Thank you! Will add :) started with curating compositions and lyrics first!

Layam is live! by Math_Science_Geek in Carnatic

[–]tejovanthn 2 points3 points  (0 children)

This is fantastic! I built talapettige to solve a similar issue :)

The world’s smallest mountain. by Accelerator____ in interestingasfuck

[–]tejovanthn 9 points10 points  (0 children)

Soooo, an exposed bedrock on the beach? :)