Arquitetura por Contratos: Engenharia que Não Depende de Heróis
Boas práticas de engenharia e arquitetura são mecanismos para reduzir ambiguidade antes que ela vire custo operacional. Em vez de depender de desenvolvedores experientes lembrando tudo, um bom sistema explicita contratos, limites, métricas e formas de mudança que qualquer pessoa do time consegue verificar.
O que significa projetar por contratos?
Projetar por contratos é definir, de forma explícita, o que cada parte do sistema promete entregar e o que espera receber. O contrato pode ser uma interface TypeScript, um schema OpenAPI 3.1, uma mensagem Avro, uma migration SQL, um evento de domínio ou um objetivo de nível de serviço como p95 abaixo de 300 ms.
O ponto não é burocratizar. O ponto é evitar que comportamento importante exista apenas na cabeça de alguém. Se o endpoint POST /orders aceita pagamento pendente, estoque reservado e cupom expirado, isso precisa aparecer em tipos, testes, documentação executável ou regras de domínio, não apenas em uma conversa perdida no Slack.
Um contrato útil tem pelo menos quatro propriedades:
- É verificável: existe teste, validação de schema, checagem de tipo ou métrica que acusa quebra.
- É versionado: mudanças relevantes entram no Git e podem ser revisadas em pull request.
- É pequeno: descreve uma fronteira específica, não uma enciclopédia do sistema inteiro.
- É acionável: quando falha, alguém sabe qual serviço, módulo ou equipe precisa agir.
Quais contratos valem mais no dia a dia?
Nem todo contrato tem o mesmo retorno. Em sistemas web comuns, três tipos costumam pagar a conta rapidamente: contratos de dados, contratos de integração e contratos operacionais.
Contratos de dados definem o formato e o significado das informações centrais. Um campo status de pedido, por exemplo, não deveria aceitar qualquer string. Se os estados válidos são draft, paid, shipped e cancelled, isso deve existir no banco, no backend e no frontend como conjunto fechado. Quando cada camada inventa sua própria interpretação, bugs aparecem em relatórios, filtros e automações.
Contratos de integração protegem fronteiras entre serviços e sistemas externos. REST com OpenAPI, GraphQL com schema versionado, filas com schema registry ou webhooks assinados são formas diferentes de resolver o mesmo problema: impedir que um produtor mude a mensagem sem avisar quem consome.
Contratos operacionais dizem como o sistema deve se comportar em produção. Exemplos concretos: disponibilidade mensal de 99,9%, latência p95 menor que 500 ms no checkout, taxa de erro 5xx abaixo de 0,1% e tempo de recuperação menor que 30 minutos para incidentes críticos. Esses números não precisam ser perfeitos no começo, mas precisam existir.
Um exemplo simples em TypeScript mostra a diferença entre valor solto e contrato explícito:
type OrderStatus = "draft" | "paid" | "shipped" | "cancelled";
type Order = {
id: string;
customerId: string;
status: OrderStatus;
totalInCents: number;
createdAt: string;
};
function canCancel(order: Order): boolean {
return order.status === "draft" || order.status === "paid";
}Esse trecho não resolve arquitetura sozinho, mas impede uma classe inteira de erro. O compilador passa a participar da revisão. Se amanhã alguém adicionar refunded, a mudança força discussão sobre regras de cancelamento, relatórios e telas.
Como evitar arquitetura que trava o produto?
O erro comum é confundir contrato com rigidez. Um contrato bom protege a fronteira, mas permite evolução interna. Um módulo de cobrança pode trocar gateway, biblioteca HTTP ou estratégia de retry sem afetar o restante do produto, desde que mantenha suas entradas, saídas e garantias observáveis.
Uma prática simples é separar decisões em três categorias:
- Decisões reversíveis: nome de pasta, biblioteca de tabela, estrutura visual de uma tela. Devem ser tomadas rápido.
- Decisões caras de reverter: banco principal, protocolo entre serviços, modelo de tenancy, estratégia de autenticação. Pedem registro técnico.
- Decisões irreversíveis na prática: exposição pública de API, formato de evento consumido por parceiros, semântica de dados financeiros. Pedem versionamento e plano de migração.
Para a segunda e a terceira categoria, um ADR curto costuma bastar. ADR significa Architecture Decision Record. Um bom registro tem uma página, data, contexto, decisão, alternativas consideradas e consequências. Não precisa virar documento corporativo de 20 páginas.
Exemplo de decisão bem registrada: “Em 2026-07-17, vamos manter PostgreSQL 16 como banco transacional principal e usar Redis apenas para cache e filas efêmeras. Motivo: precisamos de consistência forte em pedidos e conciliação. Consequência: relatórios analíticos pesados devem ir para replica, warehouse ou job assíncrono.”
Essa disciplina ajuda novos devs a entenderem por que o sistema é como é. Também evita rediscutir a mesma decisão a cada trimestre sem novos dados.
Como medir se a arquitetura está funcionando?
Arquitetura saudável aparece em sinais operacionais. Se toda mudança simples exige tocar oito módulos, se incidentes dependem sempre da mesma pessoa, ou se deploy vira evento de alto risco, a arquitetura está cobrando juros.
Algumas métricas práticas ajudam a separar sensação de evidência:
- Lead time de mudança: tempo entre abrir uma tarefa e colocar a alteração em produção.
- Frequência de deploy: quantas vezes por dia ou semana o time consegue publicar com segurança.
- Change failure rate: porcentagem de deploys que causam rollback, hotfix ou incidente.
- MTTR: tempo médio para recuperar o serviço após falha.
- Acoplamento de PR: quantidade de módulos alterados para entregar uma funcionalidade pequena.
Essas métricas não servem para punir time. Servem para revelar gargalos de arquitetura. Se o change failure rate passa de 20%, talvez faltem testes de contrato, feature flags ou ambientes de validação. Se o lead time cresce sem aumento de escopo, talvez as fronteiras entre módulos estejam mal desenhadas.
Toda prática arquitetural deve melhorar algum sinal concreto. Camadas, eventos, microsserviços, monorepo, DDD, clean architecture e serverless são meios. Se a escolha não melhora capacidade de mudança, confiabilidade, custo ou entendimento, ela provavelmente é decoração técnica.
Perguntas frequentes
O que são boas práticas de engenharia de software?
São práticas que tornam o sistema mais confiável, compreensível e fácil de mudar. Exemplos incluem testes automatizados, contratos de API, revisão de código, observabilidade, versionamento e decisões arquiteturais registradas.
Qual é a diferença entre arquitetura e engenharia de software?
Arquitetura define as decisões estruturais e os limites principais do sistema. Engenharia transforma essas decisões em código, testes, deploy, operação e manutenção contínua.
Quando devo criar um ADR de arquitetura?
Crie um ADR quando a decisão for cara de reverter ou afetar várias partes do sistema. Escolha de banco, protocolo, autenticação, mensageria e modelo de dados central são bons candidatos.
Como saber se minha arquitetura está ruim?
Sinais comuns são mudanças pequenas exigindo muitas alterações, deploys arriscados, incidentes recorrentes e dependência excessiva de uma pessoa específica. Métricas como lead time, MTTR e taxa de falha em deploy ajudam a confirmar o problema.
