skip to content

Why does JPMS forbid cyclic module dependencies, and what do you do when two modules seem to need each other?

level: middleimportance: should knowfreq 41%

answer

  1. requires graph must be acyclic (a DAG)
  2. Cycle => resolution fails
  3. Reason: topological order / reliable configuration
  4. Only MODULE cycles banned; class cycles still allowed
  5. Fix: extract shared module, services/DI, or merge

basics

~20 s

JPMS does not allow module A to require module B while B also requires A (directly or through a chain). Cycles are banned so the module graph can be resolved in a clear order. You break a cycle by extracting the shared code into a third module, or by inverting a dependency with an interface.

solid answer

~50 s

At the module level, the readability graph defined by requires must be acyclic — A requires B and B requires A (or any longer cycle) is rejected when the module graph resolves. The reason is reliable, deterministic configuration: an acyclic graph can be topologically ordered for resolution, initialization, and reasoning about visibility; cycles would make 'which module sees what' ambiguous and prevent a clean ordering. (Note this is only enforced between modules; classes within or across modules may still reference each other.) To remove a cycle: extract the shared types both modules use into a new common module both require; apply dependency inversion by defining an interface in one module and providing the implementation via services (provides/uses with ServiceLoader); or merge the two modules if they are truly cohesive. The cleanest is usually pulling the shared API down into a separate module.

go deeper

for a junior

Knows two modules cannot require each other and that you would refactor to remove the loop.

for a middle

Explains the acyclic-resolution rationale and breaks a cycle by extracting a shared module.

for a senior

Applies dependency inversion via the services mechanism, distinguishes module cycles from class cycles, and treats cycles as a design signal.

for a principal

Sets module-boundary and layering conventions that prevent cycles across a large codebase, and uses cycle detection as architectural governance rather than an ad-hoc fix.

## The readability graph In JPMS, `requires` declarations form a directed graph called the **readability graph**: an edge A→B means *module A reads module B* (A can use B's exported packages). When the JVM (or javac) builds the module graph, it **resolves** this graph starting from the root modules and following `requires` edges. ## What a cyclic dependency is A **cycle** is when you can follow `requires` edges and return to where you started: `A requires B` and `B requires A` (a 2-cycle), or longer, `A requires B`, `B requires C`, `C requires A`. JPMS **forbids cycles in the module graph** — resolution fails with an error reporting the cycle. ## Why cycles are banned - **Deterministic resolution & ordering.** An acyclic graph (a DAG) can be **topologically sorted**, giving a well-defined order to resolve, initialize, and reason about modules. A cycle has no such order — there's no 'first' module. - **Reliable configuration.** JPMS's promise is that the set of modules and their visibility is consistent and analyzable up front. Cycles make 'what can see what' circular and harder to reason about, undermining strong encapsulation guarantees. - **Design pressure.** Cyclic dependencies are usually a design smell even without modules; the module system surfaces them as a hard error rather than letting them rot. **Important scope note:** the prohibition is on **module-level** `requires` cycles. Plain **class-level** circular references (class X uses class Y and vice-versa) are still allowed within a module — JPMS does not analyze class graphs, only module graphs. ## How to break a module cycle 1. **Extract a shared module.** If A and B both need a common set of types, move those types into a new module C that both A and B `requires`. The cycle A↔B becomes A→C ←B. This is the most common fix. 2. **Dependency inversion via services.** Define an **interface** in one module and let the other module **provide** an implementation through the `provides ... with ...` / `uses ...` service mechanism (loaded with `ServiceLoader`). The consumer requires only the interface module, not the implementation, removing the back-edge. 3. **Merge** the two modules if they are genuinely one cohesive unit that was split arbitrarily — but only if cohesion truly justifies it. ## Worked example Suppose `orders` requires `billing` (to charge) and `billing` requires `orders` (to read order data). Extract an `orders.api` module with the order data types and the billing interface; both `orders` and `billing` require `orders.api`; the cycle disappears. ## Key takeaways - The `requires` graph must be a DAG; cycles fail resolution. - Reason: deterministic, reliable, topologically-orderable configuration. - Only **module** cycles are banned; **class** cycles within a module are fine. - Break cycles by extracting a shared module, inverting via services, or (rarely) merging.

  • Are class-level circular references between two classes also forbidden by JPMS?
    No. JPMS only enforces acyclicity of the module-level requires graph. Two classes that reference each other (even across modules, as long as the requires edges themselves are acyclic) are fine; the module system does not analyze the class dependency graph.
  • How does the services mechanism (provides/uses) help break a module cycle?
    It lets the consumer depend only on an interface module and discover implementations at runtime via ServiceLoader, instead of requiring the implementation module directly. Removing that direct requires edge eliminates the back-edge that formed the cycle.

Two managers who each must approve the other's report before submitting their own — nobody can start. Add a neutral coordinator (shared module) both report to, and the deadlock breaks.

saying these in an interview costs you the question

  • Thinking JPMS forbids all circular references, including between classes
  • Believing a cycle just produces a warning rather than failing resolution
  • Solving a cycle by adding --add-reads in both directions as a permanent design (it can mask but not fix the smell)
  • Merging unrelated modules just to silence the cycle error

context