A complexidade que importa em sistemas de software divide-se em duas categorias. A complexidade essencial é a que vem da natureza do problema. É o que o sistema precisa fazer de fato: regras de negócio, restrições do domínio, integrações obrigatórias. Ela é inerente ao problema que se quer resolver e não some com atalhos. A complexidade acidental é a que nasce da forma como organizamos a solução. Vem das escolhas de estrutura, de tecnologia, de como dividimos módulos e de como eles se relacionam. Diferente da essencial, ela pode ser reduzida com princípios de design, boa arquitetura e disciplina.
O Princípio das Dependências Acíclicas (ADP, ou Acyclic Dependencies Principle) atua sobre a complexidade acidental. Ele exige que o grafo de dependências entre pacotes ou componentes não tenha ciclos. Não torna o domínio mais simples, mas evita que a estrutura do sistema multiplique custo de manutenção, build e evolução. Neste artigo vamos trabalhar um pouco sobre como identificar e como resolver esse problema em várias dimensões.
Cenário típico e a dor do ciclo
Em monólitos em crescimento, o cenário típico é este: o módulo A depende de B, B de C e C volta a depender de A.
flowchart LR A[Módulo A] --> B[Módulo B] B --> C[Módulo C] C --> A
Quando há ciclos de dependência, a complexidade de mudança aumenta de forma drástica. Não há como compilar, testar ou implantar só uma parte do sistema: qualquer alteração em um módulo do ciclo obriga a recompilar e retestar todos. Módulos acabam assumindo mais responsabilidades do que deveriam, porque as regras e dados estão espalhados por várias partes interligadas, quebrando princípios de design como a separação clara de responsabilidades e a coesão de classes. A criação de testes automatizados se torna mais difícil, já que é quase impossível isolar um módulo sem arrastar dependências externas. Além disso, o risco de quebrar funcionalidades existentes cresce, porque pequenas alterações podem desencadear efeitos em cadeia em outros módulos do ciclo. O acoplamento circular compromete também a clareza do código e a previsibilidade da evolução do sistema, dificultando a manutenção e aumentando o custo de cada mudança. Esse custo costuma permanecer oculto até o dia em que alguém tenta alterar um módulo e descobre que não consegue fazer nada de forma isolada. O Princípio das Dependências Acíclicas (ADP) atua exatamente nesse ponto: ao exigir que o grafo de dependências seja acíclico, ele restaura ordenação e fronteiras nítidas entre módulos, facilita testes isolados, promove builds incrementais e ajuda a manter cada componente responsável apenas pelo que lhe cabe.
Ninguém planeja esse ciclo. Ele vem da combinação de vários fatores: pressão por entrega, ausência de uma regra explícita que proíba dependências circulares e situações locais que se repetem. Somam-se a isso a chegada de alguém novo no time sem contexto do desenho do sistema, refinamentos técnicos de incremento mal feitos (quando se "resolve rápido" sem olhar o grafo de dependências), falta de design da aplicação definido e comunicado, ausência de documentação de arquitetura ou de ADRs que orientem onde cada tipo de código deve morar, rotatividade sem handover que preserve esse conhecimento e falta de uma fitness function (ou critério automatizado em CI) para validar se esse tipo de problema não começou a ocorrer. Isolado, cada fator pode não criar o ciclo. Juntos, tornam o acoplamento circular quase inevitável com o tempo. Robert C. Martin formulou o princípio no contexto do design de pacotes e da evolução de sistemas orientados a objetos. No livro Agile Software Development: Principles, Patterns, and Practices e em artigos como Granularity: The Acyclic Dependencies Principle, situa o ADP ao lado do Stable Dependencies Principle (SDP) e do Stable Abstractions Principle (SAP). Voltaremos a esses dois princípios no final do artigo, para ver como eles se complementam ao ADP. Onde há ciclo, não há ordem; onde o ADP é respeitado, a estrutura volta a ser governável.
Definição formal e teoria de grafos
Em termos formais, o ADP diz o seguinte. O grafo de dependências de pacotes ou componentes não deve conter ciclos. Em outras palavras, as dependências devem formar um grafo direcionado acíclico (DAG), isto é, um grafo sem ciclos em que existe uma ordem coerente entre os nós. Ao longo do artigo usamos pacote como termo principal; módulo e componente aparecem como sinônimos quando o contexto (monólito, serviço, bounded context) exigir. Cada nó representa um pacote ou componente. Cada aresta direcionada de A para B indica que A depende de B, ou seja, A usa tipos, funções ou recursos definidos em B. Um ciclo existe quando há um caminho que parte de um nó e retorna a ele. Nesse caso não há como atribuir uma ordem total aos módulos. Não existe "primeiro" nem "último", e qualquer alteração em um vértice do ciclo pode forçar recompilação ou reteste de todos os outros.
"O grafo de dependências de pacotes [ou componentes] não deve conter ciclos." — Robert C. Martin, Granularity: The Acyclic Dependencies Principle
A frase preserva a formulação original do autor. O que caracteriza um ciclo de dependência é a existência de uma sequência que fecha em si mesma. Se o pacote pedidos depende de clientes, clientes de enderecos e enderecos de pedidos, os três formam um ciclo. Nenhum deles pode ser construído ou entendido de forma isolada. A ordem de compilação e a ordem de responsabilidade ficam ambíguas.
flowchart LR
subgraph ciclo["Ciclo de dependências"]
P[pedidos] --> C[clientes]
C --> E[enderecos]
E --> P
endEm contraste, uma estrutura acíclica permite que se construa primeiro o que não depende de nada (folhas do grafo), depois o que depende apenas das folhas, e assim por diante, até a raiz. Essa ordem é a ordenação topológica (a sequência em que os nós podem ser processados respeitando as dependências) e só existe quando o grafo é acíclico.
flowchart TB E[enderecos] --> C[clientes] C --> P[pedidos] style E fill:#e8f5e9 style C fill:#e8f5e9 style P fill:#e8f5e9
(Acima: grafo cíclico. Abaixo: mesmo domínio em DAG, com as folhas (enderecos) no topo e ordenação topológica possível.)
Em pseudo-código, um ciclo típico aparece quando dois módulos se referem mutuamente. O módulo de orquestração importa o de regras, e o de regras, para validar ou notificar, importa o orquestrador. A solução conceitual é introduzir uma abstração (interface ou tipo estável) que quebre a referência direta, tornando a dependência unidirecional. O grafo deixa de ter ciclo e a ordenação topológica passa a ser possível.
Fundamentos matemáticos: DAG, ordenação topológica e componentes fortemente conectados
A fundamentação do ADP repousa na teoria de grafos. Um grafo direcionado acíclico (DAG) é um grafo direcionado sem ciclos. Em um DAG é sempre possível calcular uma ordenação topológica, isto é, uma ordem linear dos vértices em que, para toda aresta (u, v), u aparece antes de v. Essa ordem não é necessariamente única, mas sua existência garante uma sequência de etapas coerente com todas as dependências. Sistemas de build (Make, Maven, Gradle, Bazel, entre outros) exploram essa propriedade. Eles modelam o projeto como um grafo de tarefas e executam as tarefas em ordem topológica. Se o grafo tiver ciclo, a ordenação não existe e o build falha ou fica indefinido.
Os componentes fortemente conectados (SCCs) são subgrafos maximais em que, para todo par de vértices u e v, existe um caminho de u para v e de v para u. Em resumo: um SCC é um conjunto de nós que formam um ou mais ciclos entre si; ferramentas de análise usam SCCs para detectar ciclos. Em um DAG, cada vértice é um SCC trivial, pois não há ciclo. Quando há ciclos, os vértices que participam do mesmo ciclo pertencem ao mesmo SCC. O diagrama abaixo ilustra um grafo com ciclo (A→B→C→A). Os três nós formam um único SCC não trivial. D é um SCC trivial.
flowchart LR
subgraph scc["SCC (ciclo)"]
A --> B
B --> C
C --> A
end
D[D]
A --> DFerramentas de análise estática e de dependências costumam detectar ciclos identificando SCCs de tamanho maior que 1. Quebrar um ciclo equivale a remover arestas (ou nós) até que nenhum SCC não trivial permaneça, ou seja, até o grafo se tornar um DAG. Ferramentas de análise fazem o trabalho de detecção. A decisão de qual aresta cortar continua humana.
O impacto em pipelines de compilação é direto. Em um monólito com dependências cíclicas entre pacotes não há como compilar apenas um subconjunto após uma mudança. Qualquer alteração em um pacote do ciclo força a recompilação de todos. Em projetos grandes isso aumenta o tempo de build, fragiliza integração contínua e dificulta deploy incremental. O ADP, ao exigir aciclicidade, restaura builds incrementais (compilar e testar só o que mudou) e testes localizados por camada ou componente.
Impacto em arquiteturas em camadas, Clean Architecture e estilos relacionados
Arquiteturas em camadas, Clean Architecture, Onion Architecture e Hexagonal (Ports and Adapters) compartilham a ideia de que as dependências devem apontar em uma direção preferencial. Em geral, do que é mais volátil ou mais próximo da infraestrutura para o que é mais estável ou mais próximo do domínio. O ADP reforça essa ideia ao proibir ciclos. Se a camada de apresentação depende da aplicação, esta do domínio e o domínio não depende de nenhuma delas, o grafo é acíclico. Se em algum momento o domínio passar a depender de infraestrutura ou da aplicação (por exemplo para resolver um serviço ou enviar um evento), surge um ciclo ou violação da regra. A inversão de dependência (DIP) é a técnica usual para restaurar a seta correta. O domínio define uma interface e a implementação concreta fica em um pacote externo que depende do domínio.
Na Clean Architecture as camadas são organizadas em anéis. Entidades no centro, casos de uso em volta, interfaces de adaptadores e na borda as implementações (frameworks, UI, banco de dados). A regra de dependência diz que o código só pode apontar para dentro, nunca para fora. Isso já implica um grafo acíclico. Não há como um anel interno depender de um externo sem violar a regra.
flowchart TB
subgraph borda["Borda"]
FW[Frameworks / UI / DB]
end
subgraph adaptadores["Interfaces de adaptadores"]
AD[Adapters]
end
subgraph aplicacao["Aplicação"]
UC[Casos de uso]
end
subgraph nucleo["Núcleo"]
ENT[Entidades]
end
FW --> AD
AD --> UC
UC --> ENT(Setas indicam direção da dependência: a borda depende do núcleo, nunca o contrário.) O ADP, nesse contexto, é a expressão da mesma restrição na escala de pacotes ou módulos. Mesmo dentro de um anel, os pacotes não devem formar ciclos. As fronteiras entre contextos ou subsistemas ganham assim uma noção de estabilidade. Componentes estáveis (domínio, núcleo) não dependem de componentes instáveis (frameworks, detalhes), e o grafo permanece ordenável e previsível.
ADP e Domain-Driven Design: bounded contexts e acoplamento semântico
Em Domain-Driven Design o sistema é decomposto em bounded contexts, cada um com seu modelo e sua linguagem ubíqua. A relação entre contextos é explicitada por um context map, que mostra quem depende de quem e sob que padrão (conformista, anticorrupção, etc.). Ciclos entre bounded contexts geram acoplamento semântico e organizacional. O acoplamento semântico surge porque os modelos passam a se influenciar mutuamente. O organizacional, porque os times passam a precisar coordenar mudanças a cada alteração. Um ciclo entre contextos expõe fronteiras mal definidas ou responsabilidades sobrepostas. Dois contextos que se referem mutuamente não podem evoluir nem ser implantados de forma independente, e a fronteira entre eles deixa de ser clara. Eric Evans, em Domain-Driven Design: Tackling Complexity in the Heart of Software, enfatiza a importância de delimitar contextos e definir relações direcionais entre eles. O ADP atua nesse nível. O grafo de dependências entre contextos deve ser acíclico.
O Anti-Corruption Layer (ACL) não elimina ciclos de dependência que já existem entre contextos. Se B já depende de A, colocar um ACL dentro de A não remove a dependência de B para A. O ciclo permanece. O ACL serve quando há (ou se desenha) uma dependência unidirecional de A para B: ele evita que A dependa do modelo interno de B, mantendo fronteiras limpas e contrato estável. Nesse cenário, A define uma camada de tradução que consome B por uma interface estável e adapta os dados ao modelo de A. B não precisa conhecer A e o context map segue um DAG porque a direção já era única. Para cumprir o ADP quando já existe ciclo entre contextos, é preciso refatorar: extrair um terceiro contexto que concentre o que é compartilhado ou introduzir comunicação assíncrona e eventos, de modo que o grafo de dependências deixe de ser cíclico.
flowchart LR
subgraph ctxA["Contexto A"]
A[Modelo A]
ACL[ACL]
end
subgraph ctxB["Contexto B"]
B[Modelo B]
end
A --> ACL
ACL --> B(Context map acíclico: A depende de B apenas via Anti-Corruption Layer. B não depende de A. O diagrama só vale quando a direção já é única. Um ciclo existente não desaparece ao adicionar um ACL.)
Lembrete: O ACL não resolve ciclos existentes. Só evita que novos ciclos surjam quando a dependência já é unidirecional.
Quando surge um ciclo entre bounded contexts, há quase sempre uma violação do princípio de autonomia de modelo. Refatorar para extrair um terceiro contexto ou introduzir eventos assíncronos restaura a aciclicidade.
A mesma ideia de grafo acíclico reaparece em microservices, com serviços no lugar de contextos.
ADP em microservices: dependências entre serviços e comunicação
Em arquiteturas de microservices o "pacote" vira serviço e as dependências tornam-se chamadas de API, mensagens ou eventos. Ciclos entre serviços aparecem quando o serviço A chama B, B chama C e C chama A, ou quando há dependências síncronas em cadeia que fecham em loop. As consequências incluem falhas em cascata, impossibilidade de deploy independente e acoplamento de versão. Não dá para evoluir um serviço sem coordenar com todos os outros do ciclo.
flowchart LR
subgraph ciclo_svc["Ciclo entre serviços"]
SvcA[Serviço A] --> SvcB[Serviço B]
SvcB --> SvcC[Serviço C]
SvcC --> SvcA
endflowchart TB P[Produtor] --> EB[Event Bus / Broker] EB --> C1[Consumidor 1] EB --> C2[Consumidor 2] style P fill:#e8f5e9
(À esquerda: dependências síncronas em ciclo. À direita: produtor e consumidores desacoplados no tempo. Em tempo de build o grafo de dependência entre serviços pode ser acíclico. Em runtime é preciso evitar cadeias de eventos que fechem em loop.)
Comunicação síncrona (HTTP, gRPC) tende a expor dependências diretas e a facilitar ciclos quando os times não impõem uma direção clara. Arquiteturas baseadas em eventos (publicar/assinar, event sourcing) desacoplam produtores e consumidores no tempo. O produtor emite um evento e não conhece quem consome. O grafo de dependência em tempo de compilação ou build é o que o ADP exige acíclico. Em runtime ainda podem existir ciclos de eventos se uma cadeia de publicações e consumos fechar em loop. Se nenhum consumidor depender do produtor de forma síncrona e bidirecional, os ciclos entre serviços tendem a diminuir. Service mesh e políticas de rede podem reforçar regras de quem pode chamar quem, alinhando a topologia ao grafo desejado (DAG). Orquestração centralizada (um orquestrador que chama vários serviços em sequência) evita ciclos, pois o orquestrador é a única raiz que depende dos demais. Coreografia (serviços que reagem a eventos e disparam outros eventos) pode manter aciclicidade desde que não haja cadeia de eventos que feche em loop.
ADP e DevOps: pipelines, build e deploy
Pipelines de CI/CD assumem que é possível construir e testar partes do sistema de forma incremental. Quando o repositório ou o grafo de módulos contém ciclos, essa suposição falha. Qualquer mudança em um nó do ciclo obriga a reconstruir e retestar todo o ciclo. O tempo de build aumenta, o feedback loop alonga e o deploy independente por serviço ou módulo fica inviável. Sistemas de build modernos (Bazel, Buck, Nx, Turborepo) modelam o workspace como um DAG de tarefas. Quando o código viola o ADP, o grafo de tarefas exige workarounds (por exemplo juntar módulos cíclicos em um único target), o que mascara o problema em vez de resolvê-lo.
Em monorepos o ADP aplica-se aos pacotes ou workspaces. A dependência entre projetos deve ser acíclica para que ferramentas de build ordenem e cacheiem corretamente. Em multirepos cada repositório costuma ser um nó. As dependências entre repos (via versões publicadas) também devem evitar ciclos, senão a publicação e o consumo de bibliotecas viram um jogo de "quem publica primeiro". Versionamento semântico e dependências direcionais (lib-core não depende de lib-app) ajudam a manter o ecossistema como um DAG. Deploy incremental e rollback segmentado dependem de unidades de deploy com fronteiras estáveis e sem ciclos. Do contrário, "deploy só do serviço X" pode quebrar o sistema porque X participa de um ciclo de dependências em runtime ou em build.
DAG de deploy em microservices e antipatterns
Em CI/CD, um DAG é usado para ordenar o deploy com base em dependências: cada nó é um serviço ou job, cada aresta indica "deve ser implantado antes de". Quando essas dependências refletem acoplamento de serviço em vez de dependências técnicas legítimas, surgem antipatterns. A tabela abaixo resume os mais comuns:
| Antipattern | Descrição | Relação com o DAG de deploy |
|---|---|---|
| Deployment coupling / Shared deployment | Serviços dependem uns dos outros para deploy, quebrando autonomia | O DAG obriga B a ir para produção só depois de A, mesmo com API de A estável |
| Distributed monolith | Microserviços fortemente acoplados, não independentes | DAG de deploy complexo indica que não dá para atualizar serviços individualmente |
| God service / Big Ball of Mud | Um serviço central dita o ritmo dos outros | Muitas arestas convergindo para o mesmo serviço: ponto único de falha e coordenação |
| Tight version coupling | Serviços precisam ser deployados juntos por causa de versão de API ou schema | DAG força versões sincronizadas; mudar um serviço exige redeploy de todos os dependentes |
| Sequential pipeline bottleneck | Cadeia de deploy longa e frágil | DAG muito profundo aumenta tempo de deploy e risco de rollback em cascata |
O ADP, nesse contexto, lembra que um DAG de deploy saudável deve refletir dependências reais e mínimas, não acoplamento que poderia ser eliminado com melhor desenho de contextos ou comunicação assíncrona.
Dimensão organizacional: Lei de Conway e ciclos sociotécnicos
Melvin Conway observou em 1967 que a estrutura dos sistemas tende a espelhar a estrutura de comunicação das organizações que os produzem. A Lei de Conway costuma ser citada assim:
"Organizações que projetam sistemas estão limitadas a produzir designs que são cópias das estruturas de comunicação dessas organizações." — Melvin Conway
Se os times estão organizados em silos que precisam se aprovar mutuamente para qualquer mudança, o sistema tende a exibir acoplamento circular entre os módulos que cada time possui. Ciclos técnicos entre pacotes ou serviços frequentemente refletem ciclos organizacionais, como quando duas equipes dependem uma da outra para entregar valor sem ordem clara de quem "alimenta" quem. Quebrar ciclos técnicos às vezes exige realinhar ownership, fronteiras de time e contratos entre equipes. Arquitetura não é só código. É também como os times se comunicam. O ADP, nesse sentido, não é só uma regra de código. É uma restrição que, quando levada a sério, pressiona a organização a definir dependências direcionais claras e a evitar que ninguém seja bloqueante de todos. Governança arquitetural que impõe revisão de dependências e rejeita merges que introduzem ciclos ajuda a manter o grafo de código e o mapa de times coerentes com um DAG.
Estratégias para quebrar ciclos
Quebrar um ciclo de dependências exige introduzir pelo menos uma nova direção ou um novo nó que absorva a dependência mútua. As técnicas mais usadas são inversão de dependência e extração de abstração. Na inversão, um dos módulos do ciclo deixa de depender do outro diretamente e passa a depender de uma interface. A implementação da interface fica no outro módulo (ou em um terceiro) e o fluxo de dependência passa a ser unidirecional.
flowchart LR
subgraph antes["Ciclo"]
M1[Módulo A] --> M2[Módulo B]
M2 --> M1
endflowchart LR M1[Módulo A] --> I[Interface] M2[Módulo B] --> I
(Antes: A e B dependem um do outro. Depois: ambos dependem da interface, que pode ficar em um pacote neutro ou em um dos módulos, e o ciclo desaparece.) Na extração, o código comum que motivava a dependência circular é movido para um novo pacote estável, do qual ambos os lados passam a depender. O ciclo some e o novo pacote vira folha ou nó intermediário do DAG.
Outras abordagens incluem interface segregation. Em vez de um módulo depender de outro inteiro, ele depende apenas de uma interface enxuta, que pode ficar em um pacote neutro. Introdução de um novo componente (event bus, facade, orquestrador) que passa a ser o único ponto que conhece os dois lados do ciclo também quebra a circularidade. Os módulos originais passam a depender apenas do novo componente, que não depende deles em termos de build. Arquitetura orientada a eventos pode substituir chamadas síncronas por publicação de eventos, de modo que o produtor não dependa do consumidor no grafo de compilação.
Cada técnica tem trade-offs. Inversão e extração de abstrações aumentam o número de interfaces e às vezes de pacotes. Em excesso podem levar a overengineering. O critério é quebrar o ciclo com o menor custo de compreensão e manutenção. Em sistemas legados, quebrar ciclos pode ser feito de forma incremental. Primeiro estabilizar um módulo do ciclo extraindo interfaces, depois migrar os consumidores, e assim por diante até o grafo ficar acíclico.
Detecção e visualização de ciclos
As estratégias da seção anterior exigem saber onde estão os ciclos. Esta seção trata de como encontrá-los. Ciclos podem ser detectados por analisadores estáticos que constroem o grafo de dependências a partir de imports, referências de tipo ou metadados de build. Ferramentas como JDepend (Java), madge (JavaScript/Node), go-callvis (Go) ou módulos de análise em IDEs calculam SCCs ou listam pacotes envolvidos em ciclos. Ferramentas de build (Maven, Gradle, Bazel) às vezes falham na resolução ou na ordenação quando há ciclo, ou oferecem plugins que geram relatórios de dependências. Visualizadores de grafo (Graphviz, dependência-graph em várias stacks) permitem inspecionar o DAG ou identificar clusters que formam ciclos. Interpretar componentes fortemente conectados com mais de um vértice equivale a listar os ciclos. A partir daí a equipe pode escolher qual aresta quebrar (qual dependência remover ou inverter) com base em coesão e estabilidade. Quando essa verificação é automatizada no pipeline (por exemplo, falhando o build ou bloqueando o merge se um ciclo for introduzido), ela funciona como fitness function: um critério objetivo que impede a arquitetura de regredir nessa dimensão.
Anti-padrões relacionados: Big Ball of Mud e grafo cíclico de pacotes
Sistemas que evoluíram sem regras de dependência tendem a exibir grafo de pacotes cíclico. Muitos ciclos entre muitos pacotes, sem camada nem ordem clara. Esse padrão está ligado ao Big Ball of Mud, em que a estrutura se degrada até que qualquer mudança toque em todo o sistema. Arquitetura em espaguete é a manifestação no código, com fluxos que cruzam camadas em todas as direções. Shared kernel abusivo (um pacote compartilhado que todos importam e que também importa vários deles) é outra fonte comum de ciclos. O kernel deixa de ser estável e vira um nó central de um componente fortemente conectado. Sinais de alerta incluem builds que recompilam grande parte do projeto após uma mudança pequena, testes que precisam rodar em ordem específica ou que falham quando rodados isoladamente, e dificuldade para descrever "quem depende de quem" sem desenhar um emaranhado. Métricas como número de SCCs não triviais, tamanho do maior ciclo ou profundidade do grafo ajudam a acompanhar a degradação e priorizar refatorações.
ADP, Stable Dependencies Principle e Stable Abstractions Principle
Como adiantado no início do artigo, o ADP não aparece sozinho na proposta de Martin. Ele compõe, com o Stable Dependencies Principle (SDP) e o Stable Abstractions Principle (SAP), o conjunto de princípios de design de pacotes. O SDP diz que um pacote deve depender apenas de pacotes mais estáveis que ele (que mudam menos). O SAP diz que pacotes estáveis devem ser abstratos e pacotes instáveis podem ser concretos. Juntos orientam um grafo em que as dependências apontam na direção da estabilidade e da abstração, sem ciclos. O ADP garante a forma do grafo (DAG). O SDP e o SAP orientam o conteúdo e a direção das arestas. Não há conflito entre os três: um pacote instável que depende de um estável e abstrato está em conformidade com todos. Na prática, o trio ajuda a manter o grafo não só acíclico, mas estável e evolúvel. Os princípios SOLID atuam no nível de classes e objetos. O ADP, o SDP e o SAP atuam no nível de pacotes e módulos, complementando o design em grande escala.
Estudo de caso: sistema legado com ciclos entre módulos
Um cenário típico é um monolito de médio porte, dividido em pacotes por domínio (pedidos, clientes, estoque, pagamentos). Com o tempo o pacote de pedidos passou a precisar de regras que estavam em clientes (descontos por perfil). Clientes passou a precisar de histórico que estava em pedidos. Estoque passou a disparar eventos que pedidos e pagamentos consomem, mas também a consultar pedidos para reserva. O grafo de dependências passou a ter ciclos entre pedidos, clientes e estoque.
flowchart LR P[pedidos] --> C[clientes] C --> P E[estoque] --> P P --> E
(Ciclos pedidos↔clientes e pedidos↔estoque.)
Após a refatoração, interfaces e orquestração extraídas quebram os ciclos:
flowchart TB APP[aplicação / casos de uso] P[pedidos] --> APP C[clientes] --> APP E[estoque] --> APP style APP fill:#e3f2fd
Os sintomas eram conhecidos: build completo levava vários minutos após qualquer alteração em um desses pacotes, testes de integração precisavam subir o contexto inteiro e deploy era sempre "tudo ou nada". A refatoração consistiu em (1) mapear o grafo com uma ferramenta de análise estática e listar os ciclos, (2) extrair interfaces de "histórico de pedidos" e "regras de desconto" para um pacote de aplicação que tanto pedidos quanto clientes passaram a usar, eliminando a dependência direta entre eles, e (3) mover a orquestração de reserva de estoque para um caso de uso que depende de pedidos e de estoque, sem que estoque dependa de pedidos. Após quebrar os ciclos o build passou a ser incremental para a maior parte das mudanças e os times conseguiram trabalhar em pedidos e clientes com menos conflitos. O custo foi o surgimento de mais algumas interfaces e de um pacote de aplicação mais explícito. O ganho foi a restauração de fronteiras claras e de tempo de build previsível.
Dimensão econômica: custo de mudança, dívida técnica e métricas
Dependências cíclicas aumentam o custo de mudança. Qualquer alteração em um nó do ciclo pode exigir reteste e reimplantação de todo o ciclo. Esse custo vira dívida técnica quando a equipe adia refatorações por medo de quebrar algo distante. Métricas de fluxo como lead time (tempo entre o início e a entrega de uma mudança) e MTTR (tempo médio para reparo) pioram quando o sistema é um emaranhado. Mudanças demoram mais e correções têm efeitos colaterais imprevisíveis.
| Cenário | Lead time | MTTR | Deploy / mudança |
|---|---|---|---|
| Grafo cíclico | Alto (mudança propaga em várias direções) | Alto (correções com efeitos colaterais) | "Tudo ou nada", rollback difícil |
| Grafo acíclico (ADP) | Menor (fronteiras estáveis) | Menor (impacto localizável) | Incremental, rollback por componente |
O ADP contribui para a sustentabilidade de longo prazo ao manter o grafo ordenável e as fronteiras estáveis, reduzindo o custo marginal de cada nova feature e facilitando onboarding e manutenção. Do ponto de vista econômico, investir em aciclicidade é investir em opções futuras. O sistema permanece evoluível sem que cada passo exija retrabalho em larga escala.
Relevância em sistemas distribuídos, eventos e ecossistemas modulares
Em sistemas distribuídos o ADP aparece na topologia de serviços e de fluxos de dados. Arquiteturas baseadas em eventos (event sourcing, CQRS, pub/sub) tendem a produzir grafos em que produtores e consumidores são desacoplados no tempo. A dependência de "quem consome o quê" pode ser modelada como um DAG se nenhum consumidor for também produtor de um evento que o produtor original consome em ciclo fechado. Plataformas modulares e ecossistemas de plugins dependem de que módulos e extensões declarem dependências unidirecionais. Ciclos entre plugins ou entre plataforma e plugin tornam a carga e a atualização imprevisíveis. Em cenários de IA e sistemas autônomos, pipelines de dados e de modelos são frequentemente descritos como DAGs (por exemplo DAGs de tarefas em orquestradores). A mesma ideia de ordenação e ausência de ciclos aplica-se para garantir execução determinística e reproduzível. O princípio tende a permanecer relevante sempre que a evolução e a operação do sistema dependam de uma ordem clara de dependências e de fronteiras bem definidas.
Resumo visual: do ciclo ao DAG
O diagrama abaixo sintetiza a ideia central do artigo. Um grafo cíclico (monólito, microservices ou bounded contexts) gera custo de mudança alto e fronteiras instáveis. As técnicas de quebra (inversão de dependência, extração de abstração, terceiro contexto, eventos) eliminam ciclos e levam a um DAG, com ordenação clara e builds ou deploys incrementais.
flowchart LR
subgraph problema["Problema"]
C[ciclo entre pacotes / serviços / contextos]
end
subgraph tecnicas["Técnicas de quebra"]
T1[Inversão de dependência]
T2[Extração de abstração]
T3[Terceiro contexto / eventos]
end
subgraph resultado["Resultado"]
D[DAG: ordenação topológica, build/deploy incremental]
end
C --> T1
C --> T2
C --> T3
T1 --> D
T2 --> D
T3 --> DConclusão
O Princípio das Dependências Acíclicas não é uma regra arbitrária de estilo. É a exigência de que o grafo de dependências do sistema seja um DAG, permitindo ordenação topológica, builds incrementais e fronteiras estáveis entre componentes. Aplica-se a pacotes em um monolito, a bounded contexts em DDD, a serviços em arquitetura de microservices e a tarefas em pipelines de build e de dados. Respeitá-lo reduz complexidade acidental, alinha estrutura técnica e organizacional (Lei de Conway) e diminui custo de mudança e dívida técnica. Quebrar ciclos exige técnicas como inversão de dependência e extração de abstrações, com trade-offs entre número de interfaces e clareza. Detectar ciclos é possível com ferramentas de análise estática e de build. Em conjunto com o Stable Dependencies Principle e o Stable Abstractions Principle, o ADP compõe uma base para design de pacotes e módulos que permanece relevante em arquiteturas em camadas, em Clean Architecture, em DDD e em sistemas distribuídos orientados a eventos.
Um passo prático: na próxima revisão de arquitetura, rode a análise de dependências do seu projeto (JDepend, madge, go-callvis ou o equivalente na sua stack) e verifique se existe algum SCC não trivial. Se existir, pergunte: qual aresta faz mais sentido quebrar primeiro, com base em coesão e estabilidade?
Referências
- Martin, Robert C. Granularity: The Acyclic Dependencies Principle. 2002. (Artigo sobre design de pacotes e ADP.)
- Martin, Robert C. Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2002.
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003.
- Conway, Melvin. How Do Committees Invent? Datamation, 1968. (Origem da Lei de Conway.)