Back to API Gateway
Q9Question 9 of 16

How to implement multi-tenancy in an API Gateway

Key Concepts
  • Multi-tenancy serves many independent customers (tenants) from one shared platform while keeping data and resources isolated.
  • Tenant identification must happen early at the gateway, ideally derived from a signed JWT claim, not a spoofable header.
  • The resolved tenant is propagated downstream as a trusted header (X-Tenant-Id) so services scope data access correctly.
  • Per-tenant rate limits and quotas solve the noisy-neighbor problem: one tenant's traffic surge cannot starve another.
  • Per-tenant routing can send different tenants to different backends for gradual rollouts or premium infrastructure.
  • Data isolation ranges from database-per-tenant (strongest) to shared schema with tenant-ID column (most efficient).
  • The worst multi-tenant bug is cross-tenant data leakage; the tenant check must be mandatory and fail closed.

Definition

Identify the tenant on every request, stamp and propagate a verified tenant context, and enforce per-tenant routing, limits, quotas, and data isolation so tenants share the platform but never each other's data or capacity.
  • Multi-tenancy – serving many independent customers (tenants) from one shared platform while keeping each tenant's data, configuration, and resources isolated.
  • Tenant identification – resolving the tenant from the JWT claim, subdomain, path prefix, or header on every request at the gateway.
  • Tenant propagation – attaching the verified tenant ID to the request context as a trusted header for downstream services.
  • Per-tenant traffic management – rate limits and quotas scoped to the tenant to prevent the noisy-neighbor problem.
  • Data isolation – database-per-tenant (strongest), schema-per-tenant, or shared schema with tenant-ID column (most efficient).
  • Fail-closed rule – a request with no resolvable tenant is rejected, never defaulted to some tenant.

Imagine National Bank of India runs a shared platform for several partner banks, each a separate tenant. It is like one big office building where each partner rents its own floor. The security desk at the entrance (the gateway) looks at each visitor's pass to see which bank they belong to, stamps their file with that bank's name, and makes sure they are only sent to their own floor, with their own limits on how many visitors they can bring. One partner's rush of customers can never spill onto another's floor.

Understanding the Concepts

How do you identify the tenant?

Tenant identification must be reliable and happen early. The JWT claim is the strongest source because it is signed and cannot be forged.

JAVA
package com.bank.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class TenantResolutionFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Derive tenant from JWT claim (signed, cannot be forged)
        String jwtTenant = extractTenantFromJwt(exchange);
        if (jwtTenant == null) {
            // Fail closed: no tenant → reject
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // Check if client-supplied header matches JWT
        String headerTenant = exchange.getRequest().getHeaders()
                .getFirst("X-Tenant-Id");
        if (headerTenant != null && !headerTenant.equals(jwtTenant)) {
            // Spoof attempt: header says one tenant, JWT says another
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // Stamp verified tenant on the request
        exchange.getRequest().mutate()
                .header("X-Tenant-Id", jwtTenant);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -90; // Run early, after auth
    }

    private String extractTenantFromJwt(ServerWebExchange exchange) {
        // Extract "tenant" claim from validated JWT
        return null; // implementation
    }
}
DIAGRAM
// JWT claim: {sub: "user@partnerbank-a.com", tenant: "PARTNER-A"}
// Header: X-Tenant-Id: PARTNER-A → matches JWT → allowed
// Header: X-Tenant-Id: PARTNER-B → does NOT match JWT → REJECTED (spoof!)
// No tenant in JWT → REJECTED (fail closed)

How do you propagate the tenant downstream?

The gateway injects the verified tenant as a trusted header. Downstream services read it to scope their data access.

JAVA
// Gateway sets: X-Tenant-Id: PARTNER-A (verified from JWT)
// Account Service reads:
String tenantId = request.getHeader("X-Tenant-Id");
List<Account> accounts = accountRepo.findByTenantId(tenantId);
// Only returns PARTNER-A's accounts, never PARTNER-B's

How do per-tenant rate limits solve the noisy-neighbor problem?

Rate limits scoped to the tenant ensure one tenant's traffic surge cannot starve another.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

@Configuration
public class TenantRateLimitConfig {

    @Bean
    public KeyResolver tenantKeyResolver() {
        // Rate limit per tenant, not per user
        return exchange -> Mono.justOrEmpty(
                exchange.getRequest().getHeaders().getFirst("X-Tenant-Id"));
    }
}
YAML
# Per-tenant rate limit configuration
# PARTNER-A (premium): 1000 requests/second
# PARTNER-B (basic): 100 requests/second
spring:
  cloud:
    gateway:
      routes:
        - id: accounts
          uri: lb://account-service
          predicates:
            - Path=/api/v1/accounts/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@tenantKeyResolver}"
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200
DIAGRAM
// PARTNER-A sends 500 req/s: within their premium limit → all processed
// PARTNER-B sends 500 req/s: exceeds basic limit → excess gets 429
// PARTNER-A is unaffected by PARTNER-B's surge (noisy-neighbor solved)

How does per-tenant routing work?

Different tenants can be routed to different backends for gradual rollouts or dedicated infrastructure.

JAVA
// Per-tenant routing:
// PARTNER-A (premium) → dedicated account-service instances
// PARTNER-B (basic) → shared account-service instances

