Back to Blog
Architecture11 min read

SaaS Architecture Patterns: Building Scalable Multi-tenant Systems

Explore proven architecture patterns for building successful SaaS products that scale.

February 19, 2024
#SaaS#Architecture#Microservices

Multi-Tenancy Approaches


1. Database per Tenant

Each tenant gets their own database - highest isolation but most expensive.

2. Schema per Tenant

Shared database, separate schemas - good isolation with shared resources.

3. Shared Database, Shared Schema

All tenants in one table with tenant_id - most cost-effective, requires careful query building.

Recommended Architecture


┌─────────────────────────────────────────────┐

│ Load Balancer │

└─────────────────────────────────────────────┘

┌─────────────┼─────────────┐

▼ ▼ ▼

┌─────────┐ ┌─────────┐ ┌─────────┐

│ API GW │ │ API GW │ │ API GW │

└─────────┘ └─────────┘ └─────────┘

│ │ │

┌────┴────┐ ┌────┴────┐ ┌────┴────┐

│ Service │ │ Service │ │ Service │

└─────────┘ └─────────┘ └─────────┘

│ │ │

┌────┴────┐ ┌────┴────┐ ┌────┴────┐

│ Database│ │ Database│ │ Database│

└─────────┘ └─────────┘ └─────────┘

Key Components


API Gateway

  • Rate limiting per tenant
  • Authentication/Authorization
  • Request routing
  • Request/response transformation
  • Service Layer

  • Business logic isolation
  • Independent scaling
  • Technology flexibility
  • Data Layer

  • Connection pooling
  • Tenant context in queries
  • Backup strategies per tenant
  • Implementing Tenant Isolation


    // Middleware to extract tenant ID

    export function withTenant(handler: NextApiHandler) {

    return async (req: NextRequest) => {

    const tenantId = req.headers.get('x-tenant-id');


    if (!tenantId) {

    return new Response('Tenant required', { status: 401 });

    }


    // Set tenant context

    setTenantId(tenantId);


    return handler(req);

    };

    }


    // Query with tenant isolation

    const getUser = async (userId: string) => {

    const tenantId = getTenantId();

    return db.query(

    'SELECT * FROM users WHERE id = $1 AND tenant_id = $2',

    [userId, tenantId]

    );

    };

    Scaling Considerations


  • **Horizontal scaling**: Add more instances
  • **Database read replicas**: For read-heavy apps
  • **Caching**: Redis for session and query caching
  • **Async processing**: Message queues for background jobs
  • Conclusion


    SaaS architecture requires balancing cost, isolation, and scalability. Start simple and evolve as you understand your tenants' needs.

    ┌─────────────────────────────────────────────┐ │ Load Balancer │ └─────────────────────────────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ API GW │ │ API GW │ │ API GW │ └─────────┘ └─────────┘ └─────────┘ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ Service │ │ Service │ │ Service │ └─────────┘ └─────────┘ └─────────┘ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ Database│ │ Database│ │ Database│ └─────────┘ └─────────┘ └─────────┘
    // Middleware to extract tenant ID export function withTenant(handler: NextApiHandler) { return async (req: NextRequest) => { const tenantId = req.headers.get('x-tenant-id'); if (!tenantId) { return new Response('Tenant required', { status: 401 }); } // Set tenant context setTenantId(tenantId); return handler(req); }; } // Query with tenant isolation const getUser = async (userId: string) => { const tenantId = getTenantId(); return db.query( 'SELECT * FROM users WHERE id = $1 AND tenant_id = $2', [userId, tenantId] ); };

    Need Help with Your Project?

    Our team can help you implement these patterns in your application.

    Get in Touch