Edsger Dijkstra escreveu em 1974 um ensaio curto chamado On the role of scientific thought, distribuído como manuscrito numerado EWD447 antes de virar capítulo de livro. Ali ele cunhou uma expressão que, mais que qualquer outra, virou refrão da engenharia de software moderna: separation of concerns. O argumento de Dijkstra era simples e radical: a única ferramenta intelectual que temos contra a complexidade é estudar um aspecto de cada vez, e isolá-lo mentalmente do resto. Não porque os aspectos sejam de fato independentes — eles raramente são — mas porque a mente humana precisa que eles pareçam independentes para conseguir raciocinar.
Vinte e três anos depois, em 1997, Gregor Kiczales e equipe no Xerox PARC publicaram um paper que dava nome a um sintoma específico do fracasso da separação: cross-cutting concerns. A observação empírica era inquietante. Mesmo sistemas projetados com cuidado, com módulos bem nomeados e responsabilidades bem distribuídas, apresentavam um padrão recorrente — havia sempre algumas preocupações que se recusavam a caber em um módulo só. Logging, autenticação, transação, instrumentação. Cada uma delas atravessava todos os módulos do sistema, deixando cicatrizes em forma de boilerplate repetido. Tentar empurrar logging para um "módulo de logging" não eliminava o problema: o módulo continha o logger, mas as chamadas ao logger continuavam espalhadas por toda parte.
Esse é o nó deste conceito, e do módulo inteiro. Cross-cutting concerns não são uma categoria de "coisas chatas que fazemos depois do código de domínio". São preocupações que têm uma topologia diferente do resto da arquitetura — não cabem na decomposição hierárquica clássica porque atravessam ortogonalmente as camadas. A taxonomia importa porque cada tipo de cross-cutting concern responde a um conjunto distinto de soluções: alguns pedem middleware, outros pedem decorator, outros pedem proxy dinâmico, outros pedem apenas composição explícita. Antes de aprender as soluções (conceitos 02 a 14), vale entender o problema com precisão.
A consequência prática é direta. Você terá ao longo da carreira uma escolha recorrente: misturar essas preocupações ao código de negócio, ou separá-las. Misturar é mais rápido no curto prazo — cinco linhas de log no início de cada método não custam nada hoje. Em três anos, esse mesmo sistema tem trinta mil linhas de log espalhadas, três formatos incompatíveis de timestamp, e ninguém consegue achar onde cada decisão foi tomada porque a regra de negócio está afogada em ruído. Esse capítulo é sobre ter o vocabulário para identificar isso antes que aconteça.
O que torna um concern cross-cutting
"Concern" no vocabulário de Kiczales significa qualquer preocupação que o sistema precisa atender — desde uma regra de negócio até uma garantia operacional. Concerns são as unidades naturais de raciocínio sobre o que o sistema faz. Calcular o frete de um pedido é um concern. Persistir o pedido é outro. Autenticar quem pediu é outro. Registrar a tentativa em log estruturado é outro. Garantir que a operação seja idempotente em caso de retry é outro.
A maioria dos concerns é local: cabe em um módulo, classe,
ou função. Calcular frete vive em uma classe FreteService;
validar CPF vive em uma classe CpfValidator. Você
pode mudar a fórmula do frete sem tocar em nada que não seja o
FreteService. A localidade é o que torna a
decomposição hierárquica útil — e é exatamente o que cross-cutting
concerns rompem.
Um concern é cross-cutting quando atende simultaneamente a duas condições. Primeiro, ele precisa estar presente em muitos lugares do sistema — não em um, dois ou três, mas em uma fração grande dos pontos de execução. Segundo, ele responde a uma política única: a regra de logging do sistema é a mesma em todos os lugares (formato JSON, campos obrigatórios), a regra de autenticação é a mesma (validar token JWT, extrair claims), a regra de transação é a mesma (uma transação por request). Quando essas duas condições se encontram, você tem o sintoma clássico: muitas instâncias do mesmo código de borda colado em pedaços diferentes da base.
Há uma forma rigorosa de pensar nisso, formulada por Tarr, Ossher, Harrison e Sutton em 1999 num paper com título contundente: N Degrees of Separation: Multi-Dimensional Separation of Concerns. O argumento dos autores é que toda decomposição tradicional — funcional, orientada a objetos, orientada a serviços — escolhe uma dimensão privilegiada para organizar o código. Eles chamam isso de tirania da decomposição dominante. Concerns que cabem nessa dimensão ficam claros; concerns que atravessam ortogonalmente — em outras palavras, cross-cutting — ficam sempre dispersos, não importa o quão cuidadosa seja a arquitetura.
Os dois sintomas: scattering e tangling
O paper original de Kiczales identifica dois sintomas que sempre aparecem juntos quando há cross-cutting concern mal organizado. Vale aprender o vocabulário porque ele aparece em revisões de código de gente sênior e em literatura clássica.
Scattering (dispersão) é a propriedade de um
mesmo concern aparecer em muitos lugares do código.
Logging é o exemplo canônico: a mesma instrução
log.Info("entrou no método X com parâmetros Y")
aparece em centenas de métodos. Cada uma dessas instruções é
idêntica em estrutura, varia apenas no nome do método e nos
parâmetros logados. Mudar a política de logging — adicionar
correlation ID, mudar o formato do timestamp, rebaixar logs
Info para Debug em produção — exige
passar por todas as ocorrências.
Tangling (entrelaçamento) é o lado oposto da
mesma moeda: um único método contém código de muitos concerns
misturados. Um CriarPedidoHandler que tem cinco
linhas de validação, três de logging, duas de abertura de
transação, uma de incrementar métrica, uma de iniciar span de
tracing, sete de regra de negócio, três de logging de saída e
duas de fechamento de transação — é tangling. As sete linhas
que importam estão afogadas em vinte que poderiam estar em outro
lugar.
Note que os dois sintomas se reforçam. Tangling em um método vira scattering quando a mesma estrutura aparece em todos os métodos do mesmo tipo. O custo composto é caro: o leitor tem dificuldade de identificar a regra de negócio dentro do método (tangling), e dificuldade de identificar a regra do concern transversal entre métodos (scattering). Mudanças simples em qualquer dos dois eixos viram refatoração ampla.
O sinal mais claro de cross-cutting mal tratado é a frase "todo método novo precisa lembrar de…". Se a equipe escreve em documento interno "lembre-se de logar entrada e saída, abrir transação, validar permissão e incrementar métrica em todo handler novo", a base já está tangled. Cada nova feature depende de um humano executar um checklist que devia ser responsabilidade do framework. Esquecer um item é só questão de tempo, e quando o esquecimento for o item de auditoria ou autorização, vira incidente.
Uma taxonomia útil — o que costuma ser cross-cutting
Há um conjunto recorrente de concerns que aparece como cross-cutting em quase todo sistema de aplicação não-trivial. A lista abaixo não é exaustiva nem definitiva — alguns sistemas têm cross-cutting concerns próprios, derivados do domínio (uma plataforma de pagamento tem auditoria regulatória como cross-cutting; uma de jogos tem anti-cheat). Mas os blocos a seguir cobrem o que se vê em 90% das bases de aplicação corporativa.
Logging e diagnóstico
Registrar o que o sistema fez, com que dados, em que momento, com que resultado. É o cross-cutting mais óbvio porque a política é uniforme — o formato do log e o conjunto de campos obrigatórios raramente variam entre módulos — e a aplicação é universal: quase toda operação de domínio precisa ser registrada de alguma forma. Voltamos a logging em profundidade no conceito 07.
Observabilidade — métricas, traces, eventos
Distinto de logging porque tem outra finalidade. Logging conta eventos discretos para humanos lerem; métricas e traces são instrumentação numérica para sistemas de monitoramento. OpenTelemetry, Prometheus, Datadog, e a infraestrutura moderna de observabilidade vivem aqui. É o cross-cutting que mais cresceu em importância na última década, à medida que sistemas distribuídos viraram default. Conceito 08 é dedicado a observabilidade como aspect.
Autenticação e autorização
Verificar identidade ("quem está chamando?") e permissão ("essa identidade pode fazer essa operação?") são duas perguntas distintas que aparecem em quase toda chamada que cruza fronteira de segurança. Autenticação tende a ser global — todo request passa pela mesma rotina. Autorização costuma ser por operação — cada endpoint tem regra própria de quem pode chamar. Conceito 11 separa as duas.
Validação de entrada
Verificar que dados de entrada respeitam forma e regras antes de o domínio recebê-los. Validar e-mail tem formato válido, valor é positivo, campo obrigatório está presente, faixa de data é coerente. Validação tem uma sutileza importante: ela é cross-cutting na borda do sistema (HTTP, mensageria) mas é domínio no núcleo (invariantes de negócio). Separar as duas é uma das decisões mais consequentes do design — e está no conceito 13.
Transação e Unit of Work
Garantir que conjuntos de operações no banco aconteçam atomicamente. Uma transação por request HTTP é o padrão clássico em aplicações corporativas. Existe a sub-armadilha das ambient transactions — escopos transacionais implícitos que parecem mágica até quebrarem. Conceito 12 trata isso.
Resiliência — retry, timeout, circuit breaker
Lidar com falha temporária de dependência externa: rede caiu, banco está lento, fornecedor terceiro está degradado. Retry com backoff, timeout configurável, circuit breaker que abre quando a taxa de erro ultrapassa limiar. É cross-cutting clássico porque a política é única — todas as chamadas de tipo X usam a mesma estratégia — mas a aplicação está em todo cliente externo. Conceito 09.
Cache
Memorizar resultado de operação cara para evitar refazê-la. É cross-cutting quando a decisão de cachear é uniforme ("toda leitura desse tipo é cacheada por 30 segundos"). Vira local — e perigoso — quando virou política diferente em cada lugar. Invalidação é o problema mais difícil do cache, e quase sempre o motivo de bug. Conceito 10.
Tratamento de erro e mapeamento
Converter exceções de baixo nível em respostas adequadas para o
consumidor. Em uma API HTTP, isso significa traduzir
NotFoundException em 404,
ValidationException em 400,
InsufficientFundsException em 402 ou
409 — sem que cada handler precise repetir o
mapeamento. É cross-cutting clássico de borda HTTP.
Internacionalização e formatação
Formatar moeda, data, número de acordo com locale do consumidor, traduzir mensagens. Aparece em quase toda interface de saída, e cabe mal em qualquer módulo específico. Em sistemas que servem múltiplos países, é cross-cutting de primeira classe; em sistemas mono-locale, costuma ser tratado por convenção sem virar aspect formal.
Auditoria regulatória
Em domínios regulados (financeiro, saúde, governo), há requisito legal de registrar quem fez o quê, quando, com que autorização. Auditoria difere de logging porque o destino é compliance, não diagnóstico — e o requisito é "não pode falhar silenciosamente". Quase sempre vira aspect dedicado, separado do logging operacional.
O mesmo método, três formas de tratar
Para concretizar o problema, considere um método de domínio simples: criar um pedido. A regra de negócio cabe em quatro ou cinco linhas. Mas em um sistema real, esse método precisa atender vários cross-cutting concerns ao mesmo tempo. Veja como cada ecossistema lida com a tensão entre clareza do domínio e atendimento aos concerns transversais.
public async Task<Pedido> CriarPedido(CriarPedidoCmd cmd, ClaimsPrincipal user)
{
_logger.LogInformation("criando pedido {Cmd}", cmd);
if (!user.HasClaim("permissao", "pedido:criar"))
throw new UnauthorizedAccessException();
var validation = _validator.Validate(cmd);
if (!validation.IsValid) throw new ValidationException(validation.Errors);
using var tx = _db.Database.BeginTransaction();
try
{
var sw = Stopwatch.StartNew();
var pedido = new Pedido(cmd.ClienteId, cmd.Itens);
_db.Pedidos.Add(pedido);
await _db.SaveChangesAsync();
tx.Commit();
_metrics.RecordHistogram("pedido.criar.ms", sw.ElapsedMilliseconds);
_logger.LogInformation("pedido criado {Id}", pedido.Id);
return pedido;
}
catch (Exception ex)
{
_logger.LogError(ex, "falha criar pedido");
_metrics.IncrementCounter("pedido.criar.erro");
tx.Rollback();
throw;
}
}
Vinte e duas linhas para uma operação cuja regra de negócio são duas: instanciar o pedido e persistir. Logging, autorização, validação, transação, métricas e tratamento de erro são todos cross-cutting concerns que poderiam estar fora do método. Multiplicado por cinquenta handlers, são mil linhas de boilerplate idêntico para manter coerentes à mão.
@router.post("/pedidos", response_model=PedidoOut)
async def criar_pedido(
cmd: CriarPedidoCmd, # Pydantic valida na borda
user: User = Depends(require_permission("pedido:criar")),
uow: UnitOfWork = Depends(get_uow), # transação por request
) -> Pedido:
pedido = Pedido(cliente_id=cmd.cliente_id, itens=cmd.itens)
await uow.pedidos.add(pedido)
await uow.commit()
return pedido
Validação está no Pydantic (borda HTTP), autorização no
Depends, transação no UnitOfWork
via Depends com escopo de request. Logging e
métricas vivem em middleware FastAPI configurado uma vez
no app.add_middleware(...). O handler tem
quatro linhas, e cada uma fala da regra de negócio.
// router setup (uma vez no main.go)
r.Use(middleware.CorrelationID)
r.Use(middleware.StructuredLog)
r.Use(middleware.Metrics)
r.Use(middleware.Auth)
r.With(middleware.RequirePermission("pedido:criar")).
Post("/pedidos", h.CriarPedido)
// handler
func (h *Handlers) CriarPedido(w http.ResponseWriter, r *http.Request) {
var cmd CriarPedidoCmd
if err := decodeAndValidate(r, &cmd); err != nil {
respondError(w, err)
return
}
pedido, err := h.uc.CriarPedido(r.Context(), cmd)
if err != nil {
respondError(w, err)
return
}
respondJSON(w, http.StatusCreated, pedido)
}
Em Go a comunidade prefere explicitude: middlewares
aplicados no roteador, transação injetada no use case via
context.Context ou parâmetro. O handler ainda
tem mais linhas que o equivalente Python, mas todas elas
são adapter — codificar/decodificar HTTP. A regra de
negócio mora em h.uc.CriarPedido, separada e
testável sem HTTP.
O eixo ortogonal — por que decomposição hierárquica não basta
David Parnas publicou em 1972 um dos artigos mais citados da computação: On the Criteria To Be Used in Decomposing Systems into Modules. Parnas argumenta que módulos devem ser decompostos por decisões de design que escondem umas das outras — não por etapas do fluxo de processamento. O critério de "encapsular o que muda junto" virou pedra angular do design orientado a objetos e da arquitetura limpa.
O princípio é correto, mas insuficiente. Concerns transversais
mudam juntos entre si — quando você muda a política
de logging, muda em todos os lugares — e ao mesmo tempo são
ortogonais à decomposição de domínio. Não há módulo
onde "logging" caiba sem deixar resto: você pode ter um
LoggingService, mas as chamadas a ele
continuam espalhadas. O critério de Parnas falha aqui não por
ser errado, e sim por presumir que existe uma única dimensão
de decomposição. Cross-cutting concerns são a evidência
empírica de que essa premissa é ingênua.
A solução conceitual é separar as dimensões. Mantenha a decomposição de domínio limpa — entidades, agregados, serviços de domínio — e adicione uma camada lateral, ortogonal, onde os concerns transversais vivem. Middleware, interceptor, decorator, aspect, behavior, filter — cada framework dá um nome diferente, mas a ideia é a mesma: um lugar onde escrever a política do concern uma vez, aplicada a muitos pontos do sistema sem que cada ponto precise saber.
Heurística de classificação — quando vale virar aspect?
Nem todo concern é candidato a virar aspect. Promover algo a aspect tem custo: introduz indireção, dificulta debug, esconde ordem de execução. A pergunta certa é "esse concern tem topologia cross-cutting de verdade?", e as quatro verificações abaixo ajudam a responder.
Universalidade. O concern aplica a quantos pontos do sistema? Se aparece em três lugares, talvez ainda seja melhor extrair função e chamar nos três. Se aparece em todos os handlers de uma camada, é candidato real a aspect. Logging, auth, métricas e correlation ID quase sempre passam esse filtro.
Uniformidade da política. A regra é a mesma
em todos os lugares onde o concern aparece? Se sim, aspect
faz sentido. Se há variação ("algumas chamadas usam log
info, outras debug, outras nem
logam"), aspect vira misto e a complexidade aumenta. Em geral
o sinal de "cabe em aspect" é a frase "queremos que todo
X faça Y".
Ortogonalidade ao domínio. O concern fala de domínio ou de plumbing? Logging é plumbing — não muda quando a regra de negócio muda. Cálculo de frete é domínio. Aspect cabe bem para plumbing; aplicado a domínio vira anti-padrão (regra escondida que ninguém vê ao ler o código).
Estabilidade da política. A regra do concern muda com qual frequência? Concerns que mudam a cada release — especialmente regras de negócio — são maus candidatos a aspect, porque cada mudança vira mexida em código longe do handler que vai ser afetado. Concerns que mudam raramente (formato de log, política de autenticação) são bons candidatos.
Se você responde "sim" às quatro perguntas — universal, uniforme, ortogonal ao domínio, estável —, vira aspect. Se responde "não" a qualquer uma, vire função explícita chamada onde precisa. A pior decisão é virar aspect quando as condições não estão atendidas: você ganha a complexidade do aspect e perde a explicitude da chamada, sem o benefício da uniformidade.
Onde vivem os cross-cutting concerns na arquitetura
Em uma aplicação organizada por camadas — borda HTTP, aplicação, domínio, infraestrutura — diferentes cross-cutting concerns vivem em camadas diferentes, e essa decisão importa. Misturar a camada errada produz acoplamento desnecessário ou regra duplicada.
Borda (HTTP, mensageria, gRPC). Lugar natural para autenticação inicial, parsing/validação de input, correlation ID, logging de acesso, mapeamento de erro para códigos HTTP, rate limiting. Esses concerns são específicos do protocolo de entrada — não fazem sentido para chamada direta entre serviços de aplicação.
Aplicação (use cases, command handlers). Lugar de transação, autorização baseada em recurso, instrumentação de operações de domínio. Esses concerns precisam estar próximos o suficiente do domínio para abrir transação corretamente, mas não dentro do domínio para não contaminá-lo com plumbing. É onde behaviors do MediatR e decorators de use case operam.
Infraestrutura (repositórios, clientes externos).
Retry, timeout, circuit breaker, cache. Concerns de
confiabilidade aparecem perto da chamada que pode falhar — não
na borda HTTP nem no domínio. Polly em torno do HttpClient,
tenacity em torno do client de fornecedor, breaker em torno do
driver de banco quando o banco é remoto.
Domínio. Idealmente, o domínio fica
livre de cross-cutting concerns. Eric Evans, em
Domain-Driven Design (2003), defende que o modelo de
domínio precisa ser pobre em dependências para ser rico em
expressão. Logging, métricas, transação não pertencem a
Pedido.AdicionarItem(item). Quando você sente
necessidade de logar de dentro do domínio, geralmente é sinal
de que a operação devia disparar um domain event que
o aplicativo escuta e loga — o domínio só fala da regra, o
aspect cuida do registro.
Por que importa para a sua carreira
Saber identificar e nomear cross-cutting concerns é divisor de águas em revisões de código de gente sênior. Quem não enxerga o eixo transversal aceita códigos com tangling como "é assim que se faz", e cada feature nova herda a dívida. Quem enxerga consegue articular: "esse pedaço do método é domínio, esse aqui é cross-cutting, e o cross-cutting está no lugar errado". Essa frase, dita em revisão, antecipa anos de retrabalho. Em entrevistas de design, a pergunta "como você organizaria autenticação, logging e métricas em um sistema novo?" é uma das formas mais comuns de testar o pensamento arquitetural — e a resposta forte não é "uso o framework X", é "identifico cada concern, classifico se atende as quatro condições de aspect, e coloco na camada certa por cada um".
Como praticar
- Auditoria de tangling em código existente. Pegue um handler ou controller de algum projeto seu (ou de um projeto open source que você usa). Conte quantas linhas o método tem. Marque cada linha com uma cor: azul para regra de domínio, vermelho para cross-cutting (logging, auth, transação, métrica, validação, mapeamento de erro, cache). Calcule a fração de linhas vermelhas. Acima de 50% é sinal de tangling alto. Escreva uma proposta de refatoração apontando, para cada linha vermelha, em qual camada ela deveria viver.
-
Mapeamento de scattering por concern.
Escolha um cross-cutting concern (logging, por exemplo) e
use
grep/ripgrep para encontrar todas as ocorrências em uma base — chamadas alogger.,log.,print(. Categorize cada ocorrência: log de entrada de método, log de erro, log de decisão de negócio, log diagnóstico. Quantos formatos diferentes você encontra? Quantos campos obrigatórios estão ausentes em parte das chamadas? Esse exercício torna visível o custo invisível do scattering. - Classifique seu domínio. Liste todos os cross-cutting concerns que aparecem em um sistema que você conhece. Para cada um, responda às quatro perguntas (universalidade, uniformidade, ortogonalidade ao domínio, estabilidade). Decida quais são bons candidatos a aspect e quais não são. Para os que não são, articule por que — e sugira onde cada um deveria viver. O resultado é um documento curto que você pode usar como base de discussão em qualquer projeto novo.
Referências para aprofundar
- paper Aspect-Oriented Programming — Gregor Kiczales et al. (ECOOP, 1997).
- paper N Degrees of Separation: Multi-Dimensional Separation of Concerns — Peri Tarr, Harold Ossher, William Harrison & Stanley Sutton (ICSE, 1999).
- paper On the Criteria To Be Used in Decomposing Systems into Modules — David Parnas (CACM, 1972).
- paper On the Role of Scientific Thought (EWD447) — Edsger W. Dijkstra (1974).
- livro Domain-Driven Design — Eric Evans (2003).
- livro Clean Architecture — Robert C. Martin (2017).
- livro Code That Fits in Your Head — Mark Seemann (2021).
- livro Software Design X-Rays — Adam Tornhill (2018).
- artigo Aspect-Oriented Software Development with Use Cases — Ivar Jacobson & Pan-Wei Ng (2004, capítulo introdutório disponível online).
- docs ASP.NET Core Middleware.
- docs FastAPI Dependencies.
- vídeo Functional Core, Imperative Shell — Gary Bernhardt (Boundaries, 2012).