skip to content

Components A, B and C form a dependency cycle: A → B → C → A. Describe the two standard techniques for breaking such a cycle and how each changes the graph.

level: middleimportance: must knowfreq 50%

answer

  1. Invert an edge with DIP: client owns the interface
  2. Extract shared parts into a new component
  3. Interface placement is the trick, not interface existence
  4. Avoid the kitchen-sink 'common' component
  5. Re-run the checker; cycles interact

basics

~20 s

Two options. (1) Apply the Dependency Inversion Principle: put an interface that C needs into C (or a place C already sees), and have A implement it — the arrow A → B → C → A becomes C ← A with no return edge. (2) Extract the shared code into a new component D that both A and C depend on.

solid answer

~50 s

**1. Invert one edge with DIP.** Find the offending edge (say C → A). Instead of C calling A's concrete class, declare an interface expressing what C needs, place it in C (or in a component C already depends on), and let A implement it. At runtime A's object is still injected into C, so the *call* direction is unchanged; but the *source dependency* now points from A to the interface, reversing the arrow and opening the loop. **2. Extract a new component.** The things C uses from A are usually a coherent subset. Move them into a new component D. Now A → D and C → D; the loop is gone, and D is often a genuinely reusable unit. Choosing: DIP-inversion suits behavioural coupling and keeps ownership stable; extraction suits shared data types/utilities. Avoid the anti-pattern of dumping everything into a single "common" kitchen-sink component — that just relocates the coupling. Re-run the cycle checker afterwards: a fix in one place can close a loop elsewhere.

code

kotlin · 14 lines
kotlin
// BEFORE: cycle — Orders -> Billing -> Orders
package billing
import orders.OrderService          // <-- closes the loop
class Invoicer(private val orders: OrderService) { /* ... */ }

// AFTER: DIP — the abstraction lives with the CLIENT (billing)
package billing
interface OrderLookup { fun total(id: String): Long }   // billing owns this
class Invoicer(private val lookup: OrderLookup) { /* ... */ }

package orders
import billing.OrderLookup          // arrow now points orders -> billing only
class OrderService : OrderLookup { override fun total(id: String) = 0L }
// runtime call still goes Invoicer -> OrderService; the SOURCE arrow flipped

go deeper

for a junior

Name the two techniques — invert with an interface, or extract shared code into a new component — and show the resulting arrows.

for a middle

Explain that the interface must be owned by the client for the arrow to flip, and that runtime call direction is unchanged; note the composition root supplies the concrete type.

for a senior

Choose deliberately per edge (behavioural coupling → DIP; shared types → extraction; hopeless entanglement → merge), warn about kitchen-sink common modules, and enforce with a CI rule.

for a principal

Discuss which edge to invert as an architectural stance (dependencies should point toward stability and abstraction, per SDP/SAP), the ownership/team implications, and the service-level analogue via events and shared contracts.

## Setup - **Component**: an independently releasable unit (package/module/library/service). - **Cycle**: `A → B → C → A` means A's code references B's, B's references C's, and C's references A's. All three must now be built, versioned and released together. - **DIP (Dependency Inversion Principle)**: depend on abstractions, not concretions; and — crucially for ADP — the abstraction should be *owned by the client*, not by the implementer. Owning the interface is what flips the source-code arrow. ## Technique 1 — invert an edge with DIP Pick the edge you want to reverse; usually the one that "feels backwards" (a low-level component reaching up to a high-level one, e.g. `Persistence → Orders` because persistence needs to fire a callback). Steps: 1. Look at exactly *what* C uses from A. Express it as an interface in terms of C's own vocabulary — e.g. `interface OrderNotifier { fun notify(id: OrderId) }`. 2. **Place that interface inside C** (or in a component C already depends on). This is the whole trick: the abstraction lives on the client side. 3. Make A implement it: `class OrdersService : OrderNotifier`. 4. Wire the concrete A instance into C at composition time — constructor injection, a DI container, a factory, a plugin registry, `main()`. Result: source arrow becomes `A → C` (A must see the interface to implement it) and the old `C → A` edge disappears. The **runtime** call still goes from C into A's object — inversion flips compile-time dependency, not control flow direction of the call itself. Cost: one more indirection, and the concrete type must be supplied somewhere (a composition root). Benefit: C becomes independently testable with a fake implementation, and often more reusable. ## Technique 2 — extract a new component When A and C are entangled because they *share* something (a data type, an enum, a value object, a small utility), the honest reading is: that shared thing belongs to neither. 1. Identify the subset of A that C actually depends on. 2. Move it into a new component `D`. 3. Now `A → D`, `C → D`, and there is no path back. The loop is cut, at the price of one more component in the graph. This mirrors how component structure is *discovered* rather than designed: the graph "jitters" as the system grows, and new components appear specifically to keep it acyclic. **Anti-pattern to avoid:** creating one giant `common` / `shared` / `utils` component and shoving every conflicted class into it. It removes the cycle on paper but produces a component that everything depends on, that changes for every reason, and that violates component cohesion (CCP: classes that change together belong together; CRP: don't force consumers to depend on things they don't use). Prefer several small, purposeful extracted components over one kitchen sink. ## Third, blunt option: merge If two components are so intertwined that neither technique yields a clean seam, they may genuinely be **one** component pretending to be two. Merging them is legitimate — it makes the truth explicit and shrinks the graph. Just don't reach for it first; it forfeits independent releasability. ## Practical checklist - Find the *smallest* set of edges whose removal breaks all cycles (many tools report the strongly connected component and offending edges). - Prefer inverting the edge that points "the wrong way" architecturally (low-level → high-level, infrastructure → domain). - Re-run the acyclicity check after the fix; cycles interact, and one refactor can expose or create another. - Add a build-breaking rule so the loop cannot silently return. - Watch for cycles reappearing through test fixtures, generated code, or build-tool configuration — those edges count too if they are compile/deploy-time. ## Distributed analogue At service granularity the same fixes apply: invert a synchronous call into an event the other side subscribes to (the publisher no longer knows the consumer), or extract a shared schema/contract package. A synchronous request cycle between services is worse than a compile cycle — it also risks deadlock, cascading failure, and impossible deploy ordering.

  • After applying DIP, which direction does the runtime call go?
    Unchanged — the client still calls into the implementation at runtime. Only the compile-time/source dependency is reversed, because the client now owns the interface and the implementer imports it.
  • When is merging two components the right answer instead of inverting or extracting?
    When they change together for the same reasons and no clean seam exists — they were never two components. Merging makes reality explicit; the cost is losing independent releasability, so it should not be the first reflex.
  • Why is a single shared "common" component a poor cycle fix?
    It removes the loop but concentrates coupling: everything depends on it, it changes for every reason (violating CCP), and consumers depend on parts they never use (violating CRP). Several small, purposeful components are better.

saying these in an interview costs you the question

  • Thinking DIP is satisfied merely by introducing an interface — if the interface sits in the implementer's component, the arrow doesn't move and the cycle remains.
  • Believing DIP reverses the runtime call direction; it reverses the source dependency only.
  • Dumping conflicted classes into a giant utils/common module and calling the cycle solved.
  • Assuming one refactor is enough without re-running the cycle analysis.
  • Treating cycle-breaking as purely cosmetic — the payoff is independent build, test and release.

context