Softechinfra
Development

SaaS Architecture Patterns: Multi-Tenant System Design

Comprehensive guide to multi-tenant SaaS architecture—database patterns, isolation strategies, and scaling approaches.

Hrishikesh BaidyaHrishikesh Baidya
July 22, 202514 min read
SaaS Architecture Patterns: Multi-Tenant System Design

Building a multi-tenant SaaS application requires fundamental architecture decisions that impact scalability, security, and cost. As Hrishikesh Baidya, our CTO, has designed dozens of SaaS platforms through our web development services, the patterns covered here have been battle-tested across diverse use cases.

70%
Cost Reduction with Multi-Tenant
3
Database Isolation Options
99.9%
Target Uptime SLA
∞
Horizontal Scale Potential

Multi-Tenancy Models

The first decision in SaaS architecture is choosing your tenancy model. Each has distinct trade-offs.

🏢
Single-Tenant
Dedicated resources per customer—maximum isolation, higher cost, ideal for enterprise
🏘️
Multi-Tenant Shared
Shared infrastructure with logical separation—cost efficient, complex security
🔀
Hybrid Model
Mix of approaches by tier—flexibility for different customer segments

Choosing Your Model

Factor Single-Tenant Multi-Tenant Hybrid
Cost per tenant High Low Variable
Data isolation Complete Logical Tiered
Customization Unlimited Limited Tier-based
Operational complexity High (many instances) Medium (shared) Highest
Best for Enterprise SMB/Self-service Mixed market

Database Architecture Patterns

Database design is the most critical multi-tenancy decision. Choose based on isolation requirements, compliance needs, and scale targets.

Pattern 1: Shared Database, Shared Schema

All tenants share tables, distinguished by tenant_id column.

sql
CREATE TABLE users (
      id UUID PRIMARY KEY,
      tenant_id UUID NOT NULL,
      email VARCHAR(255) NOT NULL,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      INDEX idx_tenant (tenant_id)
    );
    
    -- CRITICAL: Always filter by tenant_id
    SELECT * FROM users WHERE tenant_id = ? AND email = ?;
Row-Level Security: Use PostgreSQL RLS or similar database features to enforce tenant isolation at the database level. This provides defense-in-depth beyond application code.

PostgreSQL RLS example:

sql
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
    
    CREATE POLICY tenant_isolation ON users
      USING (tenant_id = current_setting('app.current_tenant')::uuid);

Pattern 2: Shared Database, Separate Schemas

Each tenant gets their own schema within a shared database.

sql
-- Create schema per tenant
    CREATE SCHEMA tenant_abc;
    CREATE TABLE tenant_abc.users (...);
    
    -- tenant_xyz gets separate schema
    CREATE SCHEMA tenant_xyz;
    CREATE TABLE tenant_xyz.users (...);
    Benefits:
  • Stronger isolation than shared schema
  • Easier per-tenant backup/restore
  • Simpler data migration between tenants

Pattern 3: Separate Databases

Maximum isolation—each tenant gets a dedicated database.

code
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
    │  tenant_abc_db  │  │  tenant_xyz_db  │  │  tenant_123_db  │
    │                 │  │                 │  │                 │
    │  users          │  │  users          │  │  users          │
    │  orders         │  │  orders         │  │  orders         │
    │  products       │  │  products       │  │  products       │
    └─────────────────┘  └─────────────────┘  └─────────────────┘
Connection Pool Management: Separate databases require careful connection pool management. Use connection poolers like PgBouncer to prevent connection exhaustion as tenant count grows.

Application Layer Implementation

Tenant Resolution Strategies

🌐
Subdomain
📁
URL Path
📋
Header
🔑
JWT Claim
    Resolution approaches:
  • Subdomain: acme.yourapp.com → tenant: acme
  • Path: yourapp.com/acme/dashboard → tenant: acme
  • Header: X-Tenant-ID: acme (API-first products)
  • JWT claim: Tenant embedded in authentication token

Request Context Pattern

typescript
import { AsyncLocalStorage } from 'async_hooks';
    
    interface Tenant {
      id: string;
      slug: string;
      plan: 'free' | 'pro' | 'enterprise';
      databaseUrl?: string;
    }
    
    class TenantContext {
      private static storage = new AsyncLocalStorage<Tenant>();
    
      static getCurrentTenant(): Tenant {
        const tenant = this.storage.getStore();
        if (!tenant) {
          throw new Error('No tenant context - middleware not configured');
        }
        return tenant;
      }
    
      static run<T>(tenant: Tenant, fn: () => T): T {
        return this.storage.run(tenant, fn);
      }
    }
    
    // Express middleware
    const tenantMiddleware = async (req, res, next) => {
      const tenantSlug = req.subdomains[0] || 'default';
      const tenant = await resolveTenant(tenantSlug);
    
      if (!tenant) {
        return res.status(404).json({ error: 'Tenant not found' });
      }
    
      TenantContext.run(tenant, () => next());
    };

Secure Data Access Layer

"The most common SaaS security vulnerability is missing tenant filters in database queries. Every query must include tenant context—enforce this at the repository layer, not individual queries."
HB
Hrishikesh Baidya CTO, Softechinfra
typescript
abstract class TenantScopedRepository<T> {
      protected abstract tableName: string;
    
