System Design

Escalabilidade Horizontal vs Vertical

Duas estratégias fundamentais para lidar com crescimento de carga: scale up (mais poder na mesma máquina) e scale out (mais máquinas fazendo o mesmo trabalho). Entender os limites e trade-offs de cada abordagem é uma das decisões de infraestrutura mais impactantes em qualquer sistema.

Intenção

Escalabilidade é a capacidade de um sistema de lidar com crescimento de carga sem degradação de performance. Quando um sistema aproxima-se do seu limite de capacidade — seja em número de requisições por segundo, volume de dados processados ou conexões simultâneas — há duas estratégias para expandir essa capacidade: crescer verticalmente (uma máquina mais poderosa) ou crescer horizontalmente (mais máquinas).

Scale up (vertical) significa adicionar recursos à mesma instância: mais CPU, mais RAM, discos mais rápidos, rede mais veloz. A aplicação não muda; a infraestrutura que a suporta cresce. Scale out (horizontal) significa adicionar novas instâncias da mesma aplicação e distribuir a carga entre elas por meio de um load balancer. A infraestrutura não precisa crescer de forma monolítica, mas a aplicação precisa ser capaz de rodar em múltiplas cópias simultaneamente sem inconsistências.

Problema

Todo sistema tem um teto de capacidade. À medida que a carga cresce — mais usuários, mais dados, mais chamadas por segundo — surgem sintomas concretos: latência aumentando, timeouts, erros de memória, CPU saturada. A questão não é se esse teto será atingido, mas quando e como o sistema vai responder a ele.

  • Limite físico do scale up: existe um hardware máximo disponível no mercado. Mesmo com orçamento ilimitado, há um ponto em que uma única máquina não consegue processar mais carga — seja por limite de CPU, de I/O ou por contenção de locks em recursos compartilhados.
  • Custo não linear: dobrar os recursos de uma máquina não custa o dobro. O preço cresce de forma exponencial à medida que se aproxima do topo da linha de hardware. Um servidor de 256 cores custa muito mais do que oito servidores de 32 cores cada com capacidade equivalente agregada.
  • Ponto único de falha: com uma única instância, qualquer falha de hardware ou manutenção planejada tira o sistema do ar. Não há redundância natural.
  • Estado local como obstáculo ao scale out: aplicações que guardam estado em memória (sessão HTTP, cache local, arquivos temporários) funcionam bem com uma instância, mas produzem comportamento incorreto quando múltiplas instâncias precisam servir o mesmo usuário.

Como funciona

  SCALE UP (Vertical)                      SCALE OUT (Horizontal)

  ┌───────────────────────────┐            ┌───────────────────────────────┐
  │      Servidor único        │            │         Load Balancer          │
  │                            │            └──────────┬────────────────────┘
  │  CPU: 4 → 16 → 64 cores   │                       │ distribui requisições
  │  RAM:  8 → 32 → 256 GB    │            ┌──────────┼──────────────┐
  │  Disco: HDD → SSD → NVMe  │            ▼           ▼              ▼
  │                            │        ┌────────┐ ┌────────┐  ┌────────┐
  │  ── mesma instância ──     │        │  App 1 │ │  App 2 │  │  App 3 │
  └───────────────────────────┘        └────────┘ └────────┘  └────────┘
                                        (mesma imagem, instâncias iguais)

  Limite: hardware máximo disponível       Limite: custo e complexidade
  Ponto de falha: único                    Ponto de falha: distribuído (resiliente)
  Mudança de código: nenhuma               Mudança de código: exige stateless
  Elasticidade: não                        Elasticidade: auto-scaling nativo

Stateless vs Stateful: a pré-condição do scale out

Scale out exige que a aplicação seja stateless: cada requisição deve poder ser processada por qualquer instância sem depender de dados armazenados localmente na instância anterior. Uma aplicação que armazena sessão do usuário em memória, grava arquivos localmente ou mantém conexões com estado não pode ser escalada horizontalmente sem quebrar o comportamento.

