System Design: Do Conceito a Produção

#System Design#Architecture#Backend#Go
System Design: Do Conceito a Produção

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":

sladowntimecusto
99%~87 horas1x
99,9%~8,7 horas3x
99,99%~52 minutos10x
99,999%~5 minutos30x

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:

Escalabilidade Horizontal: Estado Externo
Loading diagram...

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:

sistemaescolhamotivo
Banco de dados financeiroCPSaldo precisa ser correto em todos os nós
Feed de redes sociaisAPTudo bem se demorar 2s pra ver a foto nova
Sistema de inventárioCPNão pode vender produto sem estoque
DNSAPDisponibilidade > consistência imediata
CassandraAPAlta escrita distribuída, eventual consistency
PostgreSQLCPTransaçõ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:

Load Balancer em Alta Disponibilidade
Loading diagram...

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):

Padrão Cache-Aside (Lazy Loading)
Loading diagram...

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.

criteriosqlnosql
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ênciaForte (CP)Eventual (AP) na maioria
Melhor paraFinanceiro, ERP, e-commerceFeed, 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:

bancousomotivo
PostgreSQLDados transacionaisPedidos, pagamentos, usuários — precisa de ACID
RedisCache, sessões, filasLatência de microssegundos, estruturas em memória
CassandraEventos, logs, timeseriesEscala de write absurda, distribuído nativamente
ElasticsearchBusca full-textIndexaçã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:

Sem Fila: Falha em Cascata
Loading diagram...

Com fila (assíncrono): o producer retorna na hora, cada consumer processa no próprio ritmo:

Processamento Assíncrono com Kafka
Loading diagram...

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
    }
}
criteriokafkarabbitsqs
ThroughputAbsurdo (milhões/s)Alto (100k/s)Alto (gerenciado)
Replay de mensagens✅ Sim❌ Não❌ Não
RetençãoConfigurável (dias/semanas)Até consumirAté 14 dias
Complexidade operacionalAltaMédiaBaixa (serverless)
Melhor paraEvent sourcing, analyticsTask queuesAWS-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.

pilarcapturaferramenta
MétricasLatência, req/s, CPU, memóriaPrometheus + Grafana
LogsErros, eventos, debugStructured JSON Logs
TracingCaminho completo de uma requestOpenTelemetry + 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.

Estados do Circuit Breaker
Loading diagram...

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:

Blue-Green Deployment
Loading diagram...

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:

Canary Release: % de tráfego na nova versão
Dia 1:██5
Dia 2:██████████25
Dia 3:██████████████████████████████75
Dia 4:████████████████████████████████████████100

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:

Arquitetura Completa: Feed de Redes Sociais
Loading diagram...

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:

Impacto de Cache na Latência (ms)
Sem Cache (busca direto no banco):████████████████████████████████████████450
Com Cache Redis (hit):8
Com Cache + CDN (edge):2

Taxa de hit esperada por componente:

Taxa de Cache Hit por Componente (%)
CDN — assets estáticos:██████████████████████████████████████95
Redis Cache (usuários, produtos):██████████████████████████████████85
Cache L1 in-process (hot data):████████████████████████████████████████99

Um exercício prático

Pega qualquer sistema que você usa todo dia — Twitter, Spotify, iFood — e responde:

  1. Requisitos não-funcionais: quantos DAU? Quantas req/s? Qual latência você aceita?
  2. Banco de dados: SQL ou NoSQL? Por quê? Precisa de joins? Consistência ou disponibilidade?
  3. Cache: o que cachear? Qual TTL? O que não pode ir pro cache?
  4. Filas: quais operações podem ser assíncronas? O que acontece se a fila atrasar?
  5. 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.

Compartilhe este artigo

Artigos Relacionados

Comments