Arquitetura Evolutiva: Como Projetar Sem Travar o Produto

Publicado em

Boas práticas de engenharia e arquitetura servem para reduzir o custo de mudança, não para deixar o diagrama mais bonito. Um sistema bem projetado é aquele em que uma correção, uma nova regra de negócio ou uma troca de tecnologia não exige reescrever metade do produto.

O erro comum é tratar arquitetura como uma decisão inicial e definitiva. Na prática, arquitetura saudável é um conjunto de limites, contratos e hábitos que permitem evolução contínua com risco controlado.

O que faz uma arquitetura ser evolutiva?

Arquitetura evolutiva é a capacidade de adaptar o sistema sem quebrar comportamentos importantes. Isso não significa começar com microserviços, filas, CQRS e Kubernetes. Muitas vezes, o melhor ponto de partida é um monólito modular com fronteiras internas claras.

O livro Building Evolutionary Architectures, de Neal Ford, Rebecca Parsons e Patrick Kua, popularizou a ideia de fitness functions: verificações automatizadas que medem se a arquitetura continua respeitando suas restrições. Em vez de confiar apenas em revisão manual, o time transforma regras importantes em testes.

Exemplos simples de fitness functions em um projeto Node.js ou TypeScript:

// Exemplo conceitual: impedir importacao direta entre camadas
// src/modules/billing nao deve importar src/modules/users/internal

import { describe, it, expect } from "vitest";
import { scanImports } from "./architecture-scan";

describe("architecture boundaries", () => {
  it("billing nao acessa detalhes internos de users", async () => {
    const imports = await scanImports("src/modules/billing");
    expect(imports).not.toContain("src/modules/users/internal");
  });
});

O ponto não é a ferramenta exata. O ponto é transformar intenção arquitetural em algo verificável. Se uma regra só existe em uma reunião de onboarding, ela provavelmente será quebrada quando o prazo apertar.

Diagrama de arquitetura de software em uma tela
Diagrama de arquitetura de software em uma tela

Quais práticas reduzem o custo de mudança?

A melhor arquitetura nasce de decisões pequenas e consistentes. Antes de escolher um padrão famoso, vale olhar para quatro práticas que funcionam em produtos reais.

  • Defina fronteiras por capacidade de negócio. Em vez de separar apenas por tipo técnico, como controllers, services e repositories, agrupe regras relacionadas: cobrança, autenticação, catálogo, assinatura, pedidos.
  • Use contratos explícitos. APIs REST, eventos, schemas JSON, tipos TypeScript, Protobuf ou OpenAPI reduzem ambiguidade. Um contrato versionado evita que mudanças locais virem incidentes globais.
  • Prefira acoplamento visível. Dependências escondidas em variáveis globais, singletons e callbacks mágicos são difíceis de revisar. Injeção explícita e interfaces pequenas deixam o impacto mais claro.
  • Automatize o básico. Testes de unidade, testes de contrato, lint, typecheck e CI em pull request pegam problemas antes de chegarem ao deploy.

Um número prático: se uma mudança pequena exige tocar mais de 5 a 7 arquivos em áreas diferentes do sistema, isso é um sinal para investigar acoplamento. Não é uma lei, mas é uma métrica útil em revisão de código.

Outro indicador simples é o tempo de feedback. Se o desenvolvedor precisa esperar 12 minutos para descobrir que uma regra quebrou, o time vai rodar menos testes localmente. Para a maioria dos projetos web, typecheck e testes rápidos deveriam caber em 1 a 3 minutos durante o ciclo de desenvolvimento.

Quando modularizar antes de distribuir?

Distribuir um sistema cedo demais costuma trocar complexidade de código por complexidade operacional. Um monólito modular pode ser deployado como uma única aplicação, mas organizado internamente como módulos independentes. Isso preserva simplicidade de operação e melhora a clareza do domínio.

Microserviços fazem sentido quando há necessidade real de escala independente, ciclos de deploy separados, times com autonomia operacional ou isolamento de falhas. Sem esses motivos, o custo aparece rápido: observabilidade distribuída, versionamento de APIs, latência de rede, retries, idempotência, filas, tracing e consistência eventual.

Uma regra pragmática: primeiro prove que o limite existe dentro do código. Depois pense em separar o deploy. Se o módulo de pagamentos ainda importa diretamente tabelas internas de usuários, transformá-lo em serviço separado só vai mover o acoplamento para HTTP, eventos ou banco compartilhado.

Boas fronteiras internas geralmente têm três características:

  1. Entrada clara: comandos, handlers, rotas ou casos de uso expostos ao restante do sistema.
  2. Modelo protegido: regras internas que não são importadas diretamente por outros módulos.
  3. Saída controlada: eventos, DTOs ou interfaces para integração com banco, filas e serviços externos.

Esse desenho também ajuda times pequenos. Um projeto com 3 desenvolvedores não precisa de uma plataforma complexa para se beneficiar de limites. Precisa de pastas previsíveis, nomes consistentes, testes relevantes e decisões registradas.

Como registrar decisões sem criar burocracia?

Arquitetura também é memória. Quando ninguém sabe por que uma decisão foi tomada, o time tende a repetir discussões antigas ou desfazer escolhas importantes sem perceber o contexto.

Uma prática eficiente é usar ADRs, sigla para Architecture Decision Records. Um ADR não precisa ter 8 páginas. Muitas decisões cabem em 20 a 40 linhas com contexto, decisão, alternativas consideradas e consequências.

# ADR 004: usar Postgres como banco transacional principal

## Contexto
O produto precisa de consistencia forte para pagamentos, assinaturas e auditoria.

## Decisao
Usaremos PostgreSQL 16 como banco transacional principal.

## Alternativas consideradas
- MongoDB: flexivel, mas pior para relacoes transacionais deste dominio.
- MySQL: viavel, mas o time tem mais experiencia operacional com Postgres.

## Consequencias
- Migrations passam a ser parte obrigatoria do fluxo de deploy.
- Relatorios analiticos pesados devem ir para pipeline separado.

Esse tipo de documento evita arquitetura oral. Ele também melhora onboarding: alguém novo entende o motivo de uma escolha sem depender de uma conversa perdida no Slack.

Para manter a prática leve, registre apenas decisões que tenham custo de reversão ou impacto em mais de uma área. Escolher o nome de uma função não merece ADR. Escolher banco, fila, estratégia de autenticação, padrão de módulos ou contrato público merece.

Perguntas frequentes

O que são boas práticas de engenharia de software?

São práticas que reduzem risco e custo de manutenção, como testes automatizados, revisão de código, contratos explícitos, CI, observabilidade e documentação de decisões importantes.

Qual é a diferença entre arquitetura e design de código?

Arquitetura define limites, dependências e decisões estruturais difíceis de mudar. Design de código trata da organização interna de classes, funções, módulos e abstrações no dia a dia.

Quando usar microserviços em vez de monólito?

Use microserviços quando houver necessidade comprovada de escala independente, deploy separado, isolamento de falhas ou autonomia entre times. Antes disso, um monólito modular costuma ser mais simples e eficiente.

Como melhorar a arquitetura de um sistema legado?

Comece mapeando dependências, cobrindo fluxos críticos com testes e isolando módulos por capacidade de negócio. Evite reescrita total sem métricas: refatore por partes e meça redução de acoplamento.