Em 2011, Adam Wiggins — cofundador do Heroku — publicou no site 12factor.net uma lista modesta de doze princípios para construir aplicações "as a service" rodando em platform-as-a-service. O sexto princípio era curto e definitivo: "Processes — Execute the app as one or more stateless processes". A frase parece técnica e é, antes de tudo, organizacional. Stateless não é otimização — é propriedade que define o tipo de sistema que você está construindo.
Um serviço stateless não mantém estado entre requests. Cada request entra com tudo o que é necessário para ser processada (em headers, body, ou via dados externos referenciados); o serviço processa; retorna. Reset de instância não perde nada relevante; adicionar instância nova é transparente; substituir instância antiga por mais recente é deploy zero-downtime trivial. Os efeitos compostos são profundos: scale out vira fácil, deploy vira simples, falha vira tolerável.
Um serviço stateful, por contraste, mantém estado entre requests. Cache local de configurações, sessão de usuário em memória, contadores agregados, conexões persistentes "fixadas" a usuário — tudo isso é estado em memória que precisa ser preservado, sincronizado, ou sacrificado. Sistemas stateful escalam mal: cada réplica vira "única" no que tem em memória; load balancer precisa decidir o que fazer; falha de instância perde estado.
Este conceito articula a propriedade. O que qualifica um serviço como stateless de fato; onde estado pode legitimamente viver fora do serviço; os padrões para gerenciar coisas que parecem precisar de estado (sessões, cache, conexões); e os anti-padrões clássicos onde times acidentalmente criam estado em memória sem perceber. Os dez conceitos seguintes do módulo só funcionam se este estiver fixado — sharding, autoscaling, multi-region, todos pressupõem stateless.
O que estado significa, concretamente
"Estado" é palavra carregada. Em design de sistemas, estado é qualquer informação que persiste entre requests dentro do processo da aplicação. Os tipos típicos:
Sessão de usuário em memória.
Aplicação guarda userId ou
shoppingCart em dicionário local;
próxima request do mesmo usuário precisa cair na
mesma instância para encontrar o estado.
Cache local de dados. Aplicação
faz lookup ao banco uma vez, guarda em
Dictionary<K,V> ou
map[K]V local. Próximas requests
pegam do cache local — funciona até a aplicação ser
reiniciada ou outra instância pegar a request.
Conexões persistentes. WebSocket, Server-Sent Events, gRPC streaming — cliente conectado a uma instância específica. Estado da conexão (estado do canal, protocolo de mensagem) vive na instância.
Contadores em memória. Métricas locais antes de exportar; rate limiter implementado como contador local; flags de feature toggle cacheadas.
Locks em memória. Coordenação entre threads do mesmo processo via mutex.
Cada um deles é estado. Cada um, em sistema com múltiplas instâncias, vira problema próprio. Saber identificar tudo isso em código existente é trabalho de senior em revisão.
Por que stateless escala
A propriedade de stateless habilita três comportamentos que stateful não consegue.
Roteamento livre. Qualquer request pode ir para qualquer instância. Load balancer pode usar round-robin, least connections, ou qualquer algoritmo simples — não precisa lembrar "essa request veio dessa sessão e portanto precisa desta instância". Isso é o que torna load balancing barato e auto-scaling efetivo.
Substituição transparente. Instância pode ser reiniciada, atualizada, ou substituída sem cerimônia. Em deploy, rolling update tira instâncias velhas, sobe novas, sem coordenação complexa. Em falha, nova instância sobe e tráfego reflete sem que cliente perceba.
Capacidade incremental. Adicionar instâncias adiciona capacidade linear. 10 instâncias processam ~10× o que uma processa. Sistemas stateful têm "capacidade desigual" — instâncias com mais sessões pesadas ficam saturadas enquanto outras têm folga.
Esses três efeitos são o que torna scale out viável operacionalmente. Em sistema stateful, cada um desses se torna problema separado a resolver — e a soma da complexidade frequentemente é o que mata o sistema.
Onde estado pode viver — externalizando
Stateless não significa "sem estado" — significa "estado fora do processo da aplicação". O estado vive em sistemas dedicados, projetados para compartilhamento entre múltiplas instâncias. Os lugares canônicos:
Banco de dados (Postgres, MySQL). Estado persistente, transacional, coerente. Para tudo que precisa ACID e durabilidade.
Cache distribuído (Redis, Memcached). Estado rápido, em memória, compartilhado. Para dados frequentemente lidos com tolerância a perda em caso de falha do cache.
Object storage (S3, GCS, Azure Blob). Estado grande, durável, lido como arquivo. Para arquivos de mídia, backups, datasets.
Fila / message broker (Kafka, RabbitMQ, SQS). Estado em trânsito — mensagens esperando processamento.
Search engine (Elasticsearch, OpenSearch). Estado indexado para busca.
A regra: tudo que não cabe em request e não cabe em response, mas precisa sobreviver entre requests, vai para um desses sistemas. A aplicação consulta quando precisa, escreve quando muda, e fica sem nada em memória própria.
Sessões — o caso clássico
Sessão de usuário é o caso mais frequente de estado acidental. Aplicação tradicional ASP.NET, PHP, Java pré-Spring tinham mecanismo de session integrado — framework guardava sessão em memória; cookie no cliente identificava qual sessão. Funcionava bem em sistema com 1 servidor; quebrava em 2.
Há quatro estratégias modernas para sessão. Cada uma tem trade-off próprio.
Estratégia 1 — Sticky sessions
Load balancer "fixa" o cliente em uma instância
específica via cookie ou header (
AWSALB no AWS ALB,
Cookie baseado em hash em NGINX). A
sessão fica em memória na instância; balancer garante
que requests subsequentes do mesmo cliente cheguem
lá.
Ganhos: fácil de implementar, latência ótima (cache local), nenhum sistema externo adicional. Custos: instância sobrecarregada se um usuário power consome muito; falha de instância perde sessões; deploy/auto-scaling complica (instância nova não tem sessões); imbalance entre instâncias.
Sticky sessions é solução de transição — usar enquanto migra. Em sistema novo, raramente é a escolha certa.
Estratégia 2 — Sessão em store distribuído
Sessão fica em Redis (ou Memcached, ou banco). Cookie no cliente carrega só ID da sessão. Aplicação faz lookup no Redis a cada request — ou cacheia na instância por TTL curto.
Ganhos: stateless de fato; qualquer instância serve qualquer cliente; falha não perde sessão (Redis é persistente ou replicado). Custos: latência adicional (~1 ms para Redis local); SPOF se Redis cai; mais um sistema para operar.
ASP.NET Core
AddDistributedRedisCache + AddSession;
Spring Session com Redis backend; Express
express-session com
connect-redis — todos seguem essa
estratégia. É o padrão para aplicações tradicionais
com sessão server-side.
Estratégia 3 — JWT (token stateless)
Em vez de armazenar sessão, sessão vai dentro do token. Cliente recebe JWT (RFC 7519) assinado pelo servidor; cada request envia o token. Servidor valida assinatura e extrai claims (userId, roles, etc.) sem nenhum lookup. Essa é a base do conceito 11 do módulo 05 (auth).
Ganhos: zero estado server-side; escala perfeito; latência ótima. Custos: revogação difícil (token válido até expiração; sem ferramenta para "deslogar" imediatamente sem lista de blacklist); tamanho do token cresce com claims; segurança exige cuidado (usar bibliotecas testadas).
Padrão dominante em APIs modernas. Combinado com refresh token para mitigar revogação. Para sistemas onde sessão precisa ser revogável imediatamente (banking, alta segurança), JWT exige mecanismo adicional (blacklist, ou reverter para session store).
Estratégia 4 — Sem sessão (cliente carrega tudo)
APIs RESTful "puras" não têm sessão; cada request é auto-contida. Usuário envia credentials a cada request (típico em APIs B2B); ou autenticação fica em token separado por request. Não há "estado de sessão"; cada request é isolada.
Ganhos: máxima simplicidade arquitetural. Custos: pode ser menos amigável para front-end web (precisa gerenciar token em todas as chamadas).
Cache local — onde times se enganam
Cache local é o estado mais sutil. Aplicação faz
lookup, guarda em
ConcurrentDictionary<K,V> ou
sync.Map, retorna cache hit nas
próximas. Funciona, é rápido. Em sistema com 5
réplicas, vira 5 caches independentes que podem
divergir.
A pergunta diagnóstica: "se duas réplicas diferentes têm valores diferentes em cache, isso é problema?". Em alguns casos, não — lookup tables imutáveis (lista de países, configuração versionada por deploy). Em outros casos, sim — cache de configuração que muda em runtime, cache de feature flag, cache de permissão.
Estratégias para cache "local":
Imutável por deploy. Carrega no startup, nunca muda durante a vida da instância. Mudança requer redeploy. Aceitável para configuração que muda raramente.
TTL curto + fonte central. Cache local com TTL de 10-30 segundos; cada réplica revalida com fonte central periodicamente. Tolerância: até TTL segundos de divergência.
Push-based invalidation. Mudança no estado central dispara evento (Redis pub/sub, Kafka topic) que todas as réplicas escutam, e invalida cache local. Mais complexo; mais consistente.
Sem cache local. Sempre lookup ao Redis/banco. Latência um pouco maior, simplicidade máxima. Para a maioria dos sistemas, é a escolha saudável.
Conexões persistentes — WebSocket, gRPC streaming
Conexões persistentes parecem inerentemente stateful — cliente está fisicamente conectado a uma instância. Mas o pattern moderno permite que mesmo essas conexões respeitem stateless lógico.
O segredo: a conexão é stateful (a instância tem estado de conexão), mas o estado lógico da aplicação é externalizado. Mensagem que chega na conexão é processada e armazenada em sistema central (Redis pub/sub para broadcast, banco para persistência). Conexão pode cair; cliente reconecta a outra instância; estado lógico continua acessível.
// padrão pseudo-código para WebSocket multi-instância
ws.onMessage((msg) => {
// não armazena nada localmente
// publica no canal Redis pub/sub
redis.publish(`user:${userId}`, JSON.stringify(msg));
// outras réplicas que têm conexões para outros usuários
// recebem via subscriber e fazem broadcast
});
ws.onConnect((userId) => {
// subscribe no canal Redis para esse user
redis.subscribe(`user:${userId}`, (msg) => ws.send(msg));
});
ASP.NET SignalR com Redis backplane; Socket.IO com Redis adapter; phoenix-channels com PubSub — todos implementam essa estratégia. Cliente conecta a qualquer instância; estado lógico flui via Redis.
Twelve-factor — o checklist canônico
O 12-factor app (Wiggins, 2011) tem doze princípios para aplicações cloud-native. Vários se conectam diretamente com stateless. Vale relembrar os mais relevantes:
Factor VI — Processes (stateless). O princípio central. Cada processo é stateless e share-nothing.
Factor IV — Backing services. Banco, cache, fila são "serviços de apoio" tratados como recursos anexos via configuração. Trocáveis, observáveis.
Factor IX — Disposability. Processos podem iniciar e parar a qualquer momento. Startup rápido, shutdown limpo. Suporta auto-scaling e recuperação de falha.
Factor XI — Logs. Tratar logs como stream de eventos para stdout. Coletor externo indexa e consulta.
Times maduros tomam o 12-factor como guia de design desde o início. Sistemas que aderem têm ergonomia operacional excelente; sistemas que ignoram acumulam dívida que aparece em produção.
Stateless em três stacks
Para fixar como stateless aparece em código, considere o cenário canônico — sessão de usuário em sistema com múltiplas réplicas.
// startup
builder.Services.AddStackExchangeRedisCache(options => {
options.Configuration = "redis:6379";
options.InstanceName = "catalog:";
});
builder.Services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession();
// uso — qualquer instância serve qualquer cliente
app.MapGet("/cart", async (HttpContext ctx) => {
var cartJson = ctx.Session.GetString("cart") ?? "[]";
return Results.Ok(JsonSerializer.Deserialize<Cart>(cartJson));
});
app.MapPost("/cart/items", async (HttpContext ctx, AddItemReq req) => {
var cartJson = ctx.Session.GetString("cart") ?? "[]";
var cart = JsonSerializer.Deserialize<Cart>(cartJson)!;
cart.Items.Add(new CartItem(req.Sku, req.Qty));
ctx.Session.SetString("cart", JsonSerializer.Serialize(cart));
return Results.NoContent();
});
// para JWT puro (sem sessão server-side):
// AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// .AddJwtBearer(...)
// e claims do usuário ficam no token, sem nenhuma sessão
ASP.NET Core abstrai sessão via
IDistributedCache — backend Redis
torna trivialmente stateless. Mas em APIs
modernas, JWT é mais comum (sem session storage
mesmo).
from fastapi import FastAPI, Depends, HTTPException, Cookie
import redis.asyncio as redis
import json
from uuid import uuid4
r = redis.from_url("redis://redis:6379")
app = FastAPI()
# sessão externalizada via Redis
async def get_session(session_id: str | None = Cookie(default=None)):
if not session_id:
return {}
data = await r.get(f"session:{session_id}")
return json.loads(data) if data else {}
async def save_session(session_id: str, data: dict, ttl: int = 1800):
await r.set(f"session:{session_id}", json.dumps(data), ex=ttl)
@app.post("/cart/items")
async def add_item(
req: AddItemReq,
session_id: str | None = Cookie(default=None),
session = Depends(get_session),
):
session_id = session_id or str(uuid4())
cart = session.get("cart", [])
cart.append({"sku": req.sku, "qty": req.qty})
session["cart"] = cart
await save_session(session_id, session)
response = JSONResponse(content={"ok": True})
response.set_cookie("session_id", session_id, httponly=True)
return response
# JWT alternativo — sem sessão
@app.post("/cart/items")
async def add_item_jwt(req: AddItemReq, user: User = Depends(current_user)):
# carrinho persistido em banco com user.id como chave
await repo.adicionar_item(user.id, req.sku, req.qty)
return {"ok": True}
FastAPI não tem session integrada — você implementa via Redis explicitamente, ou usa JWT (mais comum em APIs modernas). A explicitude é cultural Python.
package main
import (
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
)
// middleware: valida JWT e popula contexto
func WithAuth(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
tok, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
return secret, nil
})
if err != nil || !tok.Valid {
http.Error(w, "unauthorized", 401)
return
}
claims := tok.Claims.(jwt.MapClaims)
ctx := context.WithValue(r.Context(), userKey, claims["sub"].(string))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// handler: stateless, dado vai para Postgres
func (h *Handlers) AddItem(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(userKey).(string)
var req AddItemReq
json.NewDecoder(r.Body).Decode(&req)
err := h.repo.AdicionarItem(r.Context(), userID, req.SKU, req.Qty)
if err != nil { http.Error(w, "fail", 500); return }
w.WriteHeader(204)
}
Em Go, JWT puro é o caminho mais idiomático. Middleware stateless valida; handler stateless opera. Estado vai para banco. Zero session server-side.
Anti-padrões frequentes
Lock distribuído via memory map local. Aplicação implementa "garante que só uma operação por usuário roda por vez" via mutex em memória. Funciona em uma instância; em 10, cada uma tem seu próprio lock — 10 podem rodar simultaneamente. Defesa: lock distribuído (Redis Redlock, etcd, ZooKeeper).
Rate limiter local. "Cada usuário pode 100 req/min". Implementado como contador em memória. Em 10 réplicas, usuário consegue 1000 req/min. Defesa: rate limiter centralizado (Redis, ou serviço dedicado). Ver módulo 05 conceito 09 e módulo 06 conceito 10 (edge rate limiting).
Cache de permissões em memória sem invalidação cross-instance. Admin remove permissão de usuário. Banco atualiza. 4 réplicas ainda têm a permissão em cache local; usuário consegue acessar até TTL expirar (minutos). Defesa: pub/sub para invalidação ou TTL muito curto, ou sempre lookup ao Redis.
Singleton em memória que mantém estado
mutável. Padrão comum em código legado:
ConfigCache.Instance.Update(...). Em
múltiplas instâncias, cada uma tem seu próprio
singleton. Defesa: singleton só para imutável; estado
mutável vai para Redis ou banco.
Conexões mantidas com cliente sem compartilhamento. Aplicação SSE ou WebSocket onde cada réplica mantém seus clientes conectados, sem coordenação. Mensagem que precisa ir para "todos os clientes do user X" só atinge os conectados na mesma réplica. Defesa: pub/sub backbone (Redis, NATS) que coordena entre réplicas.
Auto-scaling adiciona réplica nova com cache vazio, latência sobe momentaneamente. Sistema com cache local agressivo: 4 réplicas com cache quente; pico de tráfego dispara auto-scaling; 5ª réplica sobe com cache frio. Tráfego é distribuído por round-robin; 1 em cada 5 requests vai para a réplica nova e tem latência muito mais alta. P99 sobe durante o "warmup". Em sistemas com muitas réplicas e cache pesado, esse fenômeno é regular. Defesa: cache pré-aquecido em startup (warm-up explícito), ou cache externo (Redis) que é compartilhado.
Validar stateless — checklist de revisão
Para validar que um serviço é genuinamente stateless, a equipe pode aplicar quatro testes em revisão:
1. Reinício transparente. Reiniciar uma instância em produção (com graceful shutdown e restart) deve ser imperceptível para usuários. Se causa erro, perda de dados, ou degradação visível, há estado em memória.
2. Adição transparente. Adicionar uma instância nova ao pool deve ser imperceptível. Se a instância nova "demora a esquentar" excessivamente, há cache local crítico.
3. Distribuição uniforme de carga. Round-robin entre N instâncias deve dar utilização similar. Se algumas instâncias ficam sobrecarregadas (sticky sessions, conexões persistentes mal balanceadas), há estado.
4. Idempotência da operação. Mesma operação em qualquer réplica deve dar mesmo resultado. Se diverge, alguma réplica tem dado diferente — estado divergente.
Equipes que rodam esses testes em CI ou em ambientes de staging com regularidade pegam estado acidental antes que vire incidente.
Em revisão de código, qualquer variável estática
mutável, qualquer
ConcurrentDictionary<K,V>, qualquer
sync.Map com lifetime longo, qualquer
cache local sem TTL curto — tudo isso merece a
pergunta: "como esse código se comporta em 10
réplicas?". Se a resposta envolve "todas as
réplicas convergem" ou "essas réplicas divergem
intencionalmente", siga. Se a resposta é "cada
réplica tem comportamento ligeiramente diferente",
é estado escondido — refatorar para externalizar.
Por que importa para a sua carreira
Stateless é a propriedade arquitetural mais frequentemente perguntada em entrevista de senior backend. "O que torna um serviço stateless?" "Por que isso importa para escalabilidade?" A resposta forte cita externalização de estado, sticky vs JWT, o 12-factor app, e articula trade-offs (latência vs simplicidade vs custo). Em revisão de código, identificar estado em memória que viola stateless antes de virar bug em produção é serviço de senior. Em pos-mortem de "auto-scaling causou bug", o diagnóstico estrutural é frequentemente "tinha cache local que divergia" — e a solução articulada é "externalize ou faça push invalidation". Em design de sistemas com pretensão de escalar, tomar a decisão consciente de "tudo é stateless desde início" é o que torna scale out possível mais tarde sem reescrita.
Como praticar
-
Auditoria de estado em projeto seu.
Pegue um projeto seu (ou aberto) e use grep para
encontrar variáveis estáticas mutáveis,
ConcurrentDictionary,sync.Map, ou similar. Para cada um, articule: que estado guarda? Como se comporta em 10 réplicas? Identifique pelo menos um caso onde há divergência entre réplicas e proponha externalização. - Migração de sticky para JWT. Em um projeto com sessão server-side, implemente versão alternativa com JWT. Compare: quantas linhas de código mudaram; latência por request; complexidade de revogação. Documente os trade-offs em ADR. Esse exercício, feito em projeto real, calibra muitas decisões futuras.
- Teste de reinício transparente. Em ambiente staging, configure carga sustentada (k6 ou similar) por 5 minutos. Durante o teste, reinicie uma das réplicas (kill + restart). Observe se a métrica de erro sobe, se latência aumenta, se algum usuário recebe 500. Se algo dá errado, há estado escondido. Esse é o teste que sêniores fazem regularmente em sistemas críticos.
Referências para aprofundar
- artigo The Twelve-Factor App — Adam Wiggins (Heroku, 2011).
- livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017).
- livro Building Microservices (2ª ed.) — Sam Newman (O'Reilly, 2021).
- livro Cloud Native Patterns — Cornelia Davis (Manning, 2019).
- livro Production-Ready Microservices — Susan Fowler (O'Reilly, 2016).
- artigo JWT — RFC 7519.
- artigo Stop using JWT for sessions — Sven Slootweg (joepie91, 2016).
- artigo Critique of the 12-Factor App — Stephen Walters (blog, 2018).
- docs ASP.NET Core Session.
- docs SignalR with Redis backplane.
- docs Redis Redlock.
- vídeo Stateful Services Done Right — Caitie McCaffrey (várias palestras, 2015–2020).