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.
// 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.
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.
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.
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.
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
-
Implementar pool com RLS. Suba
Postgres local. Configure tabela com
tenant_ide 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. - 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.
- 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
- artigo SaaS Tenant Isolation Strategies — AWS Whitepaper (2017+).
- artigo Multi-Tenancy on AWS — AWS Architecture Blog series.
- livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017).
- livro Building Multi-Tenant SaaS Architectures — Tod Golding (O'Reilly, 2024).
- livro Cloud Native Patterns — Cornelia Davis (Manning, 2019).
- artigo Multi-Tenant SaaS Database Tenancy Patterns — Microsoft Azure Architecture Center.
- docs PostgreSQL Row Security Policies.
- docs EF Core Global Query Filters.
- artigo How Slack Built Its Database Architecture — Various Slack Engineering posts.
- artigo Notion's Engineering Architecture — Notion Engineering Blog.
- docs Stripe Metering and Usage-Based Billing.
- vídeo SaaS Architecture — AWS re:Invent (vários anos).