Cada uma das decisões abaixo é tomada cedo num projeto e raramente
revisitada — vira parte do "jeito que se faz aqui". Vale articular o
trade-off enquanto ainda há liberdade para escolher.
Middleware no pipeline ou interceptor por método?
Middleware HTTP é o lugar natural para concerns que se aplicam a
todo request: autenticação, logging de acesso, CORS,
correlation ID, métricas de duração de request. Interceptor (ou
filter, ou decorator de método) é melhor quando o concern depende
de qual operação está sendo executada: cache de método
específico, retry de chamada de domínio, autorização baseada em
recurso. Misturar os dois confunde: uma equipe que coloca
autorização em middleware e em filter no mesmo controlador
acaba duplicando regra. Escolha por escopo: borda → middleware,
operação → interceptor.
AOP via framework (Spring, Castle) ou composição explícita?
AOP via framework brilha quando o concern é genuinamente repetitivo
em todas as classes/métodos de uma camada — caso clássico
é transação por método de application service. Composição explícita
(decorators, higher-order functions, middleware) ganha quando os
aspects são poucos, bem nomeados, e quando a equipe valoriza poder
ler o caminho da chamada sem rodar debugger. Em Go, a comunidade
optou pela segunda via quase em peso — geração de código quando
precisa, função wrapper quando dá. Em .NET e Java, frameworks
ganharam tração porque os ecossistemas pagaram o custo de
ferramental para tornar a magia inspecionável (Spring Tools, profiler).
Heurística: se você não consegue desenhar no quadro o que vai
executar, está errando o nível de magia.
Logging estruturado: campos fixos ou contexto livre?
Estruturado significa que cada log entry é um objeto com chaves
tipadas, não uma string formatada. A pergunta seguinte é se o
schema é fixo (um conjunto de campos definidos por contrato) ou
livre (cada equipe adiciona o que faz sentido). A prática que
envelhece bem é um core fixo — timestamp,
level, service, trace_id,
span_id, user_id quando aplicável — e
uma área livre por contexto, validada por linter no CI. Sem o
core fixo, ferramentas de busca (Grafana Loki, Elasticsearch)
viram menos eficazes; sem a área livre, equipes começam a esconder
informação dentro da mensagem.
Retry: na borda do cliente ou em policy global?
Retry é tentador colocar globalmente — "todo HTTP client retenta
3x" — e é exatamente assim que se constrói uma retry storm
que derruba o serviço chamado. A regra: retry pertence à chamada
que sabe se a operação é idempotente. GET de leitura
pode retentar livremente; POST que cria recurso só
retenta com idempotency key. Por isso vale ter policy nomeada
(read-idempotent, write-idempotent,
no-retry) e exigir que o autor da chamada escolha,
em vez de aplicar default global. Polly em .NET e tenacity em
Python suportam esse modelo; em Go, escreve-se policy à mão
mesmo, e isso é uma feature, não bug.
Validação: na borda HTTP ou no domínio?
A resposta amadurecida é "ambos, com responsabilidades distintas".
A borda valida forma: campos obrigatórios, tipos, faixas,
formato de e-mail. Pydantic e FluentValidation fazem isso bem, e
falham rápido com mensagens amigáveis para o cliente HTTP. O
domínio valida invariantes: que o saldo não fica negativo,
que a data de fim é depois da data de início, que a transição de
estado é válida. Se você só valida na borda, qualquer caminho que
não passa por HTTP (job, mensagem, teste) vira fonte de bug. Se só
no domínio, a API responde 500 para input absurdo. As duas camadas
existem porque protegem coisas diferentes.