skip to content

How do you declare and constrain a module's allowed dependencies with @ApplicationModule, and what happens if a module talks to one not listed?

level: seniorimportance: must knowfreq 60%

answer

  1. @ApplicationModule(allowedDependencies=...) in package-info.java
  2. Default = depend on any module; annotate to restrict
  3. {} empty = depend on nothing
  4. 'module :: interface' targets a named interface
  5. verify() fails on unlisted dep or cycle; no runtime block

basics

~10 s

Put @ApplicationModule(allowedDependencies = {"inventory"}) in the module's package-info.java. The module may then only depend on listed modules; verify() fails if it references any other module.

solid answer

~40 s

You declare it in the module's `package-info.java`: `@ApplicationModule(allowedDependencies = { "inventory", "payment" })`. This whitelists exactly which other modules this one may depend on. By default (no annotation) a module may depend on **any** other module's exposed types, so you add `allowedDependencies` to lock a module down. Once declared, `ApplicationModules.of(App.class).verify()` fails the build if the module references any module *not* in the list — even a module that would otherwise be legal. You can target a specific named interface with the `moduleName :: interfaceName` syntax (e.g. `"order :: spi"`). An empty `allowedDependencies = {}` means the module may depend on **no** other module. Shared/open modules and the standard Spring infrastructure are still reachable. This turns intended architecture into an enforced, documented contract.

code

java · 11 lines
java
// com/example/app/order/package-info.java
@ApplicationModule(
    allowedDependencies = { "inventory", "payment :: api" }
)
package com.example.app.order;

import org.springframework.modulith.ApplicationModule;

// If OrderService references com.example.app.shipping.*,
// ApplicationModules.of(Application.class).verify() fails:
//   "Module 'order' depends on non-allowed module 'shipping' ..."

go deeper

for a junior

Know it exists: an annotation that lists which modules a module may use.

for a middle

Place it in package-info.java and know verify() fails on unlisted dependencies.

for a senior

Explain default-permissive vs empty-array semantics, named-interface targeting, and static test-time enforcement.

for a principal

Discuss encoding target architecture, name-based (non-compile-checked) module references, incremental adoption, and cycle prevention.

**Default dependency policy.** Out of the box, Spring Modulith lets any module reference any **other module's exposed (base-package) types**. Encapsulation of *internals* is always on, but *which* modules you may couple to is unrestricted until you say otherwise. That's usually too loose for a large system. **Declaring allowed dependencies.** You tighten a specific module by annotating its **package** — placed in `package-info.java` at the module's base package: ```java @org.springframework.modulith.ApplicationModule( allowedDependencies = { "inventory", "payment" } ) package com.example.app.order; ``` (In Kotlin the equivalent goes in a `package-info.java` or a `@file:`-annotated module-info-style file; Java uses `package-info.java`.) Now the **order** module is permitted to depend **only** on the `inventory` and `payment` modules. Any reference to a third module (say `shipping`) becomes a **verification failure**. **Semantics of the list:** - **Not present / no annotation:** depend on any module (default permissive). - **`allowedDependencies = {}` (empty):** depend on **no** other module — a fully standalone/leaf module. - **`allowedDependencies = {"inventory"}`:** depend on inventory only. - **Named-interface targeting:** `"order :: spi"` restricts the dependency to a specific **named interface** of that module rather than its whole API, so you can allow coupling to a narrow SPI while forbidding the rest. **Enforcement.** As with all boundary rules, this is checked statically by `ApplicationModules.of(App.class).verify()` (ArchUnit-based) at **test/build time**. If **order** imports/references a type from **shipping** while shipping isn't listed, verify() fails with a message identifying the illegal dependency and the offending types. There is no runtime enforcement — the app still wires beans normally. **What still remains reachable.** Declaring `allowedDependencies` constrains **application-module** coupling. It does not stop you from using framework/library types or modules marked as **open/shared** (via `@ApplicationModule(type = Type.OPEN)` or the shared-module mechanism), which are treated as globally available infrastructure. **Cycles.** `allowedDependencies` also helps prevent cycles: if order allows inventory and inventory allows order, verify() reports a **cyclic dependency** between modules, which is always illegal regardless of the whitelist. **Gotchas & practice.** - The annotation lives on the **package**, not on a class — a common mistake is annotating a random type. - Forgetting to list a *transitively needed* module produces a verify failure even though the code compiles fine. - Because it's opt-in per module, teams often start permissive and progressively add `allowedDependencies` to the most sensitive modules first. - Refactoring: renaming a module (its base package) breaks string references in other modules' `allowedDependencies` — those are name-based, not compile-checked, so a typo silently means 'unknown module' at verify time. **When to use.** Use `allowedDependencies` to codify a target architecture (e.g. a layered or hexagonal dependency direction) so the build fails the moment someone introduces unwanted coupling — the payoff of a modular monolith over convention-only structuring.

  • What's the difference between omitting allowedDependencies entirely and setting it to an empty array?
    Omitting it keeps the permissive default — the module may depend on any other module's exposed types. An empty array `{}` forbids all inter-module dependencies, making it a standalone/leaf module.
  • How would you allow order to use only a narrow SPI of payment, not its whole API?
    Expose that slice as a named interface in payment (e.g. `@NamedInterface("spi")`), then declare `allowedDependencies = { "payment :: spi" }` on the order package.
  • Where must the @ApplicationModule annotation be placed?
    On the module's base package, i.e. in that package's `package-info.java` — not on an individual class.

saying these in an interview costs you the question

  • Thinking allowedDependencies is just documentation with no enforcement
  • Confusing empty {} (nothing allowed) with omission (everything allowed)
  • Placing @ApplicationModule on a class instead of the package
  • Assuming the module-name strings are compile-checked (they're name-based)

context