Why isolation matters
When you start a SaaS product you quickly discover that the database is the biggest source of technical debt. Every piece of business logic depends on how you separate one customer's data from another's. A poor choice can make it hard to add features, can expose data unintentionally, and can turn routine scaling work into a nightmare.
In this article I will walk through two common isolation patterns, show where each shines, and give you a checklist you can use when you sit down with your team.
The two basic patterns
1. Shared schema with a tenant column
The simplest way to support many customers is to keep a single set of tables and add a tenant_id column to every row that needs isolation. Queries always filter on that column, and the application layer is responsible for never mixing tenant data.
Pros
- Easier to set up initially. You only need one set of migrations.
- Lower overhead for small customers because you share indexes and storage.
- Simpler reporting across tenants when you need aggregate views.
Cons
- Every query must include the tenant filter. A missing filter can cause a data leak.
- As the number of tenants grows, the tables become very large, and indexes can degrade.
- Harder to apply tenant‑specific settings such as retention policies.
2. Schema per tenant
PostgreSQL, MySQL and some other databases let you create a separate schema (or database) for each tenant. Each schema contains the same set of tables, but they are physically isolated.
Pros
- Stronger data isolation. Even a buggy query that forgets the tenant filter will only see data in the current schema.
- Easier to customize per‑tenant settings like column defaults, constraints, or even different extensions.
- Allows you to move or backup individual tenants without affecting the whole system.
Cons
- More overhead in the database. Each tenant adds its own set of indexes and metadata.
- Managing migrations becomes more complex because you must apply each change to every schema.
- If you have thousands of tenants, the number of schemas can overwhelm the database's catalog tables.
How to decide
Below is a checklist that helped my team pick a strategy for a SaaS product that started with a handful of customers and grew to a few hundred.
| Question | Shared schema | Schema per tenant |
|---|---|---|
| Expected tenant count | < 200 | > 200 (but still under a few thousand) |
| Need for tenant‑specific DB settings | No | Yes |
| Regulatory isolation requirements | Low | High |
| Operational budget for DB storage | Tight | Flexible |
| Complexity tolerance | Low | Medium |
If you answer "Yes" to any of the high‑risk rows, the schema‑per‑tenant approach is worth the extra operational work.
A hybrid approach that works for many startups
In practice I have seen teams start with a shared schema, then migrate the most demanding tenants to their own schemas. This gives you the cheap start‑up cost while still letting you address performance or compliance concerns later.
Steps to implement the hybrid model
-
Create a tenant metadata table - store
tenant_id,is_isolatedflag, and a reference to the schema name if isolated. - Wrap all data access in a repository layer - the layer reads the metadata and decides whether to set the search_path (PostgreSQL) or use a different connection string.
- Add a background job to monitor table size - when a tenant's row count crosses a threshold (e.g., 5 million rows), flag it for isolation.
- Write a migration script - the script creates a new schema, copies the tenant's data, updates the metadata flag, and then switches the repository to the new schema.
- Test the switch on a staging copy - verify that no queries leak data and that performance improves.
Concrete takeaway
When you are building the first version of a SaaS product, start with a shared‑schema design only if you expect fewer than a few hundred tenants and you have no strict compliance isolation needs. Keep the code that selects the tenant ID in a single place, and write automated tests that fail if a query runs without that filter. As soon as you notice a tenant hitting size or performance limits, move that tenant to its own schema using the migration steps above. This incremental path gives you low initial cost and a clear roadmap for scaling without rewriting the whole data layer.
TL;DR
- Shared schema = cheap, simple, but relies on disciplined code.
- Schema per tenant = stronger isolation, more customizability, higher operational overhead.
- Use a metadata‑driven hybrid model to get the best of both worlds.
- Put tenant selection logic in one place and protect it with tests.
- When a tenant grows large or requires special settings, migrate it to its own schema.
Following this approach lets you focus on building features instead of fighting data‑leak bugs or painful migrations later on.
Top comments (0)