Multi-Tenant Architecture Patterns

The Production Nightmare: When Tenant A Saw Tenant B's Orders

It was a Friday afternoon (of course it was). I was about to leave for the weekend when Slack lit up. A restaurant manager reported seeing orders from a completely different restaurant in their dashboard. Then another report came in. Then another.

My blood ran cold. This wasn't a UI glitch—this was a catastrophic multi-tenant data leak. Restaurant A could see Restaurant B's orders, revenue data, customer information. Everything.

The root cause? One missing line of code:

# What I wrote (WRONG):
@app.get("/orders")
async def get_orders(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    orders = db.query(Order).offset(skip).limit(limit).all()
    return orders

# What I should have written:
@app.get("/orders")
async def get_orders(
    skip: int = 0, 
    limit: int = 100,
    tenant_id: str = Depends(get_tenant_id),
    db: Session = Depends(get_db)
):
    orders = db.query(Order).filter(Order.tenant_id == tenant_id).offset(skip).limit(limit).all()
    return orders

One missing filter(Order.tenant_id == tenant_id) exposed every tenant's data to every other tenant. That Friday night, I learned that multi-tenancy isn't a feature—it's a fundamental architectural constraint that must be enforced at every level.

What is Multi-Tenancy?

Multi-tenancy means serving multiple customers (tenants) from a single application instance while keeping their data completely isolated.

In my POS system, I onboarded several types of clients:

  • Tenant 1: A local restaurant client

  • Tenant 2: A fast food chain client

  • Tenant 3: A retail shop client

Each tenant should:

  • See only their own data

  • Be completely unaware other tenants exist

  • Have isolated configuration and customization

  • Never be able to access another tenant's data

Three Multi-Tenant Isolation Strategies

spinner

Let me show you each strategy as implemented in the POS system.

Strategy 1: Separate Database Per Tenant

Pros:

  • Strongest isolation (physically separate data)

  • Easy backup/restore per tenant

  • Can customize schema per tenant

  • Simple compliance (delete tenant = drop database)

Cons:

  • Expensive (one DB instance per tenant)

  • Complex connection management

  • Hard to do cross-tenant analytics

  • Schema migrations run N times

When to use: Enterprise SaaS where tenants pay premium, need guaranteed isolation, or have compliance requirements.

Strategy 2: Shared Database, Separate Schemas

Pros:

  • Good isolation (schema-level separation)

  • Cheaper than separate databases

  • Easier connection pooling

  • Can do cross-tenant queries if needed

Cons:

  • Still complex migrations

  • Need schema switching logic

  • Backup/restore more complex

When to use: Mid-market SaaS with moderate tenant count, need good isolation but not separate databases.

Strategy 3: Shared Database, Shared Tables (Discriminator Column)

Pros:

  • Simplest to implement

  • Best performance (shared connection pool)

  • Easy cross-tenant analytics

  • Cheapest option

Cons:

  • Higher risk of data leaks (my Friday nightmare)

  • Must filter every query by tenant_id

  • Indexes include tenant_id (larger indexes)

This is what I use in the POS system. Here's how to do it safely:

The x-tenant-id Header Flow Through All Services

The tenant ID must flow through every request, to every service, for every operation.

spinner

Enforcing Tenant Isolation at Every Layer

PostgreSQL Row-Level Security (Defense in Depth)

Even with application-level filtering, I added database-level protection:

This saved me multiple times! When I forgot to add tenant filters during rapid development, RLS prevented data leaks.

Redis Cache with Tenant Prefixes

Caching adds another layer where tenant isolation is critical:

Preventing Cross-Tenant Data Leaks: Comprehensive Checklist

Here's my checklist to prevent the nightmare I experienced:

1. Request Level

2. Service Level

3. Repository Level

4. Database Level

5. Cache Level

6. Testing Level

Automated Testing for Tenant Isolation

The Real Bug: How One Tenant Saw Another's Data

Let me share the exact bug that caused the Friday incident:

The chatbot showed aggregated data, so it wasn't immediately obvious. But when a tenant expanded the inventory section, they saw products from other restaurants.

Lesson: When aggregating data from multiple services, verify each service call includes tenant_id.

Performance Considerations

Multi-tenancy adds overhead. Here's how to optimize:

1. Index Strategy

2. Connection Pooling

3. Query Optimization

Key Learnings

  1. Tenant isolation must be enforced at every layer

    • Middleware, services, repositories, database, cache

    • Defense in depth prevents catastrophic leaks

  2. Always verify token tenant matches header tenant

    • Prevents malicious users from accessing other tenants

    • Simple check, massive security benefit

  3. Use PostgreSQL Row-Level Security as safety net

    • Catches bugs before they become data breaches

    • Small performance cost, huge security gain

  4. Prefix all cache keys with tenant_id

    • Easy to forget, easy to exploit

    • Automated testing is critical

  5. Test with multiple tenants from day one

    • Don't add multi-tenancy later

    • Design for it from the start

Common Mistakes

  1. Forgetting tenant filter in queries

    • Use repository pattern to enforce filtering

    • Enable PostgreSQL RLS

  2. Not validating tenant_id from JWT

    • User could send arbitrary tenant_id in header

    • Always verify against JWT claims

  3. Caching data without tenant prefix

    • All tenants share Redis

    • Unprefixed keys leak data

  4. Using separate database without connection pooling strategy

    • Too many connections kill database

    • Need connection pool manager

When to Use Each Strategy

Separate Database:

  • Enterprise customers paying premium

  • Compliance requirements (HIPAA, GDPR)

  • < 100 tenants (connection management gets complex)

Separate Schemas:

  • Need strong isolation

  • Medium tenant count (100-1000)

  • Want to balance cost and isolation

Shared Tables (Discriminator Column):

  • High tenant count (1000+)

  • Need best performance

  • Can enforce isolation at application layer

  • My choice for POS system

Next Steps

Now that you understand multi-tenant architecture, the next article dives into service layer architecture—how to structure your code within each service for maintainability and testability.

We'll cover:

  • Three-layer architecture (Domain, Application, Infrastructure)

  • Dependency injection with FastAPI

  • Repository pattern implementation

  • Why layers matter for testing

Next Article: 04-service-layer-architecture.md - Learn how to structure services so they're testable, maintainable, and ready to scale.


Remember: Multi-tenancy isn't a feature you add later. It's a fundamental architectural constraint. Design for it from day one, test it thoroughly, and never trust yourself to remember that WHERE clause.

Last updated