TL;DR
Existe uma pergunta que separa devs jrs de seniors em entrevista técnica: "Como você desenharia um sistema que precisa aguentar 1 milhão de usuários?" A maioria trava — não porque não sabe programar, mas porque nunca parou para pensar em sistema, só em código.
Com IA isso vai ficando mais evidente. Quando o Copilot escreve a função por você e o Claude gera o CRUD inteiro em 30 segundos, a parte que sobra pra você pensar não é mais o código. É a arquitetura. É decidir onde o dado fica, o que acontece quando o serviço cai, como o sistema se comporta com 10x mais carga. Escrever código virou commodity. Saber desenhar o sistema ao redor dele é o que diferencia.
Aqui vai o que cobre esse post: pilares de system design + os componentes que aparecem em todo sistema + o que muda quando você vai para produção de verdade.
Pontos principais:
- Stateless é regra de ouro — sem isso, escalar horizontalmente vira batalha perdida
- CAP Theorem — você sempre está escolhendo dois dos três, mesmo sem perceber
- Cache-aside — o padrão que resolve boa parte dos problemas de performance
- Circuit breaker — o que separa sistemas que degradam bem dos que caem em cascata
- Observabilidade — se não está monitorado, não existe em produção
Tempo de leitura: ~20 minutos.
Por onde tudo começa: os requisitos
Antes de desenhar qualquer componente, você responde duas perguntas.
Pular isso? É o erro mais caro que existe.
Requisitos funcionais: O que o sistema faz?
Usuários postam fotos, outros curtem, seguem perfis, recebem um feed personalizado.
Requisitos não-funcionais: Como o sistema se comporta?
10 milhões de DAU, latência < 200ms no P99, disponibilidade de 99,9%, 50k writes/s no pico.
Esses números mandam em tudo que vem depois. Olha a diferença de um "9":
| sla | downtime | custo |
|---|---|---|
| 99% | ~87 horas | 1x |
| 99,9% | ~8,7 horas | 3x |
| 99,99% | ~52 minutos | 10x |
| 99,999% | ~5 minutos | 30x |
Cada "9" adicional multiplica o custo de infraestrutura em 3-10x.
Antes de prometer "five nines" para alguém — saiba o que você está vendendo.
Os três pilares
1. Escalabilidade
Vertical (scale up): mais CPU/RAM na mesma máquina. Simples. Tem teto físico. E quando a máquina cai, tudo cai junto.
Horizontal (scale out): mais máquinas. É o que sustenta qualquer sistema de verdade. Mas tem um preço: sua aplicação precisa ser stateless.
Uma pergunta antes de seguir:
"Minha aplicação guarda estado que não está no banco ou cache externo?"
Se sim, você tem um problema. Usuário bate no nó A, sessão está lá. Próxima request cai no nó B — sumiu.
Solução? Tira o estado de dentro. Redis para sessões, banco para dados persistentes, S3 para arquivos.
// ❌ Stateful: estado na memória local
var sessionCache = map[string]Session{}
func GetSession(id string) Session {
return sessionCache[id] // só funciona se sempre bater no mesmo nó
}
// ✅ Stateless: estado no Redis
func GetSession(ctx context.Context, id string) (Session, error) {
val, err := redisClient.Get(ctx, "session:"+id).Result()
if err != nil {
return Session{}, err
}
var s Session
return s, json.Unmarshal([]byte(val), &s)
}
Arquitetura com escala horizontal habilitada:
Cada instância é idêntica. Descartável.
O load balancer manda qualquer request para qualquer nó. O estado está nos serviços externos, não na memória do processo.
2. Disponibilidade vs. Consistência: o CAP Theorem
O CAP Theorem diz que um sistema distribuído não consegue garantir as três coisas ao mesmo tempo:
- Consistency — todos os nós veem os mesmos dados ao mesmo tempo;
- Availability — toda requisição recebe resposta, mesmo que não seja a versão mais recente;
- Partition tolerance — o sistema segura se parte da rede cair.
Partição de rede não é hipótese. Acontece.
Então P é dado. Na prática você escolhe entre CP ou AP:
| sistema | escolha | motivo |
|---|---|---|
| Banco de dados financeiro | CP | Saldo precisa ser correto em todos os nós |
| Feed de redes sociais | AP | Tudo bem se demorar 2s pra ver a foto nova |
| Sistema de inventário | CP | Não pode vender produto sem estoque |
| DNS | AP | Disponibilidade > consistência imediata |
| Cassandra | AP | Alta escrita distribuída, eventual consistency |
| PostgreSQL | CP | Transações ACID, consistência forte |
Saber isso antes de escolher banco de dados elimina metade das discussões de arquitetura.
3. Latência vs. Throughput
Latência: tempo de uma requisição, do início ao fim. Throughput: quantas requisições por segundo você processa.
Otimizar um atrapalha o outro. Sempre.
Alto Throughput / Alta Latência
└── Batch: 1 job processa 10k registros de uma vez
→ Ótimo pra throughput, péssimo pra resposta imediata
Baixa Latência / Menor Throughput
└── Streaming: processa cada evento individualmente
→ Resposta imediata, mas escalar throughput custa caro
Qual é a prioridade? Resposta rápida ou volume? Em sistemas reais você equilibra os dois — mas quando der conflito, precisa saber qual sacrificar.
Os componentes que todo sistema usa
Load Balancer
Ponto de entrada do sistema. Distribui tráfego entre instâncias:
Algoritmos comuns:
├── Round Robin: A → B → C → A → B → C
├── Least Connections: manda pra quem tem menos conexões ativas
├── IP Hash: mesmo IP sempre vai pro mesmo nó (sticky sessions)
└── Weighted: nó com mais recurso recebe mais tráfego
Detalhe que muita gente esquece: o próprio load balancer é ponto único de falha.
Em produção você tem pelo menos dois, com failover automático:
Cache
O dado mais rápido é o que você não precisa buscar.
Cache resolve dois problemas ao mesmo tempo: reduz latência (memória é ~100x mais rápida que disco) e alivia a carga no banco.
O padrão mais comum é o Cache-Aside (Lazy Loading):
Em Go fica assim:
func GetUser(ctx context.Context, userID string) (*User, error) {
cacheKey := "user:" + userID
// 1. Tenta o cache primeiro
cached, err := redis.Get(ctx, cacheKey).Result()
if err == nil {
var user User
if err := json.Unmarshal([]byte(cached), &user); err == nil {
return &user, nil // cache hit ✅
}
}
// 2. Cache miss: busca no banco
user, err := db.GetUser(ctx, userID)
if err != nil {
return nil, err
}
// 3. Popula o cache com TTL de 5 minutos
data, _ := json.Marshal(user)
redis.Set(ctx, cacheKey, data, 5*time.Minute)
return user, nil
}
// Invalidação: quando o usuário atualiza o perfil
func UpdateUser(ctx context.Context, user *User) error {
if err := db.UpdateUser(ctx, user); err != nil {
return err
}
// Remove do cache para forçar re-fetch atualizado
redis.Del(ctx, "user:"+user.ID)
return nil
}
Cache Stampede: uma chave expira e mil requisições chegam ao mesmo tempo — todas indo direto ao banco. O sistema que estava rápido agora lentifica exatamente onde você não quer.
Solução: mutex via Redis.
func GetUserSafe(ctx context.Context, userID string) (*User, error) {
cacheKey := "user:" + userID
lockKey := "lock:user:" + userID
if cached, err := redis.Get(ctx, cacheKey).Result(); err == nil {
var user User
json.Unmarshal([]byte(cached), &user)
return &user, nil
}
// Só uma goroutine popula o cache
acquired, _ := redis.SetNX(ctx, lockKey, 1, 3*time.Second).Result()
if !acquired {
time.Sleep(50 * time.Millisecond)
return GetUserSafe(ctx, userID) // tenta de novo
}
defer redis.Del(ctx, lockKey)
user, err := db.GetUser(ctx, userID)
if err != nil {
return nil, err
}
data, _ := json.Marshal(user)
redis.Set(ctx, cacheKey, data, 5*time.Minute)
return user, nil
}
Banco de Dados: SQL vs. NoSQL
A escolha mais importante — e mais mal feita — em arquitetura de sistemas.
| criterio | sql | nosql |
|---|---|---|
| Transações ACID | ✅ Nativo | ⚠️ Limitado ou ausente |
| Joins complexos | ✅ Primeira classe | ❌ Custoso ou impossível |
| Esquema flexível | ❌ Schema obrigatório | ✅ Schema-less |
| Escala horizontal de writes | ⚠️ Complexo (sharding) | ✅ Nativo |
| Consistência | Forte (CP) | Eventual (AP) na maioria |
| Melhor para | Financeiro, ERP, e-commerce | Feed, analytics, IoT, logs |
O erro clássico: escolher NoSQL "porque escala" sem perceber que você está abrindo mão de consistência e joins.
Não tem bala de prata. Nunca teve.
Na prática, sistemas grandes usam os dois, cada um no lugar certo:
| banco | uso | motivo |
|---|---|---|
| PostgreSQL | Dados transacionais | Pedidos, pagamentos, usuários — precisa de ACID |
| Redis | Cache, sessões, filas | Latência de microssegundos, estruturas em memória |
| Cassandra | Eventos, logs, timeseries | Escala de write absurda, distribuído nativamente |
| Elasticsearch | Busca full-text | Indexação invertida, queries complexas de texto |
Filas de Mensagens
Desacoplar produtor de consumidor. Um dos movimentos mais baratos que existem. Resolve problema de confiabilidade sem precisar mudar a lógica de negócio.
Sem fila (síncrono): qualquer falha downstream derruba o fluxo inteiro, e o usuário espera tudo:
Com fila (assíncrono): o producer retorna na hora, cada consumer processa no próprio ritmo:
Exemplo com Kafka em Go:
// Producer: publica evento de upload
func PublishUploadEvent(ctx context.Context, upload UploadEvent) error {
payload, _ := json.Marshal(upload)
return kafkaWriter.WriteMessages(ctx, kafka.Message{
Topic: "uploads.completed",
Key: []byte(upload.UserID),
Value: payload,
})
}
// Consumer: processa thumbnail de forma assíncrona
func StartThumbnailWorker(ctx context.Context) {
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"kafka:9092"},
Topic: "uploads.completed",
GroupID: "thumbnail-service",
})
for {
msg, err := reader.FetchMessage(ctx)
if err != nil {
log.Printf("fetch error: %v", err)
continue
}
var upload UploadEvent
if err := json.Unmarshal(msg.Value, &upload); err != nil {
reader.CommitMessages(ctx, msg) // mensagem corrompida: descarta
continue
}
if err := generateThumbnail(ctx, upload); err != nil {
log.Printf("thumbnail error: %v", err)
// NÃO commita: Kafka vai re-entregar a mensagem
continue
}
reader.CommitMessages(ctx, msg) // só commita se processou com sucesso
}
}
| criterio | kafka | rabbit | sqs |
|---|---|---|---|
| Throughput | Absurdo (milhões/s) | Alto (100k/s) | Alto (gerenciado) |
| Replay de mensagens | ✅ Sim | ❌ Não | ❌ Não |
| Retenção | Configurável (dias/semanas) | Até consumir | Até 14 dias |
| Complexidade operacional | Alta | Média | Baixa (serverless) |
| Melhor para | Event sourcing, analytics | Task queues | AWS-native |
Um ponto que só aparece quando você precisa.
Bug descoberto dias depois? Com Kafka você reprocessa o período inteiro. Com RabbitMQ ou SQS essa conversa não existe.
Do papel para a produção: o que muda
Observabilidade: os três pilares
Se não está monitorado, não existe em produção.
| pilar | captura | ferramenta |
|---|---|---|
| Métricas | Latência, req/s, CPU, memória | Prometheus + Grafana |
| Logs | Erros, eventos, debug | Structured JSON Logs |
| Tracing | Caminho completo de uma request | OpenTelemetry + Jaeger |
Métricas que todo serviço precisa expor. Padrão RED: Rate, Errors, Duration:
var (
httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
}, []string{"method", "path", "status"})
httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5},
}, []string{"method", "path"})
)
func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r)
status := strconv.Itoa(rw.statusCode)
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, status).Inc()
httpDuration.WithLabelValues(r.Method, r.URL.Path).Observe(time.Since(start).Seconds())
})
}
Logs estruturados com correlation ID. Sem isso, rastrear uma request num sistema distribuído é chute:
func Handler(w http.ResponseWriter, r *http.Request) {
correlationID := r.Header.Get("X-Correlation-ID")
if correlationID == "" {
correlationID = uuid.New().String()
}
log := logger.With(
zap.String("correlation_id", correlationID),
zap.String("user_id", getUserID(r)),
zap.String("path", r.URL.Path),
)
log.Info("request started")
// ... lógica
log.Info("request completed", zap.Duration("duration", time.Since(start)))
}
Circuit Breaker
Quando um serviço downstream começa a falhar, o que você menos quer é ficar esperando timeout em cada chamada.
30s de timeout × 1000 req/s. Você já sabe o que acontece.
Em Go:
type CircuitBreaker struct {
failures int
threshold int
state string // "closed", "open", "half-open"
openedAt time.Time
timeout time.Duration
mu sync.Mutex
}
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case "open":
if time.Since(cb.openedAt) > cb.timeout {
cb.state = "half-open"
} else {
return errors.New("circuit breaker open: serviço indisponível")
}
case "half-open":
err := fn()
if err != nil {
cb.state = "open"
cb.openedAt = time.Now()
return err
}
cb.state = "closed"
cb.failures = 0
return nil
}
err := fn()
if err != nil {
cb.failures++
if cb.failures >= cb.threshold {
cb.state = "open"
cb.openedAt = time.Now()
log.Printf("circuit breaker ABERTO após %d falhas", cb.failures)
}
return err
}
cb.failures = 0
return nil
}
var paymentCB = &CircuitBreaker{threshold: 5, timeout: 30 * time.Second, state: "closed"}
func ProcessPayment(ctx context.Context, p Payment) error {
return paymentCB.Call(func() error {
return stripeClient.Charge(ctx, p)
})
}
Rate Limiting
Protege o sistema de abuso — e de bugs que viram loop infinito de requisições. Já vi os dois acontecerem.
O Token Bucket: tokens chegam a uma taxa fixa, cada request consome um. Bucket vazio = 429.
Distribuído com Redis, funciona com múltiplas instâncias:
func RateLimiter(rdb *redis.Client, key string, limit int, window time.Duration) (bool, error) {
ctx := context.Background()
now := time.Now().UnixMilli()
windowStart := now - window.Milliseconds()
pipe := rdb.Pipeline()
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
pipe.ZCard(ctx, key)
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
pipe.Expire(ctx, key, window)
results, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
count := results[1].(*redis.IntCmd).Val()
return count < int64(limit), nil
}
func RateLimitMiddleware(limit int, window time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := "ratelimit:" + getClientIP(r)
allowed, _ := RateLimiter(redisClient, key, limit, window)
if !allowed {
w.Header().Set("Retry-After", "60")
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
Deployment: Blue-Green e Canary
Deployar em produção sem estratégia é aposta.
Blue-Green: dois ambientes idênticos, swap instantâneo:
Canary: além de ser a cor da camisa do brasil 2026, é tráfego gradual para a nova versão, monitorando métricas a cada etapa:
Qualquer métrica piorou? Volta pra 0% imediatamente.
Juntando tudo: arquitetura de um sistema real
Juntando tudo num sistema real. Um feed de redes sociais:
Cada camada resolve um problema específico. E quando uma falha — e vai falhar — não derruba as outras.
Performance: o que os números dizem
O impacto de cada camada na latência de uma request:
Taxa de hit esperada por componente:
Um exercício prático
Pega qualquer sistema que você usa todo dia — Twitter, Spotify, iFood — e responde:
- Requisitos não-funcionais: quantos DAU? Quantas req/s? Qual latência você aceita?
- Banco de dados: SQL ou NoSQL? Por quê? Precisa de joins? Consistência ou disponibilidade?
- Cache: o que cachear? Qual TTL? O que não pode ir pro cache?
- Filas: quais operações podem ser assíncronas? O que acontece se a fila atrasar?
- Gargalo: qual componente você estrangula primeiro? Como escala ele especificamente?
Não tem resposta certa. Tem trade-off consciente. É a diferença entre quem projeta e quem torce.
System design é habilidade que se constrói errando.
Começa com algo simples. Vai para produção. Descobre o gargalo. Melhora. Não tenta acertar tudo de primeira. Over engineering antes da hora é tão problemático quanto sistema que cede na hora do pico.
O objetivo não é o diagrama bonito. É saber onde olhar quando algo quebrar na sexta-feira às 23h.
E vai quebrar. Sempre.
Quer ir fundo? O System Design Primer no GitHub cobre cada um desses tópicos em detalhe, gratuito e vale o tempo.