A solução é externalizar o estado para serviços compartilhados acessíveis por todas as instâncias:

  • Sessão HTTP: Redis, Memcached ou banco de dados compartilhado em vez de memória local.
  • Arquivos temporários: object storage (S3, GCS) ou volume de rede compartilhado (EFS, NFS) em vez do sistema de arquivos local.
  • Cache: Redis ou Memcached centralizados — cache local por instância é ineficiente e produz inconsistências entre instâncias.
  • Conexões com estado (WebSockets, SSE): exigem que o load balancer use sticky sessions ou que o estado da conexão seja gerenciado por um broker centralizado, como Redis Pub/Sub.

Elasticidade: o benefício exclusivo do scale out

Scale out permite auto-scaling: adicionar instâncias quando a carga sobe e removê-las quando a carga cai, pagando apenas pela capacidade em uso. Isso é especialmente valioso para cargas elásticas — sistemas com picos previsíveis (tráfego de fim de dia, campanhas de marketing, fechamento mensal) ou imprevisíveis (conteúdo viral, falha de concorrente). Com scale up, a máquina grande permanece ligada e cobrada mesmo em períodos de baixa carga, ou precisa ser redimensionada manualmente — operação que geralmente exige janela de manutenção.

Quando usar

  • Scale up — banco de dados relacional: bancos como PostgreSQL e MySQL são difíceis de distribuir horizontalmente de forma transparente. Scale up é a rota mais direta para aumentar capacidade — mais RAM para buffers, mais cores para queries paralelas, SSDs mais rápidos para I/O.
  • Scale up — ferramentas legadas e aplicações stateful: quando o código não pode ser modificado para suportar múltiplas instâncias, scale up aumenta capacidade sem mudança de arquitetura.
  • Scale up — spike temporário e previsível: se o sistema precisa de mais capacidade por um período curto e definido, provisionar uma máquina maior temporariamente pode ser mais simples do que configurar e operar um cluster.
  • Scale out — serviços web stateless: APIs REST e aplicações HTTP sem estado local são candidatas naturais. Qualquer instância pode responder qualquer requisição, tornando o balanceamento trivial.
  • Scale out — alta disponibilidade: múltiplas instâncias eliminam o ponto único de falha. Se uma instância cai, as demais absorvem a carga sem interrupção visível para o usuário.
  • Scale out — tráfego imprevisível ou elástico: auto-scaling reage a variações de carga em minutos, sem intervenção manual e com custo proporcional ao uso real.
  • Scale out — múltiplas regiões geográficas: instâncias em regiões distintas reduzem latência para usuários globais e oferecem redundância geográfica.

Quando evitar

  • Evitar scale up exclusivo em sistemas críticos: uma única instância, por maior que seja, é um ponto único de falha. Para sistemas que não podem ter downtime, scale up isolado não é suficiente — é preciso ao menos uma instância de standby.
  • Evitar scale out sem tornar a aplicação stateless primeiro: adicionar instâncias de uma aplicação stateful resolve nada e introduz bugs difíceis de reproduzir, onde o comportamento varia dependendo de qual instância atende a requisição.
  • Evitar escalar antes de medir: escalar é caro operacionalmente e financeiramente. Identifique o gargalo real — CPU, memória, I/O, queries ineficientes — antes de provisionar mais recursos. Frequentemente a solução é otimização de código ou adição de cache, não mais hardware.

Prós e contras

Scale up — Prós

  • Nenhuma mudança no código ou na arquitetura da aplicação.
  • Operacionalmente simples: uma instância, sem sincronização entre nós.
  • Latência interna zero: sem serialização de chamadas entre instâncias.
  • Transações ACID simples: sem necessidade de coordenação distribuída.

Scale up — Contras

  • Limite físico absoluto: o hardware mais poderoso disponível tem um teto.
  • Custo cresce de forma não linear próximo ao topo de linha.
  • Ponto único de falha: qualquer falha de hardware ou manutenção derruba o sistema.
  • Sem elasticidade: a máquina está ligada e cobrada mesmo em períodos de baixa carga.

