MÓDULO 05 · CONCEITO 01 DE 14

Cross-cutting concerns

Por que algumas preocupações se recusam a caber em um único lugar do código — e por que ignorar essa recusa é a fonte mais subestimada de erosão arquitetural. Dijkstra, Parnas, e o nome que Kiczales deu ao problema.

Tempo de leitura ~22 min Pré-requisito Módulos 00–04 (separação de responsabilidades, design) Próximo AOP — a teoria de Kiczales

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.

armadilha em produção

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.

C# — handler tangled, todo cross-cutting inline
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.

Python — handler limpo, concerns por decorator e middleware
@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.

Go — handler explícito, middlewares no router
// 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.

heurística do sênior

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

  1. 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.
  2. 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 a logger., 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.
  3. 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

  1. paper Aspect-Oriented Programming — Gregor Kiczales et al. (ECOOP, 1997). cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf — O paper que cunhou o termo "cross-cutting concerns" e propôs AOP como solução. Leitura curta e fundadora; vale ler o original mesmo trinta anos depois.
  2. paper N Degrees of Separation: Multi-Dimensional Separation of Concerns — Peri Tarr, Harold Ossher, William Harrison & Stanley Sutton (ICSE, 1999). A tese da "tirania da decomposição dominante". Argumento mais teórico do que o de Kiczales, e mais rigoroso sobre por que cross-cutting é estrutural, não acidental.
  3. paper On the Criteria To Be Used in Decomposing Systems into Modules — David Parnas (CACM, 1972). Onze páginas que mudaram a forma de pensar modularidade. Não fala explicitamente de cross-cutting, mas o critério de Parnas é o que se quebra com cross-cutting concerns — ler ajuda a entender por que o problema é fundamental.
  4. paper On the Role of Scientific Thought (EWD447) — Edsger W. Dijkstra (1974). cs.utexas.edu/~EWD/transcriptions/EWD04xx/EWD447 — Texto curto onde Dijkstra cunha "separation of concerns". Mais filosófico que técnico, mas formula a base ética da disciplina.
  5. livro Domain-Driven Design — Eric Evans (2003). Capítulos 4 e 5 (Isolating the Domain, Software in a Layered Architecture). A defesa clássica de manter o domínio livre de plumbing — base do raciocínio sobre onde cross-cutting concerns devem viver.
  6. livro Clean Architecture — Robert C. Martin (2017). Capítulo 22 (The Clean Architecture) e seguintes. A formulação de "Dependency Rule" que justifica isolar cross-cutting concerns em camadas externas ao domínio.
  7. livro Code That Fits in Your Head — Mark Seemann (2021). Capítulos 7 e 11 tratam separação entre domínio e cross-cutting com exemplos em C# — Seemann é referência canônica para AOP "à mão" via composição.
  8. livro Software Design X-Rays — Adam Tornhill (2018). Tornhill mostra como detectar tangling e scattering empiricamente em bases reais usando análise de código + git history. O capítulo sobre "hot spots" é a forma quantitativa de identificar cross-cutting concern mal organizado.
  9. artigo Aspect-Oriented Software Development with Use Cases — Ivar Jacobson & Pan-Wei Ng (2004, capítulo introdutório disponível online). Jacobson — co-criador do UML — argumenta que cross-cutting concerns são casos de uso secundários. Modelo mental útil para classificar concerns sem entrar em formalismo de AOP.
  10. docs ASP.NET Core Middleware. learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware — A documentação oficial sobre middleware é a melhor forma de ver, em código real, como cada cross-cutting concern de borda é resolvido. Vai ser referência em vários conceitos do módulo.
  11. docs FastAPI Dependencies. fastapi.tiangolo.com/tutorial/dependencies — A página de Dependencies é a leitura mais clara que existe sobre como Python moderno resolve cross-cutting concerns sem AOP framework.
  12. vídeo Functional Core, Imperative Shell — Gary Bernhardt (Boundaries, 2012). destroyallsoftware.com/talks/boundaries — Bernhardt formula em vinte minutos a regra que rege boa separação entre domínio puro e cross-cutting plumbing. Influência direta em Hexagonal e Clean Architecture.