Skip to content

Multi-tenancy Patterns#

Problem statement (interviewer prompt)

A SaaS serves tens of thousands of customers from small free-tier to enterprise. Each tenant needs predictable performance, data isolation, and a billing trail. Compare the architectural patterns for sharing infrastructure and pick one per tier.

Three core models trade isolation for cost:

flowchart LR
  subgraph Silo[Silo: stack per tenant]
    T1([T1]) --> S1[App1] --> D1[(DB1)]
    T2([T2]) --> S2[App2] --> D2[(DB2)]
  end
  subgraph Pool[Pool: shared stack]
    T3([T3]) --> SA[App]
    T4([T4]) --> SA
    SA --> DA[(DB w/ tenant_id col)]
  end
  subgraph Bridge[Bridge: shared compute, isolated DB]
    T5([T5]) --> SB[App]
    T6([T6]) --> SB
    SB --> D5[(DB5)]
    SB --> D6[(DB6)]
  end

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    class T1,T2,T3,T4,T5,T6 client;
    class S1,S2,SA,SB service;
    class D1,D2,DA,D5,D6 datastore;

Bigger tenants get more isolation (silo); small tenants share (pool). Bridge is the common middle ground.

Multi-tenancy is the architectural axis "how much of my stack is shared between customers?" The answer drives cost, isolation, security, and operational complexity.

The three models#

Silo (one stack per tenant)#

flowchart TB
  T([Tenant]) --> LB[Tenant LB] --> S[App] --> D[(DB)]
  S --> C[(Cache)]
  S --> Q[(Queue)]

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
  • Isolation: maximum. No noisy neighbours.
  • Cost: highest. Fixed overhead per tenant.
  • Compliance: easiest. Per-tenant encryption keys, audit logs, data residency.
  • Use: regulated enterprise (banking, healthcare), top-tier customers.

Pool (single shared stack)#

flowchart LR
  T1([T1]) --> LB
  T2([T2]) --> LB
  LB --> S[App<br/>tenant_id in every query]
  S --> D[(Single DB<br/>row-level security)]

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
  • Isolation: weakest. Bug = cross-tenant leak risk.
  • Cost: lowest. Resources amortized.
  • Compliance: hardest. Row-level security, signed JWT context.
  • Use: free / freemium / SMB tiers, B2C apps.

Bridge (pooled compute, siloed DB)#

flowchart LR
  T1([T1]) --> S[Shared app]
  T2([T2]) --> S
  S -->|routes by tenant_id| D1[(DB T1)]
  S -->|routes by tenant_id| D2[(DB T2)]

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
  • Isolation: data-layer isolated, compute shared.
  • Cost: middle.
  • Compliance: per-tenant encryption keys at rest are possible.
  • Use: mid-market, most modern SaaS default.

Data isolation techniques in pool mode#

Technique Mechanism
tenant_id column Every query has WHERE tenant_id = ?
Row-level security (PostgreSQL RLS) DB enforces predicate; app can't bypass
Schema-per-tenant Each tenant gets own schema in same DB
Database-per-tenant Each tenant gets own DB instance (= bridge)
Hybrid (shards of pools) Pool inside cells; see cell-based-architecture

Noisy-neighbour mitigation#

flowchart LR
  T([Heavy tenant]) -->|10k QPS| QoS[Tenant-aware rate limiter]
  QoS -->|throttled to 100 QPS| App[Shared app]

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
  • Per-tenant rate limits at the edge.
  • Bulkheads: per-tenant connection pools, thread pools.
  • Resource quotas: max CPU, memory, queue depth per tenant.
  • Cell-based isolation: shuffle-shard so noisy tenant blast radius is bounded.

Identity and tenant context#

sequenceDiagram
  participant U as User
  participant Auth as Identity provider
  participant API as API gateway
  participant App as Service
  U->>Auth: login
  Auth-->>U: JWT (sub, tenant_id, role)
  U->>API: GET /resource (Bearer JWT)
  API->>App: forward + verify JWT
  App->>App: extract tenant_id, bind to DB session

Every layer must verify and propagate tenant identity. A missing tenant filter is the most common multi-tenant bug.

Migration paths#

From To Reason
Pool → Bridge Compliance ask Per-tenant key, audit log
Bridge → Silo Enterprise upsell Dedicated stack pricing
Silo → Bridge Cost rationalisation Long tail of small enterprises
Pool → Pool of cells Blast-radius limit One bug doesn't down all

How multi-tenancy composes#

flowchart TB
  MT((Multi-tenancy<br/>silo / pool / bridge))
  CB[Cell-based architecture<br/>physical isolation unit]
  DBS[Database sharding<br/>pool-mode partitioning]
  SEC[Security fundamentals<br/>per-tenant isolation]
  RL[Rate limiter<br/>per-tenant throttling]
  MT --> CB
  MT --> DBS
  MT --> SEC
  MT --> RL

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class MT service;
    class CB,DBS,SEC,RL datastore;

Glossary & fundamentals#

Tag Concept What it is Page
HLD Cell-based architecture physical isolation unit cell-based-architecture
HLD Database sharding within-pool data partitioning database-sharding
HLD Security fundamentals tenant isolation guarantees security-fundamentals
HLD Rate limiter per-tenant throttling rate-limiter

Quick reference#

Three models#

Model Cost Isolation Use
Silo High Maximum Regulated, enterprise
Pool Low Weak (logical only) Free / SMB
Bridge Middle DB-isolated, compute pooled Mid-market

Data isolation in pool#

  • tenant_id column on every row
  • PostgreSQL Row-Level Security
  • Schema-per-tenant
  • DB-per-tenant
  • Cells of pools

Noisy-neighbour controls#

  • Per-tenant rate limits (token bucket)
  • Per-tenant bulkhead (connection pool)
  • Resource quotas (CPU, memory, queue depth)
  • Cell or shuffle-shard isolation

Identity propagation#

  • JWT with tenant_id claim
  • Every layer verifies
  • DB session sets current_tenant_id
  • Missing tenant filter = the most common bug

Migration paths#

  • Pool → Bridge: per-tenant key, audit
  • Bridge → Silo: dedicated for enterprise
  • Pool → Pool-of-cells: blast-radius bound

Refs#

  • AWS SaaS Lens
  • Salesforce multi-tenant architecture
  • PostgreSQL RLS docs

FAQ#

What is multi-tenancy?#

Multi-tenancy is when a single software stack serves many isolated customers. The architecture decides how much each tenant shares: hardware, database, schema, or rows.

Silo vs pool vs bridge: which model fits?#

Silo gives one stack per tenant, max isolation and cost. Pool shares everything by tenant_id, cheapest. Bridge shares compute but keeps databases separate per tenant.

What is the noisy neighbour problem?#

A heavy tenant slows or breaks others on shared infra. Mitigations include per-tenant rate limits, resource quotas, isolated worker pools, and tiered cell architectures.

How do you isolate tenant data in a pool?#

Add tenant_id to every row, force it into every query at the ORM layer, and back it up with database row-level security so a bad query cannot leak across tenants.

Should free and enterprise tenants share infra?#

Often no. Most teams pool free and small customers for cost, then silo enterprise customers for isolation, SLA control, and compliance. Bridge sits between for mid-tier.

Further reading#