MÓDULO 07 · CONCEITO 10 DE 12

Multi-tenancy

Silo (DB por tenant), pool (DB compartilhado), hybrid (bridge). Noisy neighbor, tenant isolation, billing implications, compliance. A arquitetura que define se SaaS escala financeiramente — ou se cada cliente novo é prejuízo.

Tempo de leitura ~22 min Pré-requisito Conceitos 02 e 06 (stateless, sharding) Próximo Multi-region e geographic distribution

Em 1999, Marc Benioff fundou a Salesforce com tese ousada: software empresarial entregue como serviço, hospedado por nós, atendendo todos os clientes na mesma infraestrutura. Era contra-cultural — empresas em 1999 esperavam software instalado on-premise, customizado por cliente. Salesforce construiu o que viria a se chamar de multi-tenant SaaS: uma única instância da aplicação, um único banco compartilhado, isolamento lógico por org_id em cada query. Em 2026, esse modelo virou default na indústria de software empresarial.

Multi-tenancy é a propriedade arquitetural de servir múltiplos clientes (tenants) na mesma infraestrutura, com isolamento adequado entre eles. A motivação é econômica: 100 clientes em 100 infraestruturas separadas custa 100×; em 1 infraestrutura compartilhada, custa próximo de 1×. A diferença de custo unitário é o que torna SaaS viável a preços competitivos.

Mas economia compartilhada vem com problema: como garantir que cliente A não afeta cliente B? "Noisy neighbor" — cliente abusivo ( intencionalmente ou por bug) consome recursos compartilhados e degrada experiência de outros — é o problema central. E quando cliente A precisa de isolation regulatória (compliance, soberania de dados), como atender sem perder economia?

Este conceito articula multi-tenancy como decisão arquitetural. Os três modelos canônicos (silo, pool, hybrid), o problema de noisy neighbor e suas mitigations, isolation por camadas (rede, compute, banco, cache), billing implications, e quando considerar abandonar pool por silo. O conceito 11 do módulo (multi-region) extende para distribuição geográfica.

Os três modelos — silo, pool, hybrid

AWS publicou em 2017 um whitepaper canônico ( "SaaS Tenant Isolation Strategies") que articula três estratégias. Em 2026, esse vocabulário virou padrão.

Silo (isolation completo)

Cada tenant tem infraestrutura completamente isolada — sua própria aplicação, seu próprio banco, seu próprio cache. A única coisa compartilhada é o provider cloud e talvez DNS.

Ganhos: isolation máxima (noisy neighbor impossível); compliance fácil (dado de cliente nunca cruza fronteira); customização possível (cliente pode ter versão diferente do software). Custos: caro (custo escala linearmente com tenants); deploy de novo cliente lento (provisionar nova stack); manutenção multiplicada.

Silo é apropriado para poucos clientes grandes: enterprise SaaS premium, sistemas regulados (saúde, governo, financeiro com compliance estrita), clientes que pagam o prêmio do isolamento.

Pool (compartilhamento total)

Todos os tenants compartilham a mesma infraestrutura. Aplicação stateless serve qualquer tenant; banco compartilhado tem coluna tenant_id em cada tabela; cache compartilhado tem chaves prefixadas por tenant. Isolation é lógica, não física.

Ganhos: economia máxima (custos compartilhados); deploy de novo cliente trivial (apenas adicionar registro); manutenção simples (uma versão). Custos: noisy neighbor é problema real; bug em isolation lógica vaza dados entre tenants; compliance mais difícil.

Pool é apropriado para muitos clientes pequenos: SaaS produtividade, ferramentas de colaboração, plataformas onde clientes pequenos são maioria.

Hybrid / bridge model

Combinação. Tenants pequenos compartilham infra (pool); tenants grandes têm infra dedicada (silo). Implementação: aplicação roteia request por tenant tier — cliente premium vai para "silo cluster" dedicado, cliente standard vai para "pool cluster" compartilhado.

Ganhos: economia para massa, isolation para premium; tier-based pricing naturalmente alinhado. Custos: complexidade operacional (dois modos coexistem); migração de pool para silo (cliente cresceu) tem custo.

