Multi-tenancy decisions you cannot undo later
Tenancy model, tenant identifier and data residency are the three SaaS choices that are painful to reverse. How to pick each one deliberately, up front.
Most SaaS architecture is reversible. Three decisions are not, or at least not cheaply.
The distinction matters because early-stage advice — ship it, decide later, optimise when you have customers — is correct about almost everything else. These three are the exceptions, and the reason is the same in each case: by the time the decision hurts, reversing it means moving live customer data.
1. The tenancy model
Shared schema with a tenant column is simplest and cheapest, and it puts the burden of isolation on every query you ever write. Schema- or database-per-tenant costs operational complexity and buys real isolation plus per-tenant restore. Choose based on your compliance story and your largest expected tenant, not on what is fastest to start.
Shared schema means one database, every table carrying a tenant_id, and a global scope that had better never be forgotten. It is the cheapest to operate by a wide margin — one migration, one connection pool, one backup — and it is what most successful SaaS products run. Its weakness is that isolation is a property of your code rather than of your infrastructure: a single query missing its scope is a cross-tenant data leak, and that query will be written eventually, in a raw SQL report or a hastily added admin endpoint.
Database-per-tenant makes isolation structural. A query cannot reach another tenant’s data because the connection does not point at it. You get per-tenant restore, per-tenant residency and a clean answer to enterprise security questionnaires. You pay in operations: migrations run N times and can fail partway; connection pooling gets complicated; a thousand small tenants become a genuine cost and cross-tenant analytics stops being a query and starts being a pipeline.
Schema-per-tenant sits between them and inherits from both sides — most of the migration complexity, some of the isolation. It is the right answer less often than its position on the spectrum suggests.
The pragmatic middle path: shared schema by default, with the ability to isolate a specific tenant into its own database. That keeps the common case cheap and gives you an answer when a large customer’s procurement team asks. Build the tenant-resolution layer so the connection is a lookup rather than a constant, and you have kept the door open at almost no cost.
2. The tenant identifier
Whatever appears in URLs and foreign keys is effectively permanent. Use an opaque, immutable identifier and treat the human-readable name as a separate, changeable label.
Concretely: the subdomain, path segment or key a customer sees is a slug, stored as an attribute and changeable — companies rebrand, get acquired, and typo their own name during signup. The identifier your foreign keys reference is separate, opaque and never changes. Conflating them means a rename becomes a data migration and every old link becomes a 404.
Two related traps. Sequential integer tenant IDs leak your customer count to anyone who can read a URL, which is a commercial disclosure rather than a security one, but it is a disclosure you cannot retract. And once slugs are in URLs, you own them forever: decide early whether a released slug can be reused, because reusing one silently hands old inbound links to a different customer.
3. Data residency
If a customer will eventually require their data in a specific region, the ability to run isolated regional deployments has to exist in the architecture from the start. Retrofitting it means re-partitioning live data.
This is the decision most likely to be deferred and most expensive to reverse, because it is not really a database question — it is a question about whether your architecture assumes one home. Global sequences, a single search index, cross-tenant aggregate tables and a background job runner that assumes one queue are all cheap to build and painful to regionalise.
You do not need multi-region on day one. You need to avoid the assumptions that make it impossible: tenant-scoped identifiers rather than global ones, no cross-tenant table that must be globally consistent, and a deployment that is parameterised by region even when there is only one.
Prove isolation with tests
Whatever model you pick, write tests that assert tenant A cannot read tenant B — at the query layer, not the interface. Those tests are the only thing standing between you and the incident that ends the company.
Interface-level tests are not enough, because the leaks do not happen in the interface. They happen in a report query written under deadline, a background job that iterates without a scope, a cache key missing its tenant prefix, a webhook handler that trusts an ID from the payload, or an admin tool built for support that quietly became a production dependency.
Test the layer below: fabricate two tenants, run every repository method as one, assert nothing from the other comes back. Then make it a default that new code inherits — a base query class that requires a scope, a lint rule against raw queries in application code, a CI check that fails on a new table without a tenant column. A test suite catches today’s mistake; a default prevents next year’s.
The one you can change
Almost everything else — pricing model, plan gating, onboarding, where you host, which queue you use, even your database engine — is a migration you can plan and execute. Do not let the reversible decisions consume the attention that the three above deserve.
These are the first three conversations on any SaaS engagement, before a line of product code, and they get written down as decision records — because the reasoning behind a tenancy choice needs to outlive the people who made it.
Sources and further reading
// TAGS
// RELATED POSTS
(02) // LET'S BUILD
START APROJECT
