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 tenants table

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:456

Prevents 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.