Padrão dominante em SaaS B2B moderno. AWS, Salesforce, Slack, Microsoft 365 — todos operam híbrido em alguma forma.

Onde isolation acontece — em camadas

Isolation não é binário "isolado vs compartilhado". Há múltiplas camadas, e isolation pode ser feita em algumas e não em outras.

Compute

Aplicação rodando: pode ser instâncias dedicadas por tenant (silo) ou compartilhadas (pool). Em Kubernetes, namespaces por tenant é meio termo — mesmas máquinas físicas, mas isolation lógica em pods, networking, recursos.

Em pool, recursos são compartilhados; resource quotas (limits no K8s) impedem que um workload use 100% da CPU.

Banco

Pool puro: tabela única com tenant_id em toda query. Schema-per-tenant: mesmo banco, schemas diferentes (Postgres SET search_path). Isolamento melhor sem multiplicar instâncias. Database-per-tenant: bancos separados (silo no nível de banco). Instance-per-tenant: instâncias de banco totalmente separadas (silo total).

A escolha tem consequências enormes em backup, migrations, queries cross-tenant, e custo. Pool puro é mais barato e operacionalmente simples, mas exige row-level security para evitar vazamento.

Cache

Redis com prefixo por tenant (tenant:42:user:1) é pool. Redis cluster por tenant é silo.

Em pool, cuidado com hot key — tenant grande pode saturar Redis. Defesa: rate limit por tenant; ou Redis dedicado para tenants grandes (hybrid).

Network

Em sistemas com isolation regulatória, network isolation pode ser exigida. VPC por tenant (silo); subnets dedicadas; firewall rules tenant-specific.

Em SaaS B2C ou B2B comum, network isolation é raro — overhead alto, ganho operacional baixo.

Noisy neighbor — o problema central de pool

Noisy neighbor: tenant que consome recursos desproporcionais e degrada experiência de outros. Aparece em qualquer modelo pool. Causas:

Carga abusiva intencional. Cliente faz scraping massivo; ou lança feature interna que multiplica chamadas a 100×. Sem controle, satura sistema para todos.

Bug do cliente. Loop infinito em integração; fila não consumida acumula. Sistema atende essa carga abusiva não-intencional.

Tamanho desigual. Cliente enterprise com 1M de usuários no mesmo pool de 1000 clientes pequenos. Naturalmente, esse cliente domina recursos.

Distribuição desigual de uso. Cliente faz queries muito mais pesadas que média (sem necessariamente ser maior).

Mitigations canônicas:

Rate limit por tenant. Cada tenant tem cota; quando excede, recebe 429. Conceito 09 cobriu — aqui aplicado por chave tenant.

Resource quotas. Em compute, limites de CPU/memória por tenant (em Kubernetes: ResourceQuota por namespace).

Connection pool por tenant. Banco com pool dedicado por tenant ou tier — cliente grande não esgota pool global.

Tier-based isolation. Tenants premium em silo; standard em pool. Quando standard noisy, premium não é afetado.

Sharding por tenant. Tenants distribuídos entre shards físicos. Noisy tenant afeta apenas seu shard. Conceito 06 do módulo cobriu sharding tenant-based.

Detecção e quarentena. Sistema detecta tenant abusivo (queries lentas, alta taxa); move para "quarentena" (rate limit baixo, ou shard dedicado). Aplicação automática ou via operator.

Tenant identification — passando contexto

Em sistema multi-tenant, identificar qual tenant uma request pertence é operação fundacional. Tipicamente acontece na autenticação (JWT contém tenant_id) e fica em contexto de request — propaga via middleware/interceptor para cada camada.

Em padrão moderno, tenant_id está em três lugares simultâneos:

Header HTTP / claim do JWT. Origem da identificação. Validado na borda (autenticação).

Application context. Após autenticação, tenant_id em HttpContext/request.state/context.Context passa para handlers e repositórios.

Database session. Idealmente, tenant_id está no contexto da sessão de banco para row-level security automática (Postgres RLS). Garante que mesmo bug em código não vaza dado de outro tenant.

Esse cuidado em três camadas previne uma das falhas mais perigosas em multi-tenant: tenant isolation breach. Sem RLS, bug em query ("esqueci o WHERE tenant_id") expõe dados de todos os clientes — incidente catastrófico de privacidade.