      private get tenantId(): string {
        return TenantContext.getCurrentTenant().id;
      }
    
      async findAll(conditions?: Partial<T>): Promise<T[]> {
        // Tenant filter is ALWAYS applied automatically
        return this.db.query(
          `SELECT * FROM ${this.tableName} WHERE tenant_id = $1`,
          [this.tenantId]
        );
      }
    
      async create(data: Omit<T, 'id' | 'tenant_id'>): Promise<T> {
        // Tenant ID is ALWAYS injected automatically
        return this.db.query(
          `INSERT INTO ${this.tableName} (tenant_id, ...) VALUES ($1, ...)`,
          [this.tenantId, ...]
        );
      }
    }

Isolation Strategies

Compute Isolation Options

1
Shared Compute (Default)
All tenants share application instances. Most cost-effective. Use resource quotas to prevent noisy neighbors.
2
Namespace Isolation
Kubernetes namespaces per tenant with resource limits. Good balance of isolation and efficiency. See our Kubernetes guide.
3
Dedicated Compute
Separate clusters/instances for premium tenants. Maximum isolation. Higher cost but enables compliance requirements.

Data Isolation Hierarchy

From least to most isolated:

  • 1. Application-enforced - Code filters by tenant_id
  • 2. Row-level security - Database enforces tenant filter
  • 3. Schema separation - Separate tables per tenant
  • 4. Database separation - Separate databases per tenant
  • 5. Infrastructure separation - Dedicated cloud accounts
  • Scaling Patterns

    Horizontal Scaling Architecture

    code
    ┌─────────────────┐
                            │  Load Balancer  │
                            └────────┬────────┘
                                     │
                ┌────────────────────┼────────────────────┐
                │                    │                    │
                ▼                    ▼                    ▼
        ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
        │   App Pod 1   │  │   App Pod 2   │  │   App Pod N   │
        │  (Stateless)  │  │  (Stateless)  │  │  (Stateless)  │
        └───────┬───────┘  └───────┬───────┘  └───────┬───────┘
                │                  │                  │
                └──────────────────┴──────────────────┘
                                   │
                            ┌──────┴──────┐
                            ▼             ▼
                     ┌──────────┐  ┌───────────┐
                     │  Primary │  │  Replica  │
                     │    DB    │──│    DB     │
                     └──────────┘  └───────────┘
      Key principles:
    • Stateless application tier (session in Redis/database)
    • Horizontal pod autoscaling based on load
    • Database read replicas for query distribution

    Tenant Sharding

    For large scale, shard tenants across database clusters:

    typescript
    interface ShardConfig {
          id: number;
          connectionUrl: string;
          tenantRange: [number, number];
        }
        
        function getShardForTenant(tenantId: string): ShardConfig {
          const hash = consistentHash(tenantId);
          const shardId = hash % totalShards;
          return shardConfigs.find(s => s.id === shardId);
        }
        
        // Route tenant to correct shard
        const tenant = await TenantContext.getCurrentTenant();
        const shard = getShardForTenant(tenant.id);
        const db = getConnection(shard.connectionUrl);

    Tiered Infrastructure

    🆓
    Free Tier
    Shared compute, shared database, rate-limited, noisy neighbor possible
    💼
    Pro Tier
    Shared compute with priority, dedicated schema, better limits
    🏛️
    Enterprise Tier
    Dedicated compute, dedicated database, custom SLAs, compliance options

    Security Checklist

    • Tenant ID verified on every request (middleware)
    • No cross-tenant data access possible (test with penetration testing)
    • Audit logging for sensitive operations
    • Encryption at rest (database-level or application-level)
    • Encryption in transit (TLS everywhere)
    • Per-tenant encryption keys for enterprise (optional)
    • Data residency compliance (store data in required regions)
    • Right to deletion workflow implemented

    Operational Considerations

    Per-Tenant Monitoring

    Track metrics at the tenant level for debugging and capacity planning:

  • Resource usage - CPU, memory, storage per tenant
  • Error rates - Isolate problematic tenants
  • Performance - P95 latency per tenant
  • Cost allocation - For usage-based billing
  • Tenant Lifecycle

    📝
    Signup
    ⚙️
    Provision
    ✅
    Active
    📤
    Export
    🗑️
    Delete
      Automate the entire lifecycle:
    • Provisioning - Database setup, initial data, DNS configuration
    • Onboarding - Welcome emails, sample data, getting started guides
    • Offboarding - Data export, grace period, secure deletion

    Real-World Example

    Teamstead uses a hybrid approach: shared database with schema separation for standard customers, and dedicated databases for enterprise customers with compliance requirements. This balances cost efficiency with enterprise security needs.

    Building a SaaS Platform?

    We help companies design and implement scalable multi-tenant architectures. From early-stage startups to enterprise platforms, we've built SaaS systems that scale.

    Discuss Your Architecture
    Tags:
    SaaSArchitectureMulti-TenantBackendDatabase Design
    Share this post:
    Hrishikesh Baidya

    Hrishikesh Baidya

    CTO at Softechinfra specializing in Python, system architecture, and building secure, scalable software solutions.