skip to content

What problem do named interfaces (@NamedInterface) solve in Spring Modulith, and how do they interact with allowedDependencies?

level: seniorimportance: should knowfreq 40%

answer

  1. Default: only base package exposed, rest internal
  2. @NamedInterface('spi') exposes a sub-package slice
  3. Consume via 'module :: interfaceName'
  4. Base package = default/unnamed interface
  5. allowedDependencies scopes which named interface is legal

basics

~20 s

By default only a module's base-package types are exposed. A named interface (@NamedInterface) publishes a specific sub-package as an additional named entry point, so other modules can depend on just that slice via 'module :: interfaceName'.

solid answer

~40 s

Normally a module exposes only the types directly in its base package; everything nested is internal. **Named interfaces** let you publish an *additional*, explicitly named slice — typically a sub-package like `order.spi` — so other modules may depend on it while the rest of the module stays hidden. You declare it with `@NamedInterface("spi")` on the package's `package-info.java` (or on individual types). Consumers then target it in their `allowedDependencies` using the `"order :: spi"` syntax, which permits coupling to that named interface only and forbids the module's other exposed types. This gives you fine-grained, directional contracts: a module can offer a stable SPI to one set of modules and a different API to others, all verified by `verify()`. It's the mechanism behind the 'advanced' arrangement where structured sub-packages still need controlled exposure.

code

java · 13 lines
java
// Producer: expose an SPI slice of the order module
// com/example/app/order/spi/package-info.java
@NamedInterface("spi")
package com.example.app.order.spi;
import org.springframework.modulith.NamedInterface;

// Consumer: may use ONLY order's spi interface
// com/example/app/fulfillment/package-info.java
@ApplicationModule(allowedDependencies = { "order :: spi" })
package com.example.app.fulfillment;
import org.springframework.modulith.ApplicationModule;
// Referencing com.example.app.order.OrderService (base pkg)
// from fulfillment now FAILS verify() — only :: spi is allowed.

go deeper

for a junior

Awareness only: a way to expose a specific part of a module beyond its base package.

for a middle

Know @NamedInterface exposes a sub-package and is consumed via 'module :: name'.

for a senior

Explain multiple named interfaces, base-package = default interface, and how allowedDependencies scopes each slice.

for a principal

Discuss modeling API/SPI/events as separate contracts, directional per-consumer routing, and avoiding premature named interfaces on simple modules.

**The gap named interfaces fill.** Default Modulith exposure is coarse: **base-package types = public**, **everything nested = internal**. That's fine for small modules, but larger modules want internal structure (sub-packages) *and* the ability to expose a curated slice — e.g. an **SPI** other modules implement, or an **events** package — without flattening everything into one package or exposing all internals. **Declaring a named interface.** Two forms: - **Package-based:** put `@NamedInterface("spi")` in the `package-info.java` of a sub-package. All top-level types of that sub-package become part of the named interface `spi`. ```java // com/example/app/order/spi/package-info.java @org.springframework.modulith.NamedInterface("spi") package com.example.app.order.spi; ``` - **Type-based:** annotate individual types with `@NamedInterface("spi")` to group them into a named interface regardless of package. A module can have **multiple** named interfaces (e.g. `api`, `spi`, `events`). The base package itself is the **unnamed/default** named interface. **Consuming a named interface.** Other modules reference it with the double-colon syntax in their dependency declaration: ```java @ApplicationModule(allowedDependencies = { "order :: spi" }) package com.example.app.fulfillment; ``` This says fulfillment may use **only** the `spi` named interface of order — not order's default API and not other named interfaces. If fulfillment references an order type outside `spi`, `verify()` fails. You can allow several: `{ "order :: spi", "order :: events" }`. **Interaction with allowedDependencies.** - Without `allowedDependencies`, a consuming module may use any module's exposed named interfaces (default permissive) — but still not internals that aren't part of any named interface. - With `allowedDependencies`, the `module :: interface` entries precisely scope *which* interface(s) of a target module are legal. `"order"` alone means order's default (base-package) interface; `"order :: spi"` means the spi slice. **Enforcement.** Same as everything else — static, at `ApplicationModules.of(App.class).verify()` time via ArchUnit. Named interfaces change *what is exposed*; allowedDependencies changes *who may consume what*. Together they express directional, slice-level contracts. **Gotchas.** - A sub-package with **no** `@NamedInterface` is fully internal — adding structure doesn't auto-expose it. - Named-interface and module names are **strings** in `allowedDependencies`; typos resolve to 'unknown' and fail verification (not compile). - The base package is always the default interface; you can't hide it by adding named interfaces elsewhere. - Type-based named interfaces let you expose a couple of classes from an otherwise-internal package without moving them. **When to use.** Use named interfaces when a module needs real internal package structure *and* must expose more than one coherent contract (public API vs. SPI vs. published events), especially combined with `allowedDependencies` to route each consumer to the correct slice. For simple modules, the default base-package exposure is enough — don't add named interfaces prematurely.

  • If a module has a sub-package with no @NamedInterface, can other modules use its types?
    No. Without a named interface, a nested sub-package is fully internal. Only the base package (default interface) plus any declared named interfaces are exposed.
  • What does allowedDependencies = { "order" } permit versus { "order :: spi" }?
    `"order"` permits the default (base-package) interface of order. `"order :: spi"` permits only the spi named interface and forbids the base-package API and any other named interface.

saying these in an interview costs you the question

  • Believing sub-packages are exposed automatically once you add structure
  • Thinking 'order :: spi' broadens rather than narrows the allowance
  • Assuming the base package needs @NamedInterface to be public
  • Confusing named interfaces (what's exposed) with allowedDependencies (who may consume)

context