Multi-tenant SaaS architecture.
One database serving every customer, and exactly one bug standing between that and showing one customer another customer's data.
The components
Every row below is read from the graph that produced the diagram above, so the two cannot disagree.
| Component | Tier | Why it is there |
|---|---|---|
| Web app | Client | Supporting component |
| API gateway | Edge | Supporting component |
| Auth service | Application | Tenant id is resolved here, once |
| Tenant context | Application | Middleware, so no query can forget the filter |
| Application API | Application | Supporting component |
| Billing & plans | Application | Supporting component |
| Background jobs | Application | Queue partitioned per tenant, so one cannot starve the rest |
| Admin console | Application | Supporting component |
| Postgres | Data | Row-level security, tenant id on every table |
| Redis | Data | Keys namespaced per tenant |
| Object store | Data | Prefix per tenant |
| Billing provider | External | Supporting component |
Design decisions worth arguing about
A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.
Shared database with row-level security
A database per tenant gives the strongest isolation and turns every migration into an operation across hundreds of databases. A shared schema with a tenant column is far easier to operate and puts one predicate between customers. Enforcing that predicate in the database with row-level security rather than in application code is what makes it a guarantee rather than a convention, because a forgotten WHERE clause then returns nothing instead of everything.
Tenant context in middleware, resolved once
Reading the tenant from the request in every handler means every new endpoint is an opportunity to forget. Resolving it once at the edge of the request and making it ambient means queries cannot be written without it. The cost is a piece of implicit context, which is unfashionable and is exactly the right shape for something that must never be omitted.
Partition background work per tenant
A single shared job queue lets one tenant's bulk import consume every worker and stall everyone else, and it will, usually on the day they onboard. Partitioning by tenant with per-tenant concurrency limits contains it. It costs scheduling complexity and some idle capacity, which is much cheaper than the alternative of every customer being affected by the largest one.
Noisy neighbours are a product problem
Technical isolation limits the damage but does not decide what should happen when a tenant exceeds their share. Per-plan quotas make that explicit and enforceable, and they turn an operational incident into a billing conversation. Without them the only available responses are to absorb the cost or to intervene manually.
How it changes with scale
Cost is dominated by the largest tenants while revenue is spread across all of them, so per-tenant resource accounting matters earlier than expected. Most tenants are small, which is what makes sharing efficient; a handful are large enough that moving them to dedicated infrastructure is both possible and often the right answer.
Where it breaks first
A missing tenant predicate in a query written outside the normal path, such as a reporting job or a data migration. Row-level security catches it if the job connects as a normal role; jobs that connect with elevated privileges bypass exactly the protection that makes the design safe, which is why the admin path deserves more scrutiny than the application path.
Draw this yourself
Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.
When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.
Questions about this design
More templates
- URL Shortener System DesignThe canonical read-heavy system: roughly a hundred reads for every write, and a redirect that has to be fast enough that nobody notices it happened.
- Chat Application System DesignLong-lived connections change everything: the hard part is not storing messages, it is knowing which of your servers is holding the socket you need to write to.
- Payment System DesignThe only system on this list where being approximately right is indistinguishable from being wrong, and where the provider, not you, holds the truth.
- RAG Pipeline ArchitectureAlmost every RAG system that disappoints is failing at retrieval, not generation, and the architecture is what decides whether you can tell.