Postgres Row-Level Security — defesa estrutural

PostgreSQL tem Row-Level Security (RLS) desde 9.5 (2016). É feature subutilizada e poderosa para multi-tenancy: define policies que automaticamente filtram queries por tenant_id, sem exigir que cada query tenha WHERE tenant_id = ?.

-- ativar RLS na tabela
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;

-- policy: cada tenant só vê seus próprios pedidos
CREATE POLICY tenant_isolation ON pedidos
    USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- na aplicação, cada conexão seta o tenant antes de query
SET app.tenant_id = '...';
SELECT * FROM pedidos;       -- só retorna dessa tenant; sem WHERE explícito
INSERT INTO pedidos (...) VALUES (...);  -- com tenant_id automaticamente

RLS é defesa em profundidade. Mesmo se aplicação tem bug e esquece WHERE, RLS impõe. Mesmo se SQL injection chega ao banco, RLS limita. É a diferença entre "isolation por convenção" e "isolation enforced pelo banco".

Custos: pequeno overhead de performance; complexidade de configurar e debugar; necessidade de setar contexto na conexão a cada request. Para sistemas com requirement de compliance forte, vale o custo.

Billing — multi-tenancy implica metering

Em SaaS, multi-tenancy traz problema correlato: billing. Como medir o que cada tenant consome para cobrar adequadamente? Em silo, é trivial — a infraestrutura é o custo. Em pool, é mais sutil.

Estratégias:

Per-seat. Cobre por usuário ativo. Simples. Pode ser desconectado de uso real (10 usuários que usam pouco custam mais que 1 usuário que abusa).

Per-feature/tier. Plans diferentes (basic, pro, enterprise) com features diferentes. Cliente escolhe seu tier.

Usage-based. Mede uso real (requests, storage, computação). Mais alinhado com custo, mas exige metering preciso. AWS, Stripe, Twilio operam assim.

Hybrid. Plan básico inclui X uso; acima cobra extra. Combina previsibilidade (cliente sabe o piso) com flexibilidade.

Implementar usage-based billing em pool exige metering por tenant em todas as operações relevantes. Counter por API call, bytes processed, storage used. Em sistema com milhões de operações/dia, metering precisa ser eficiente — tipicamente buffered em memória, flushed periodicamente para sistema de billing.

Stripe Metering, Lago, Orb são plataformas que automatizam usage-based billing. Para sistemas sérios, integrar com uma delas economiza meses de desenvolvimento.

Compliance — quando pool não cabe

Algumas regulações requerem isolation que pool não pode oferecer.

GDPR/LGPD com requirement geográfico. Dados de cidadãos europeus devem ficar na Europa. Em pool global, todos os dados misturam. Soluções: sharding geográfico (tenants europeus em região europeia; conceito 11 do módulo); ou silo regional.

HIPAA (saúde, EUA). Exige BAA (Business Associate Agreement) e controles específicos. Pool é possível mas requer auditoria rigorosa; alguns clientes preferem silo para simplicidade de compliance.

SOC 2 / ISO 27001. Requer controles de acesso e auditoria. Pool é viável; a diferença é o esforço de auditoria — em silo, o escopo é menor.

PCI DSS (cartões). Tipicamente requer isolation forte do environment que processa cartão. Mesmo em pool geral, processador de cartão é silo.

Data residency / soberania. Alguns países (Russia, China, India, Brasil em alguns casos) exigem dados nacionais residentes no país. Multi-region por país (silo geográfico) pode ser obrigatório.

Articular requirements de compliance antes de escolher modelo é trabalho de arquiteto. Adicionar compliance depois pode forçar reescrita.

Multi-tenancy em três stacks

Cada ecossistema implementa multi-tenancy de formas idiomáticas.

C# — middleware tenant + EF Core query filter + RLS
// middleware extrai tenant do JWT e armazena em context
public class TenantMiddleware
{
    private readonly RequestDelegate _next;
    public TenantMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext ctx, ITenantContext tenant)
    {
        var tenantId = ctx.User.FindFirst("tenant_id")?.Value;
        if (string.IsNullOrEmpty(tenantId))
        {
            ctx.Response.StatusCode = 403;
            return;
        }
        tenant.SetCurrent(Guid.Parse(tenantId));
        await _next(ctx);
    }
}

