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
When should a tenant get their own database?
When they are large enough that their load affects others, or when a contract requires physical isolation. Designing so that a tenant can be moved later, without changing application code, is more valuable than choosing correctly up front.
How should tenants be identified?
From the authenticated session, never from a request parameter. A tenant id the client can set is an access control bug waiting for someone to notice it.
Do tenants need separate encryption keys?
Only if the threat model or a contract requires it. Per-tenant keys complicate every operation touching data at rest, including backups and restores, and that cost is real.
How do you test tenant isolation?
With a test that runs every query as one tenant and asserts another tenant's rows are invisible, run in CI rather than reviewed by eye. Isolation is exactly the kind of property that holds until a refactor quietly breaks it.
More templates
- Data Warehouse ETL Pipeline DesignLand the raw data first and transform it later, because the transformation you want in six months is not the one you would write today.
- IoT Telemetry Platform DesignAssume every device is offline, on a bad network, running firmware from two years ago, and cannot be recalled. The architecture follows from that.
- API Gateway System DesignOne entry point, so authentication, rate limiting and observability are implemented once instead of in every service.
- Feature Flag Service DesignEvaluation has to be local and instant, because a flag check sits in the hot path of code that would otherwise not make a network call at all.
Last updated