skip to content

How do requires, requires transitive, and the readability graph shape module API design and migration at scale?

level: principalimportance: nice to knowfreq 30%

answer

  1. Readability = 'A reads B' edge, created by requires
  2. transitive re-exports readability to your consumers
  3. Use transitive when B's types appear in your exported API
  4. Aggregator module = only requires transitive, no code
  5. Export surface = public API; transitive edges are hard to remove later

basics

~20 s

requires says your module depends on another. requires transitive also re-exports that dependency to anyone who depends on you, so they read it automatically. The set of who-can-read-whom is the readability graph. Designing these directives well keeps APIs clean and migration manageable.

solid answer

~50 s

Module access is governed by 'readability': module A can use module B only if A reads B, established by `requires B`. `requires transitive B` additionally makes A's own consumers read B implicitly — essential when B's types appear in A's public API (return types, parameters), so consumers do not need a redundant explicit requires. The full set of reads-edges is the readability graph, and JPMS resolves it before runtime. At scale this is an API-design tool: use plain `requires` for internal dependencies you do not want to leak, and `requires transitive` only for dependencies that are genuinely part of your exported API. Misusing transitive over-couples consumers; omitting it where API types leak forces every consumer to add a requires. Aggregator modules (only `requires transitive`) bundle a façade. Qualified exports (`exports ... to`) plus a deliberate readability graph let large codebases enforce architectural layers the compiler checks.

go deeper

for a junior

Knows requires declares a dependency and that one module must read another to use it.

for a middle

Can explain readability, the difference between requires and requires transitive, and when transitive is needed because a type leaks into the API.

for a senior

Reasons about API design with the graph: plain vs transitive vs static requires, qualified exports for SPI, and migration constraints around the unnamed module.

for a principal

Treats the readability graph as enforced architecture across teams — sets policy on transitive usage, aggregator façades, qualified exports for layering, stable module names, and a sequenced top-down/bottom-up modularization plan, weighing long-term coupling and breaking-change risk.

## Readability: the foundation In JPMS, one module can use another's exported API only if it **reads** that module. **Readability** is the directed relationship 'A can see B's exported packages.' You create it with **`requires B;`** in A's descriptor. The complete set of these edges across all resolved modules is the **readability graph** (a directed graph: nodes = modules, edges = 'reads'). Resolution builds and validates this graph before the program runs — this is precisely the 'reliable configuration' guarantee applied to the dependency structure. Three pieces always combine for cross-module use: the producer **exports** a package, the producer is **read** by the consumer (`requires`), and the type is **public**. Readability is the 'requires' half. ## Plain `requires` vs `requires transitive` - **`requires B;`** — A reads B. This is **not** propagated: if C reads A, C does **not** automatically read B. A's dependency on B is A's private business. - **`requires transitive B;`** — A reads B **and** implied readability is granted to anyone who reads A. So if C `requires A`, C automatically reads B too, with no explicit `requires B` of its own. ### When transitive is required, not optional If A's **exported API exposes types from B** — e.g. a public method returns a `B.SomeType` or takes one as a parameter — then any consumer C that calls that method must be able to name `B.SomeType`. Without `requires transitive B`, C would have to add its own `requires B` just to use A's API, which is leaky and brittle. The rule of thumb: **if a dependency's types appear in your public/exported signatures, declare it `requires transitive`; otherwise use plain `requires`.** This makes the transitive keyword an explicit statement of 'this dependency is part of my API contract.' ## `requires static` (compile-time-only) — for completeness `requires static B;` declares an **optional** dependency needed at compile time but not necessarily at run time (e.g. annotations or an optional integration). It does not force B to be present at runtime. It is a third flavour worth knowing when shaping the graph, used for optional/compile-only edges. ## Aggregator (façade) modules Because `requires transitive` re-exports readability, you can build an **aggregator module** whose descriptor is *only* a list of `requires transitive` directives and contains no code. Consumers `requires` the single aggregator and transitively read the whole bundle. The JDK uses this pattern (e.g. an umbrella module pulling in several others). It is a deliberate API-surface-as-a-module technique. ## Qualified exports/opens for layering `exports pkg to specific.module;` (a **qualified export**) restricts who may use a package — ideal for SPI/plugin or internal-shared packages that should be visible to a sibling module but not the world. Combined with the readability graph, this lets architects encode **layering rules the compiler enforces**: a UI module that should never touch a persistence-internal package simply has no read edge / export to it, and the build fails if someone tries. ## Designing the graph at scale For a large, multi-team codebase the readability graph becomes an architectural artifact: - **Minimize transitive edges** to avoid over-coupling — each `requires transitive` is a promise that leaks a dependency to all your consumers and is hard to remove later (removing it can break downstream compiles). - **Use plain `requires` for implementation dependencies** so they stay swappable internal details. - **Use qualified exports** to expose SPI to known collaborators without going public. - **Treat the export surface as the public API** — changing or removing an export is a breaking change subject to the same versioning discipline as method signatures. - **Mandate `Automatic-Module-Name`** on libraries so consumers can pin a stable `requires` target even before full modularization. ## Migration implications Because named modules cannot read the **unnamed module** (classpath), modularizing one layer often forces its dependencies onto the modulepath too (as explicit or automatic modules). Planning the readability graph top-down lets you sequence this: introduce explicit modules at the leaves of your dependency tree first, let upstream dependencies ride as automatic modules, and tighten `requires`/exports as each layer modularizes. The graph also surfaces hidden cyclic or split-package problems early, since resolution rejects them. ## The payoff The readability graph turns dependency and visibility decisions into **compiler-checked architecture**. `requires` vs `requires transitive` is the lever that decides whether a dependency is private plumbing or part of your published contract — a small keyword with large, long-lived API and coupling consequences.

  • When must you use requires transitive instead of plain requires?
    When types from the required module appear in your own exported API (return types, parameters, supertypes). Transitive re-exports readability so your consumers can name those types without declaring their own requires on that module.
  • What does requires static mean?
    It declares an optional, compile-time-only dependency: the module is needed to compile (e.g. annotations or an optional integration) but is not required to be present at runtime, and it does not propagate readability to consumers.

saying these in an interview costs you the question

  • Using requires transitive everywhere — it over-couples consumers and is hard to walk back
  • Omitting transitive when your public API returns/accepts another module's types — forces consumers into redundant requires
  • Believing plain requires propagates to your consumers — it does not; only transitive does
  • Treating exports as cheap/non-breaking — changing the export surface is an API-breaking change
  • Forgetting named modules cannot read classpath code, complicating staged migration

context