// EF Core: global query filter automaticamente filtra por tenant
public class AppDbContext : DbContext
{
    private readonly ITenantContext _tenant;

    public AppDbContext(DbContextOptions opts, ITenantContext tenant) : base(opts)
        => _tenant = tenant;

    protected override void OnModelCreating(ModelBuilder mb)
    {
        // toda entidade tenant-aware ganha filter automático
        mb.Entity<Pedido>().HasQueryFilter(p => p.TenantId == _tenant.Current);
        mb.Entity<Cliente>().HasQueryFilter(c => c.TenantId == _tenant.Current);
    }
}

// adicional: configurar RLS no Postgres + setar contexto na conexão
public class RLSInterceptor : DbConnectionInterceptor
{
    private readonly ITenantContext _tenant;
    public override async Task ConnectionOpenedAsync(...)
    {
        using var cmd = ctx.Connection.CreateCommand();
        cmd.CommandText = $"SET app.tenant_id = '{_tenant.Current}'";
        await cmd.ExecuteNonQueryAsync();
    }
}

Defesa em duas camadas: EF Core global query filter (esquecimento de programador) + Postgres RLS (defesa estrutural). Mesmo bug não vaza dado.

Python — FastAPI + SQLAlchemy + middleware
from fastapi import FastAPI, Depends, HTTPException, Request
from contextvars import ContextVar
from sqlalchemy.orm import Session

# context var armazena tenant atual da request
current_tenant: ContextVar[str | None] = ContextVar("current_tenant", default=None)

app = FastAPI()

# middleware extrai tenant_id do JWT e armazena no context
@app.middleware("http")
async def tenant_middleware(request: Request, call_next):
    token = request.headers.get("Authorization", "").removeprefix("Bearer ")
    payload = jwt.decode(token, KEY, algorithms=["HS256"])
    tenant_id = payload.get("tenant_id")
    if not tenant_id:
        return JSONResponse(status_code=403, content={"error": "missing tenant"})

    token = current_tenant.set(tenant_id)
    try:
        response = await call_next(request)
    finally:
        current_tenant.reset(token)
    return response

# repositório usa current_tenant em queries
async def listar_pedidos(session: Session, status: str):
    tenant_id = current_tenant.get()
    return session.execute(
        select(Pedido)
        .where(Pedido.tenant_id == tenant_id)   # explícito
        .where(Pedido.status == status)
    ).scalars().all()

# alternativa: SQLAlchemy event listener para auto-filter (similar a EF Core)
from sqlalchemy import event

@event.listens_for(Session, "do_orm_execute")
def auto_tenant_filter(orm_execute_state):
    if orm_execute_state.is_select:
        tenant_id = current_tenant.get()
        if tenant_id:
            orm_execute_state.statement = orm_execute_state.statement.where(
                Pedido.tenant_id == tenant_id
            )

# defesa adicional: Postgres RLS
# CREATE POLICY tenant_isolation ON pedidos
#     USING (tenant_id = current_setting('app.tenant_id'))

contextvars propaga tenant_id pelo asyncio context. SQLAlchemy event listener automatiza filter. Postgres RLS como defesa estrutural última.

Go — context.Context + tenant em query
package main

import (
    "context"
    "github.com/jackc/pgx/v5"
)

type tenantKey struct{}

