How do named interfaces change what verify() allows, and why would you use them instead of relying on the default module API?
answer
- default API = base package types only
- @NamedInterface labels a curated slice
- 'module :: interface' pins consumer to a slice
- expose a nested package or split multiple contracts
- string-matched names; nested→exposed is a widening
basics
~20 sBy default a module exposes every type in its base package. A named interface lets you publish a curated subset (a labeled package or types) so other modules can only use that slice — verify() then fails any access outside it.
solid answer
~40 sWithout configuration, a module's 'public API' is everything in its base (top-level) package; nested packages are internal. A @NamedInterface lets you carve the exposed surface into labeled subsets — e.g. annotate a sub-package's package-info with @NamedInterface("stock-api"). Once a module has named interfaces, other modules can be restricted to a specific one via allowedDependencies = { "inventory :: stock-api" }, and verify() rejects any reference to the module's other exposed types. This matters when the default 'whole base package is public' surface is too coarse: you want to expose a small contract while keeping supporting public-but-not-for-others types unreachable. It also lets a module publish multiple distinct contracts (e.g. a read API and an admin API) to different consumers, all enforced structurally.
code
java · 11 lines// Producer: expose a curated slice of 'inventory'
// com/example/shop/inventory/stock/package-info.java
@org.springframework.modulith.NamedInterface("stock-api")
package com.example.shop.inventory.stock;
// Consumer: 'order' may use ONLY that slice
// com/example/shop/order/package-info.java
@org.springframework.modulith.ApplicationModule(
allowedDependencies = { "inventory :: stock-api" })
package com.example.shop.order;
// order referencing any other exposed inventory type -> verify() Violationsgo deeper
Know default exposure = base-package types; nested packages are internal.
Explain that @NamedInterface publishes a curated slice and consumers target it with 'module :: iface'.
Discuss multiple contracts per module, promoting nested packages, and how verify() enforces the slice.
Weigh coarse default exposure vs curated interfaces for contract minimization and consumer-specific APIs; note static-analysis limits.
## The default exposure model Every Spring Modulith module has a **base package** (its root). By default: - Types **directly in the base package** are the module's **public API** — any other module may reference them (subject to allowed-dependency rules). - Types in **nested sub-packages** are **internal** — off-limits to other modules; `verify()` fails on cross-module access to them. That single, package-based boundary is often good enough. But it's coarse: sometimes you have types that must be `public` for framework or intra-module reasons yet shouldn't be part of the *cross-module contract*, or you want to offer **different contracts to different consumers**. ## Named interfaces: curated exposure A **named interface** is a *labeled slice* of a module's exposed surface, declared with `@org.springframework.modulith.NamedInterface`. Two common forms: 1. **Package-based** — mark a sub-package's `package-info.java`: ```java @org.springframework.modulith.NamedInterface("stock-api") package com.example.shop.inventory.stock; ``` Now the `stock` sub-package (normally internal!) becomes a named, exposed interface called `stock-api`. 2. **Type-based** — annotate specific types to assign them to a named interface. A module can declare **several** named interfaces, each a different contract. ## How this changes `verify()` - Consumers can be pinned to a specific slice through the **double-colon** allow syntax: ```java @org.springframework.modulith.ApplicationModule( allowedDependencies = { "inventory :: stock-api" }) package com.example.shop.order; ``` `order` may now reference **only** the `stock-api` named interface of `inventory`. Referencing any other exposed inventory type is a `verify()` violation. - If a module defines named interfaces, allow-list entries that name the module *without* a `:: interface` may resolve to the **unnamed/default** interface — so being explicit prevents surprises about which slice is actually granted. ## Why use them instead of the default API - **Narrow the blast radius**: publish a minimal, intentional contract; keep the rest reachable only inside the module even if it's `public`. - **Multiple contracts**: expose, say, `read-api` to most modules and `admin-api` to only one, each enforced. - **Expose a deliberately-nested package** as API without flattening your package structure. - **Documentation clarity**: named interfaces show up in the Documenter canvas, making the intended contract explicit. ## Gotchas - Naming a module in an allow-list is **string-matched**; the same applies to the interface name after `::` — a typo silently points at nothing valid and fails verification. - Promoting a nested package to a named interface **changes** it from internal to exposed — a deliberate widening; don't do it accidentally. - Named interfaces control *what's reachable*, not *who can reach it* by identity — access control is still 'any module allowed to depend on this slice'. Fine-grained per-consumer restriction is expressed on the **consumer's** allowedDependencies, not the producer. - Reflection-based access still bypasses the static check. ## When to use Reach for named interfaces once a module's honest contract is smaller than 'everything in the base package', or when one module must serve distinct APIs to distinct consumers. Otherwise the default exposure is simpler and sufficient.
- A type is 'public' in a module's base package. Can another module always use it?Only if that other module is allowed to depend on the producer (or its default/named interface). Being public in the base package makes it part of the exposed API, but the consumer's allowedDependencies still governs access.
- How do you expose a nested (normally internal) package to other modules?Annotate that sub-package's package-info with @NamedInterface("name"). That promotes it from internal to an exposed, named slice consumers can be granted via 'module :: name'.
saying these in an interview costs you the question
- Believing nested sub-packages are exposed by default (they are internal)
- Thinking named interfaces restrict WHICH consumer by identity rather than WHAT surface is reachable
- Assuming naming just the module always grants its whole surface even when named interfaces exist