Multi-tenancy is one of the first architectural decisions for any SaaS product. Pick wrong and migration later is painful. This article covers the three pure models, their trade-offs, and the hybrid that most mature SaaS companies end up with.
The three models
Shared everything. One database, one schema, tenant ID on every row. Query every table with WHERE tenant_id = ?. Simplest operationally.
Schema per tenant. One DB instance, one schema per tenant. Postgres schemas or MySQL databases. Some isolation; still share the server.
Instance per tenant. Dedicated DB per tenant. Maximum isolation; maximum operational cost.
Each has specific strengths. Let’s look at trade-offs.
Shared everything
Pros:
- Simplest to operate (one DB to monitor, back up, patch)
- Lowest cost per tenant
- Easy to run cross-tenant queries (reports, analytics)
- New tenants = just a new row in
tenantstable
Cons:
- Per-tenant scaling impossible (big tenant affects small ones)
- Security requires discipline — every query needs
tenant_id; bugs = data leak - Per-tenant customization limited (same schema for everyone)
- Noisy neighbor problem (one tenant’s batch job slows others)
- Hard to do per-tenant backups / restores
Right for:
- Early-stage SaaS, lots of small tenants
- B2C products
- Products with uniform usage patterns across tenants
Schema per tenant
Pros:
- Tenant data physically separated (easier to convince security / auditors)
- Per-tenant schema evolution possible (rare but useful)
- Per-tenant backup / restore
- Still operationally simple — one DB instance
Cons:
- Schema migrations complex (apply to N schemas)
- Cross-tenant queries painful (union across schemas)
- Connection pool complexity (per-schema search path)
- Scale ceiling (thousands of schemas in one Postgres becomes slow)
Right for:
- Mid-market SaaS with 10s-1000s of tenants
- Products where tenants have regulatory isolation requirements
Instance per tenant
Pros:
- Full isolation (compute, storage, network per tenant)
- Per-tenant upgrades, backups, restores
- Noisy neighbor eliminated
- Per-tenant scaling
- Highest security posture
Cons:
- Highest operational cost (N DBs to manage)
- Per-tenant provisioning complexity
- Wasted capacity on small tenants
- Migrations happen N times
Right for:
- Enterprise SaaS (few large tenants, regulatory requirements)
- Products with very different per-tenant scale
- Deployments where tenants pay for dedicated infrastructure
The hybrid that usually wins
For maturing SaaS, the pure models rarely suffice. A common pattern:
- Tier 1 (small tenants): shared everything in a “commons” DB
- Tier 2 (large tenants): schema per tenant in a shared DB cluster
- Tier 3 (enterprise): dedicated instance per tenant
Tenants migrate up as they grow. New tenants start in tier 1. Large tenants get upgraded (and pay more for it).
This lets you amortize operational cost across tiers while offering genuine isolation where it matters.
Application-layer patterns
Regardless of DB model, application code needs tenancy-aware behavior:
Tenant context per request
Middleware sets tenant from auth token:
@Component
public class TenantContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
String tenantId = resolveTenantFromAuth(req);
TenantContext.setCurrentTenant(tenantId);
try {
chain.doFilter(req, res);
} finally {
TenantContext.clear();
}
}
}Every DB query then implicitly filters on tenant. Combining with Hibernate filters or a custom JPA interceptor automates this.
Tenant-aware caching
Cache keys include tenant ID:
cache:tenant:abc-123:product:456Prevents cross-tenant cache leaks. Critical.
Tenant-aware connection pool
For schema-per-tenant or instance-per-tenant, different pools per tenant. Router datasource that picks the right pool based on tenant context.
Data isolation at the query layer
For shared-everything: every table has tenant_id. Every query includes it. But developers forget. Defense in depth:
Row-level security (Postgres):
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::uuid);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;Now forgotten WHERE tenant_id = ? isn’t a security hole — Postgres enforces it. Strong defense.
ORM-level filters. Hibernate @Filter, JPA listeners that add tenant clauses.
The cross-tenant admin case
Superadmin tools (support UI, reports, migrations) need to query across tenants. Usually:
- Separate admin context that bypasses tenant filters
- Separate connection / read-only replica for analytics
- Explicit “I’m looking at tenant X now” UI for support
Migration pain
Schema changes in multi-tenant systems:
- Shared everything: one migration, runs once. Easy but affects all tenants simultaneously.
- Schema per tenant: loop over tenants, apply each. Slow at scale; handle failures per-tenant.
- Instance per tenant: even slower; staggered rollouts.
Design migrations to be:
- Backward-compatible (add columns as nullable; phase deprecation)
- Runnable incrementally without downtime
- Idempotent (can run twice safely)
Closing note
Multi-tenancy choice shapes every architectural decision that follows. The pure models are conceptually clean but rarely serve a growing SaaS for long. Plan for the tiered hybrid from the start — even if you launch with shared-everything, design your code so tiering in large tenants later isn’t a rewrite. Tenant context in the app layer, tenant-aware caching, row-level security — these things are easy to build in from day one, expensive to retrofit after you have thousands of tenants.