// middleware
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        claims, err := parseJWT(token)
        if err != nil { http.Error(w, "unauthorized", 401); return }

        tenantID, ok := claims["tenant_id"].(string)
        if !ok { http.Error(w, "missing tenant", 403); return }

        ctx := context.WithValue(r.Context(), tenantKey{}, tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func TenantFromContext(ctx context.Context) string {
    if v := ctx.Value(tenantKey{}); v != nil {
        return v.(string)
    }
    return ""
}

// repositório: tenant_id é parâmetro explícito de query
func (r *PedidoRepo) Listar(ctx context.Context, status string) ([]Pedido, error) {
    tenantID := TenantFromContext(ctx)
    if tenantID == "" {
        return nil, errors.New("tenant required")
    }

    rows, err := r.pool.Query(ctx,
        `SELECT id, valor FROM pedidos
         WHERE tenant_id = $1 AND status = $2`,
        tenantID, status,
    )
    if err != nil { return nil, err }
    defer rows.Close()

    var pedidos []Pedido
    for rows.Next() { /* scan */ }
    return pedidos, nil
}

// para RLS no Postgres, configure conexão com tenant_id
// pgx permite hooks BeforeAcquire/AfterRelease
config.BeforeAcquire = func(ctx context.Context, conn *pgx.Conn) bool {
    tenantID := TenantFromContext(ctx)
    if tenantID != "" {
        conn.Exec(ctx, fmt.Sprintf("SET app.tenant_id = '%s'", tenantID))
    }
    return true
}

Go é o mais explícito: tenant_id em context, repositório passa explicitamente em query, RLS é defesa adicional. Sem ORM "automágico" — a explicitness vence em correctness.

Tenant migration — quando cliente cresce

Em sistemas hybrid, cliente pequeno em pool pode crescer e precisar virar silo. Migração tenant entre arquiteturas é processo não trivial.

Etapas típicas:

1. Provisionar nova infraestrutura silo (banco, cache, possivelmente compute).

2. Migrar dados existentes do pool para silo. Tipicamente snapshot + restore, ou replicação cross-environment.

3. Sincronizar mudanças durante janela de migração (CDC, dual-write).

4. Switch de roteamento: requests do tenant agora vão para silo. Verificar consistência.

5. Limpar dados antigos do pool após período de validação.

Esse processo, em sistema crítico, pode levar horas a dias dependendo de tamanho de dados. Articular plano antes de precisar evita caos durante crescimento — algo que sêniores em SaaS aprendem cedo.

Anti-padrões frequentes

Esquecer WHERE tenant_id. Bug clássico em pool puro. Defesa: query filter automático (EF Core, SQLAlchemy listener) + Postgres RLS como defesa profunda.

Cache shared sem prefixo de tenant. Redis com chave user:42 em vez de tenant:abc:user:42. Tenant A vê dado cacheado de tenant B. Defesa: convenção de naming + lint que valida.

Sem rate limit por tenant. Tenant grande consome 80% do pool. Outros sofrem. Defesa: rate limit por tenant_id; tier-based caps.

Tenant hardcoded em config. Sistema pool com algumas configurações por tenant hardcoded em arquivo (feature flags, limits). Add novo tenant exige redeploy. Defesa: configuração por tenant em banco; aplicação consulta.

Sem plano de migration. Sistema pool sem caminho para silo. Quando cliente cresce ou pede isolation, time se vira sob crise. Defesa: arquitetar para migration desde início, mesmo que não use logo.

Tenant ID em header confiando no cliente. Tenant ID vindo de header controlado pelo cliente, não de claim autenticado. Cliente pode forjar. Defesa: tenant ID sempre extraído de JWT validado; header de cliente tratado como input não-confiável.

armadilha em produção

Tenant isolation breach. Aplicação tem bug em query: programador esqueceu WHERE tenant_id em alguma query nova. Em produção, todos os usuários veem dados de todos os tenants. Em sistema sem RLS, o bug pode passar revisão e ir a produção. Quando descoberto, é incidente de privacidade catastrófico (compliance violations, perda de confiança, possíveis multas). Defesa estrutural: Postgres RLS torna o bug impossível no nível do banco. Sem RLS, a única defesa é rigor de revisão e testes — frágil. Para sistemas multi-tenant sérios, RLS deveria ser default, não opcional.

heurística do sênior

Antes de adotar pool, articule cinco coisas. "Como tenant é identificado em cada request?". "Como toda query é filtrada — convenção, framework, ou banco?". "Quais são os requirements de compliance e cabem em pool?". "O que acontece quando um tenant fica abusivo — rate limit, quarentena, isolation?". "Quando um tenant crescer demais, qual é o caminho para silo?". Sistemas que articulam essas cinco previnem incidentes; sistemas que não articulam descobrem em produção.

Por que importa para a sua carreira

Multi-tenancy é arquitetura central de SaaS, e sêniores em qualquer empresa SaaS conhecem o vocabulário. Em entrevistas, "como você arquitetaria um SaaS B2B com 1000 clientes?" é pergunta direta. A resposta forte cita silo vs pool vs hybrid, noisy neighbor, tenant isolation em camadas (RLS, query filters), billing implications. Em revisão de código, identificar query sem filter de tenant é serviço crítico. Em pos-mortem de "vazamento de dados entre clientes", articular como RLS teria prevenido é trabalho de senior. Em decisões de pricing tier, articular como tier-based isolation se mapeia para hybrid model é vocabulário maduro de produto+engenharia.

Como praticar

  1. Implementar pool com RLS. Suba Postgres local. Configure tabela com tenant_id e RLS policy. Implemente aplicação simples com tenant em JWT, middleware que seta context, queries que confiam em RLS. Tente intencionalmente "vazar" tenant — verifique que RLS bloqueia. Esse exercício torna concreto o ganho.
  2. Auditar isolation em projeto seu. Pegue projeto multi-tenant seu. Liste todas as queries; verifique que cada uma filtra por tenant. Liste todas as chaves de cache; verifique que tem prefixo. Identifique pelo menos uma falha de isolation; proponha correção. Essa auditoria, raramente feita, frequentemente acha bugs latentes.
  3. Modelar hybrid pricing. Pegue um produto SaaS imaginário. Articule planos (free, pro, enterprise) com características de isolation diferentes. Decida: o que cada plano ganha em isolation (compute? banco? cache?). Articule custo unitário por plano. Esse exercício consolida pensamento de produto+engenharia.

Referências para aprofundar

  1. artigo SaaS Tenant Isolation Strategies — AWS Whitepaper (2017+). aws.amazon.com/whitepapers/saas-tenant-isolation-strategies — A referência canônica de silo vs pool vs hybrid. Atualizado periodicamente.
  2. artigo Multi-Tenancy on AWS — AWS Architecture Blog series. aws.amazon.com/blogs/architecture — Casos práticos de multi-tenant em AWS. Inclui sharding, isolation, billing.
  3. livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017). Cap. 6 (Partitioning) cobre tenant-based partitioning como caso especial de sharding.
  4. livro Building Multi-Tenant SaaS Architectures — Tod Golding (O'Reilly, 2024). Livro recente focado especificamente em arquitetura multi-tenant. Cobertura completa: identity, isolation, data, deployment, billing.
  5. livro Cloud Native Patterns — Cornelia Davis (Manning, 2019). Patterns para sistemas cloud-native incluindo multi-tenancy. Bons exemplos com Spring Cloud.
  6. artigo Multi-Tenant SaaS Database Tenancy Patterns — Microsoft Azure Architecture Center. learn.microsoft.com/en-us/azure/azure-sql/database/saas-tenancy-app-design-patterns — Padrões de banco para multi-tenancy. Útil mesmo fora de Azure.
  7. docs PostgreSQL Row Security Policies. postgresql.org/docs/current/ddl-rowsecurity.html — Documentação canônica de RLS. Curtinha; exemplos práticos.
  8. docs EF Core Global Query Filters. learn.microsoft.com/en-us/ef/core/querying/filters — Filter automático por entidade. Combina bem com RLS para defesa em camadas.
  9. artigo How Slack Built Its Database Architecture — Various Slack Engineering posts. slack.engineering — Slack escreve sobre evolução de tenant isolation com workspaces. Casos práticos em escala.
  10. artigo Notion's Engineering Architecture — Notion Engineering Blog. notion.so/blog — Notion documenta evolução de pool para sharded multi-tenant. Casos reais e decisões.
  11. docs Stripe Metering and Usage-Based Billing. stripe.com/docs/billing/subscriptions/usage-based — Como Stripe lida com billing baseado em uso. Vale para entender requirements de metering.
  12. vídeo SaaS Architecture — AWS re:Invent (vários anos). YouTube. AWS publica anualmente palestras sobre arquitetura SaaS, incluindo multi-tenancy. Vale procurar pelas mais recentes.