Arquitetura de Software na Prática: como projetar sistemas que escalam sem perder clareza
Construir software que funciona em produção é diferente de construir software que sobrevive ao crescimento.
No início, quase tudo parece simples: poucas rotas, uma fila pequena, um banco principal e deploy rápido. Com o tempo, entram novos times, mais clientes, mais integrações, mais regras de negócio e mais risco operacional. O sistema deixa de falhar por bugs óbvios e passa a falhar por decisões de arquitetura mal explícitas.
Este artigo é um mapa prático para engenheiros de software que querem evoluir de implementação para decisão técnica. O foco aqui é: escalabilidade, resiliência, consistência de dados, observabilidade, custo operacional e alinhamento com produto.
1. Arquitetura não é sobre tecnologia, é sobre restrições
A primeira pergunta em design de sistemas não é "qual framework usar?". É:
- Qual volume de tráfego preciso suportar hoje e em 12 meses?
- Qual latência máxima é aceitável para o usuário?
- O que acontece quando uma dependência externa cai?
- Que dados exigem consistência forte e quais aceitam consistência eventual?
- Qual equipe vai manter isso daqui a 2 anos?
Sem restrições explícitas, toda discussão vira preferência pessoal.
Um princípio útil:
Toda decisão de arquitetura deve declarar claramente o que ela otimiza e o que ela sacrifica.
Exemplo real de trade-off:
-
Otimizar para velocidade de entrega: menos camadas, menos abstrações, menor custo inicial.
-
Sacrifício: maior risco de retrabalho quando o domínio evoluir.
-
Otimizar para escalabilidade horizontal: filas, processamento assíncrono, particionamento.
-
Sacrifício: maior complexidade operacional, debugging mais difícil.
2. Monolito modular vs microserviços: pare de escolher por hype
microservices não são nível avançado por padrão. São apenas um modelo de distribuição de responsabilidades com custo alto.
Quando começar com monolito modular
Monolito modular costuma ser melhor quando:
- Time é pequeno ou médio.
- Domínio ainda está mudando rápido.
- Não existe maturidade operacional (SRE, observabilidade, incident response).
- O gargalo principal é descoberta de produto, não throughput técnico.
Monolito modular bem estruturado entrega:
- Deploy simples.
- Menos sobrecarga cognitiva.
- Menos custos de rede e serialização.
- Melhor produtividade no curto e médio prazo.
Quando migrar partes para serviços
Migração para serviços faz sentido quando existe:
- Limite claro de domínio e ownership.
- Necessidade real de escalar componentes de forma independente.
- Requisitos diferentes de disponibilidade/latência por contexto.
- Capacidade de operar filas, retries, tracing e contratos.
Sinal clássico de hora certa:
- A dependência entre módulos já está clara,
- e a separação reduz risco organizacional, não só risco técnico.
3. Decisões fundamentais de sistemas distribuídos
3.1 Consistência forte vs consistência eventual
Em sistemas distribuídos, você raramente tem consistência forte global sem pagar alto em latência e disponibilidade.
Use consistência forte para:
- saldo financeiro,
- controle de estoque crítico,
- garantias contratuais de negócio.
Use consistência eventual para:
- atualização de read models,
- notificações,
- analytics,
- workflows secundários.
O erro comum é tentar consistência forte em tudo. O resultado costuma ser sistema lento e acoplado.
3.2 Síncrono vs assíncrono
Síncrono (HTTP/RPC) é melhor para resposta imediata ao usuário.
Assíncrono (fila/evento) é melhor para:
- amortecer picos,
- desacoplar produtor/consumidor,
- tolerar indisponibilidade parcial,
- processar tarefas não críticas fora da request principal.
Regra prática:
- O que impacta UX imediata: caminho síncrono.
- O que pode atrasar sem prejuízo: caminho assíncrono.
3.3 Idempotência é obrigatória
Sem idempotência, qualquer retry vira risco de duplicidade de cobrança, pedido, e-mail ou evento.
Exemplo simples de chave de idempotência em TypeScript:
async function createOrder(input: CreateOrderInput, idemKey: string) {
const existing = await idemStore.get(idemKey);
if (existing) return existing;
const order = await orderRepo.insert(input);
await idemStore.set(idemKey, order, { ttlSeconds: 24 * 60 * 60 });
return order;
}
3.4 Outbox pattern para integridade entre banco e mensageria
Publicar evento diretamente após INSERT pode falhar no meio e gerar inconsistência.
Outbox pattern resolve isso persistindo dados e evento na mesma transação local.
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (...);
INSERT INTO outbox (event_type, payload, status) VALUES ('OrderCreated', '{...}', 'pending');
COMMIT;
Depois, um worker confiável publica os eventos pendentes no broker e marca como enviados.
Benefícios:
- Evita perda de eventos.
- Evita estado "pedido criado sem evento".
- Torna reprocessamento auditável.
4. Observabilidade: sem isso, escalar vira loteria
Escalabilidade sem observabilidade é aumento de risco.
O mínimo para um sistema de produção:
- Logs estruturados com
request_id,user_id,service,duration_ms. - Métricas de latência, taxa de erro, throughput e saturação.
- Tracing distribuído para mapear gargalos entre serviços.
- SLO/SLI definidos por jornada crítica de usuário.
Exemplo de métrica que importa de verdade:
checkout_success_ratecheckout_p95_latencypayment_provider_error_ratio
Sem métrica de resultado, time otimiza o que é fácil medir, não o que importa ao negócio.
5. Resiliência e engenharia de falhas
Falhas vão acontecer. A pergunta madura é: "como o sistema falha?".
Técnicas básicas e eficientes:
- Timeout explícito em chamadas externas.
- Retry com backoff exponencial e jitter.
- Circuit breaker para dependência instável.
- Bulkhead para isolar saturação por componente.
- Fallback controlado (degradação graciosa).
Exemplo de retry com backoff simples:
async function retry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
let error: unknown;
for (let i = 1; i <= attempts; i++) {
try {
return await fn();
} catch (err) {
error = err;
const delay = Math.min(1000, 100 * 2 ** i) + Math.floor(Math.random() * 50);
await new Promise((r) => setTimeout(r, delay));
}
}
throw error;
}
Importante: retry sem limite pode amplificar incidente.
6. Design orientado a produto: feature não é resultado
Times maduros não medem sucesso por quantidade de features entregues.
Medem por impacto:
- redução de tempo de onboarding,
- aumento de conversão,
- queda de incidentes,
- diminuição de lead time,
- estabilidade de operação.
Boas decisões técnicas nascem de perguntas de produto:
- Qual dor estamos resolvendo agora?
- Qual métrica prova que resolvemos?
- Qual custo operacional essa decisão cria no próximo trimestre?
Arquitetura que ignora produto vira exercício acadêmico caro.
7. Checklist prático para decisões de arquitetura
Antes de aprovar um design, valide:
- Problema e contexto estão descritos com dados?
- Existe alternativa mais simples com 80% do valor?
- Trade-offs foram explicitados (latência, custo, complexidade)?
- Plano de observabilidade está definido?
- Estratégia de falha e recuperação está clara?
- Ownership e fronteiras de domínio estão definidos?
- Há plano de rollback e reversibilidade?
- A decisão melhora o sistema para o próximo engenheiro que tocar nisso?
Se metade dessas respostas for "não", você ainda não tem um bom design.
8. Erros comuns que atrasam times seniores
- Abstração prematura antes da variação real aparecer.
- Criar microserviços para parecer moderno.
- Ignorar custo de operação (on-call, runbook, incidentes).
- Querer consistência forte global sem necessidade.
- Tratar observabilidade como tarefa futura.
- Confundir complexidade técnica com maturidade técnica.
Maturidade em engenharia de software é construir sistemas que:
- entregam valor continuamente,
- falham de forma controlada,
- e podem ser entendidos e evoluídos por outras pessoas.
Conclusão
A melhor arquitetura não é a mais sofisticada, é a que equilibra clareza, resiliência e velocidade de entrega dentro das restrições reais do produto e da equipe.
Se você quer evoluir como engenheiro sênior, mude o foco de "escrever código certo" para "tomar decisões técnicas sustentáveis".
No longo prazo, software de alta qualidade é uma combinação de:
- escolhas de arquitetura conscientes,
- excelência operacional,
- comunicação clara,
- e compromisso com aprendizado contínuo.
Esse é o tipo de engenharia que realmente escala.