Pular para o conteúdo

Construindo para Escala: Padrões de Arquitetura Que Realmente Funcionam

A maioria dos conselhos sobre escalabilidade é genérica. Aqui estão os padrões que funcionaram consistentemente em sistemas reais que lidam com milhões de requisições — e aqueles que parecem bons, mas falham na prática.

Durval Pereira
Durval Pereira
5 min

O problema com os conselhos de escalabilidade

A maioria dos artigos sobre escalabilidade repete o mesmo manual: adicione caching, use uma CDN, faça sharding do seu database, torne-se async. Este conselho não está errado — está apenas incompleto. Ele pula a parte em que você precisa descobrir quais dessas coisas importam para o seu sistema, quando aplicá-las e como evitar a criação de novos problemas ao resolver os antigos.

Depois de construir e escalar sistemas em diferentes domínios — de APIs de alta vazão a data pipelines em tempo real — descobri que os padrões de escalabilidade mais úteis compartilham uma característica comum: eles reduzem o raio de impacto da falha enquanto aumentam a independência dos componentes.

Comece pelo gargalo, não pelo diagrama de arquitetura

O erro mais comum na escalabilidade é otimizar a coisa errada. Antes de redesenhar seu sistema, você precisa saber onde ele realmente falha.

// A simple but effective approach: instrument first, optimize second
import { metrics } from '@/lib/telemetry'

export async function handleRequest(req: Request) {
  const timer = metrics.startTimer('request_duration')
  const route = extractRoute(req)

  try {
    const result = await processRequest(req)
    timer.end({ route, status: 'success' })
    return result
  } catch (error) {
    timer.end({ route, status: 'error' })
    throw error
  }
}

Isso não é glamoroso. Mas saber que 80% da sua latência vem de três queries de database específicas vale mais do que qualquer diagrama de arquitetura.

Padrão 1: A read replica com roteamento inteligente

O padrão de escalabilidade mais simples que oferece resultados excepcionais. A maioria das aplicações é read-heavy — frequentemente 90% ou mais de reads. Roteamento de reads para replicas é direto, mas os detalhes de implementação importam.

interface DatabaseRouter {
  write(): DatabaseConnection
  read(consistency?: 'eventual' | 'strong'): DatabaseConnection
}

class SmartRouter implements DatabaseRouter {
  private primary: DatabaseConnection
  private replicas: DatabaseConnection[]
  private recentWrites: Map<string, number> = new Map()

  write(): DatabaseConnection {
    return this.primary
  }

  read(consistency: 'eventual' | 'strong' = 'eventual'): DatabaseConnection {
    if (consistency === 'strong') {
      return this.primary
    }
    return this.selectReplica()
  }

  private selectReplica(): DatabaseConnection {
    const healthy = this.replicas.filter((r) => r.isHealthy())
    if (healthy.length === 0) return this.primary
    return healthy[Math.floor(Math.random() * healthy.length)]
  }
}

O map recentWrites é fundamental. Depois que um usuário escreve dados, você roteia suas reads para o primary por um curto período para evitar inconsistência de read-your-writes. Este é o tipo de detalhe que os conselhos genéricos de escalabilidade perdem.

Padrão 2: Caching em camadas com invalidação explícita

Caching é fácil. A invalidação de cache é o problema real. A abordagem mais robusta que encontrei usa camadas explícitas com propriedade clara.

Camada 1 — Nível de Requisição: Memoize dentro de uma única requisição. Risco zero, benefício massivo para computações repetidas.

Camada 2 — Nível de Aplicação: Cache em memória (como um map TTL) para dados quentes. Rápido, mas requer dimensionamento cuidadoso.

Camada 3 — Cache Distribuído: Redis ou Memcached para estado compartilhado. Adiciona um salto de rede, mas escala horizontalmente.

Camada 4 — Edge da CDN: Para conteúdo estático e semi-estático. A camada mais eficaz para APIs públicas read-heavy.

O erro que a maioria das equipes comete é pular direto para a Camada 3 ou 4 sem esgotar o valor das Camadas 1 e 2. Um cache em memória com um TTL de 30 segundos pode eliminar 95% das queries de database idênticas com custo zero de infraestrutura.

Padrão 3: Backpressure como uma funcionalidade

A maioria dos sistemas falha não porque um componente é lento, mas porque um componente lento é sobrecarregado por um produtor rápido. Backpressure — a capacidade de um consumidor sinalizar que não consegue acompanhar — é essencial em escala.

class BoundedQueue<T> {
  private queue: T[] = []
  private waiters: Array<(value: T) => void> = []

  constructor(private maxSize: number) {}

  async enqueue(item: T): Promise<boolean> {
    if (this.queue.length >= this.maxSize) {
      return false // Signal backpressure
    }

    if (this.waiters.length > 0) {
      const waiter = this.waiters.shift()!
      waiter(item)
    } else {
      this.queue.push(item)
    }
    return true
  }

  async dequeue(): Promise<T> {
    if (this.queue.length > 0) {
      return this.queue.shift()!
    }

    return new Promise((resolve) => {
      this.waiters.push(resolve)
    })
  }
}

Quando enqueue retorna false, o produtor sabe que deve recuar. Isso é muito melhor do que filas ilimitadas que consomem memória até o processo falhar, ou circuit breakers que descartam requisições sem que o produtor saiba o motivo.

O que não funciona

Alguns padrões que parecem razoáveis, mas consistentemente causam problemas:

Microsserviços prematuros. Se sua equipe não consegue implantar um monólito de forma confiável, microsserviços não ajudarão — eles multiplicarão seus problemas operacionais. Comece com um monólito modular bem estruturado.

Databases compartilhados entre serviços. Isso cria um acoplamento invisível que torna a escalabilidade independente impossível. Se dois serviços compartilham um database, eles não são serviços separados — são um monólito distribuído.

Excesso de dependência em processamento async. Tornar tudo async pode criar pesadelos de depuração e problemas de experiência do usuário. Algumas operações devem ser síncronas e rápidas.

O meta-padrão

O verdadeiro padrão de escalabilidade não é uma técnica única. É este: torne os componentes independentemente implantáveis, independentemente escaláveis e independentemente observáveis. Cada padrão concreto — read replicas, caching, queuing, sharding — é apenas uma aplicação específica deste princípio.

Ao avaliar qualquer proposta de escalabilidade, faça três perguntas:

  1. Isso reduz o acoplamento entre os componentes?
  2. Isso torna a falha mais localizada?
  3. Posso observar e depurar isso independentemente?

Se a resposta for não para qualquer uma dessas, o padrão pode resolver seu problema imediato, mas criar um mais difícil depois.


Este é o primeiro de uma série sobre design de sistemas práticos. Próximo: como pensar sobre decisões de modelagem de dados que escalam.

Tagsscalingdistributed-systemssystem-designpatterns