Scale out — Prós

  • Sem limite teórico de capacidade: adicione instâncias conforme a demanda.
  • Alta disponibilidade natural: múltiplas instâncias eliminam o ponto único de falha.
  • Elasticidade: auto-scaling adiciona e remove instâncias sob demanda, com custo proporcional ao uso.
  • Instâncias menores e homogêneas são mais baratas e fáceis de substituir do que máquinas de topo de linha.

Scale out — Contras

  • Exige que a aplicação seja stateless — mudança de código e arquitetura.
  • Maior complexidade operacional: load balancer, health checks, drain de conexões, configuração de cluster.
  • Problemas distribuídos: consistência de cache, sessões compartilhadas, coordenação entre instâncias.
  • O banco de dados vira gargalo se não for escalado junto com a camada de aplicação.

Armadilhas comuns

1. Guardar estado em memória local e tentar escalar horizontalmente depois

A armadilha mais frequente: uma aplicação armazena sessão do usuário em memória local — um HashMap, uma variável de módulo, um objeto singleton com estado — e, quando chega a hora de escalar, o comportamento quebra silenciosamente. Usuários cujas requisições chegam a instâncias diferentes perdem sessão, veem dados de outra sessão ou recebem respostas inconsistentes. Corrigir retroativamente exige refatorar a lógica de estado espalhada por toda a aplicação.

Regra prática: projete a aplicação como stateless desde o início. Guarde estado em Redis ou banco mesmo quando ainda há uma única instância. O custo de fazer certo desde o começo é muito menor do que refatorar depois sob pressão.

2. Escalar verticalmente até o limite sem perceber que o código é bloqueante

Mais CPU e RAM não ajudam uma aplicação com thread único que aguarda I/O de forma síncrona e bloqueante. O servidor tem 32 cores ociosos enquanto a thread principal espera uma query de banco terminar ou uma chamada externa responder. Antes de escalar qualquer coisa, identifique se o gargalo é recurso (CPU, RAM, disco) ou arquitetura (código síncrono, queries N+1, ausência de cache). Escalar não substitui otimização.

3. Escalar a aplicação sem escalar o banco de dados

É comum adicionar instâncias de aplicação e descobrir que o banco de dados vira o novo gargalo. Dez instâncias de aplicação fazendo queries concorrentes num banco que antes servia uma instância pode saturar o pool de conexões, a CPU do banco ou o disco. Scale out da camada de aplicação sem uma estratégia para o banco — réplicas de leitura, pool de conexões externo como PgBouncer, caching de resultados ou sharding — simplesmente troca um gargalo por outro.

4. Tratar auto-scaling como substituto de otimização de código

Auto-scaling é uma ferramenta para absorver variações de carga, não para compensar código ineficiente. Uma query N+1 que faz 500 chamadas ao banco por requisição vai custar 500 vezes mais caro com dez instâncias do que com uma. O custo de infraestrutura cresce proporcionalmente ao desperdício de código. Monitore latência e custo por requisição — não apenas uptime e disponibilidade.

Arquiteturas e padrões relacionados

Load Balancing é o mecanismo que torna o scale out possível na prática: o load balancer distribui as requisições entre as instâncias, detecta falhas via health checks e drena conexões de instâncias sendo removidas. Sem um load balancer, escalar horizontalmente é apenas ter múltiplas instâncias inacessíveis de forma unificada.

A decisão entre Monolito e Microsserviços está diretamente ligada à estratégia de escalabilidade. Microsserviços permitem escalar partes do sistema de forma independente — o serviço de pagamento pode ter mais instâncias do que o serviço de relatórios — enquanto um monolito exige escalar tudo junto, mesmo que o gargalo seja apenas um módulo específico.

Caching é frequentemente o primeiro passo antes de escalar: reduzir a carga no banco e no servidor de aplicação com camadas de cache pode adiar ou eliminar a necessidade de scale out, ao custo de lidar com invalidação de cache e consistência eventual.