// Route predicate:
// .route(r -> r
//     .path("/api/v1/accounts/**")
//     .and().header("X-Tenant-Id", "PARTNER-A")
//     .uri("lb://account-service-premium"))
// .route(r -> r
//     .path("/api/v1/accounts/**")
//     .uri("lb://account-service-shared"))

What are the data isolation models?

The gateway enables data isolation, but the data layer enforces it. Three models offer different trade-offs between isolation and cost.

DIAGRAM
// Model 1: Database per tenant (strongest isolation)
// PARTNER-A → partner_a_db (PostgreSQL)
// PARTNER-B → partner_b_db (PostgreSQL)
// Pros: complete isolation, easy compliance, per-tenant backup
// Cons: expensive, many databases to manage

// Model 2: Schema per tenant (medium isolation)
// Shared DB → partner_a schema, partner_b schema
// Pros: single DB, schema-level isolation
// Cons: schema management overhead, shared DB resources

// Model 3: Shared schema with tenant-ID column (most efficient)
// accounts table: id, tenant_id, balance, ...
// WHERE tenant_id = 'PARTNER-A'
// Pros: simplest, cheapest, one DB
// Cons: weakest isolation, must NEVER forget WHERE clause

What is the worst multi-tenant bug?

Cross-tenant data leakage: one tenant sees another tenant's data. The tenant check must be mandatory and fail closed.

JAVA
// WRONG: optional tenant check
public List<Account> getAccounts(String tenantId) {
    if (tenantId != null) {
        return accountRepo.findByTenantId(tenantId);
    }
    return accountRepo.findAll(); // Returns ALL tenants' data!
}

// CORRECT: mandatory tenant check, fail closed
public List<Account> getAccounts(String tenantId) {
    if (tenantId == null || tenantId.isBlank()) {
        throw new TenantRequiredException("Tenant ID is mandatory");
    }
    return accountRepo.findByTenantId(tenantId);
}

How do you tag logs and metrics per tenant?

Tenants need separately attributable logs and metrics for billing, debugging, and alerting.

JAVA
// Every log and metric tagged with tenant:
// log.info("Balance query for account {} tenant {}", accountId, tenantId);
// Metrics: gateway.requests{tenant=PARTNER-A, status=200} count=1500
// Metrics: gateway.requests{tenant=PARTNER-B, status=429} count=50

// Per-tenant billing:
// PARTNER-A: 1.5M requests this month → invoice based on plan
// PARTNER-B: 800K requests → different invoice

What should a senior engineer know?

A senior engineer resolves the tenant at the edge precisely so no downstream service has to re-derive it inconsistently. They treat cross-tenant leakage as a showstopper bug with the same severity as a security breach.

DIAGRAM
// Senior multi-tenancy checklist:
// 1. Tenant from JWT claim (signed, not spoofable)
// 2. Fail closed: no tenant → reject
// 3. Per-tenant rate limits keyed on tenant ID in Redis
// 4. Per-tenant routing for premium vs shared infrastructure
// 5. Data isolation model chosen per compliance requirement
// 6. Logs and metrics tagged with tenant for billing and debugging
// 7. Cross-tenant leakage treated as P0 security incident

Visual Overview

DIAGRAM
  Multi-Tenancy Gateway Flow
  ──────────────────────────
  Client (PARTNER-A)
       │
       ▼
  ┌──────────────────────────────────┐
  │          API Gateway              │
  │                                   │
  │  1. Auth: validate JWT            │
  │  2. Tenant: extract from JWT      │
  │     claim → "PARTNER-A"           │
  │  3. Rate limit: check PARTNER-A   │
  │     budget (1000 req/s)           │
  │  4. Route: PARTNER-A → premium    │
  │     backend                       │
  │  5. Inject: X-Tenant-Id header    │
  └──────────┬───────────────────────┘
             │
             ▼
  Account Service (premium)
  WHERE tenant_id = 'PARTNER-A'
  → returns only PARTNER-A data

Key Takeaway

Multi-tenancy serves many independent customers from one shared platform while keeping each tenant's data, configuration, and resource usage strictly isolated. The gateway is where this begins, because it is the single place every request passes through and the natural authority for identifying the tenant and stamping it onto the request.

At National Bank of India, the platform serves several partner banks as tenants. The gateway derives the tenant from a signed JWT claim, never from a spoofable header, and injects the verified tenant ID as a trusted header for downstream services. When a request arrives with a header claiming one tenant but a token belonging to another, the gateway rejects it as a spoof attempt. Per-tenant rate limits scoped in Redis solve the noisy-neighbor problem: when one partner's app went viral, the per-tenant limiter only counted their traffic against their budget, and other partners kept running normally.

Data isolation depends on the trade-off between isolation and cost. Database-per-tenant gives the strongest separation. Shared schema with a tenant-ID column is the most efficient. The gateway guarantees the tenant context is present and trustworthy so the data layer applies the correct filter. The worst possible bug in a multi-tenant system is cross-tenant data leakage, so the tenant check is mandatory and fails closed.

Remember one sentence: resolve the tenant from a signed JWT claim at the gateway, propagate it as a trusted header, and enforce per-tenant rate limits, routing, and data isolation so tenants share the platform but never each other's data or capacity.

---

Share
Use ← → arrow keys to navigate