skip to content

How do you declare which modules a given module is allowed to depend on, and how does verify() catch a violation?

level: middleimportance: must knowfreq 55%

answer

  1. package-info.java + @ApplicationModule
  2. allowedDependencies = closed allow-list
  3. adding it flips open→closed
  4. 'module :: named-interface' syntax
  5. Violations exception lists offending class

basics

~10 s

You put an @ApplicationModule annotation with allowedDependencies on the module's package-info.java. verify() then fails if any class in that module references a module not on the list.

solid answer

~40 s

By default a module may depend on any other module's exposed (API) types. To lock it down, add a package-info.java in the module's base package annotated with @ApplicationModule(allowedDependencies = { "inventory", "shared" }). That turns the module into a 'closed' allow-list: it may now only reach the named modules, and only their public types — everything else, including previously-fine modules, becomes a violation. verify() derives ArchUnit rules from these declarations and, on any disallowed cross-module reference or access into another module's internal package, throws a Violations exception listing the exact offending class and target. You can also reference a specific named interface with the 'module :: interfaceName' syntax to allow only part of a module's surface.

code

java · 8 lines
java
// src/main/java/com/example/shop/order/package-info.java
@org.springframework.modulith.ApplicationModule(
    allowedDependencies = { "inventory", "shared :: dto" }
)
package com.example.shop.order;

// A class in 'order' referencing com.example.shop.billing.* now
// causes verify() to throw Violations: billing is not on the list.

go deeper

for a junior

Know that the allow-list lives on package-info.java via @ApplicationModule(allowedDependencies=...).

for a middle

Explain the open→closed flip, non-transitivity, and how verify() reports the offending class.

for a senior

Cover named-interface-scoped allowances ('module :: iface') and the rename-breaks-the-string gotcha.

for a principal

Discuss using allow-lists to codify intended layering incrementally and the static-analysis blind spots (reflection).

## The default rule Out of the box, Spring Modulith is permissive about *which* modules you depend on: any module may depend on the **exposed** types of any other module. What's always forbidden is reaching into another module's **internal** packages, and forming **cycles**. Allowed-dependencies let you tighten the first part into an explicit allow-list. ## Declaring an allow-list with `@ApplicationModule` You configure a module through a **`package-info.java`** file in the module's **base package** (the root package of that module). Annotate the package with `@org.springframework.modulith.ApplicationModule`: ```java @org.springframework.modulith.ApplicationModule( allowedDependencies = { "inventory", "shared" } ) package com.example.shop.order; ``` Semantics: - The `order` module may now depend **only** on the `inventory` and `shared` modules. - Any reference from `order` to a module **not** in that list is a violation — even one that was legal before you added the annotation. Declaring `allowedDependencies` flips the module from 'open to all' to 'closed to all but these'. - Depending on nothing? `allowedDependencies = {}` forbids all outgoing module dependencies. ## Named-interface granularity A module can expose one or more **named interfaces** (curated subsets of its public types, declared via `@NamedInterface`). You can allow only a specific one using the double-colon syntax: ```java @ApplicationModule(allowedDependencies = { "inventory :: stock-api" }) package com.example.shop.order; ``` Now `order` may use only the `stock-api` named interface of `inventory`, not its whole public surface. ## How `verify()` catches a violation 1. Modulith reads each module's `@ApplicationModule` metadata and builds the allowed-dependency graph. 2. It generates **ArchUnit** rules encoding: 'classes in module X may only access classes belonging to allowed modules / named interfaces, and never internal packages of others.' 3. During `verify()`, ArchUnit walks the actual class-to-class references (field types, method params/returns, calls, inheritance). Any edge that violates the derived rule is collected. 4. The result is a `Violations` exception whose message enumerates each violation with the source type, the illegal dependency, and the target — e.g. `Module 'order' depends on module 'billing' via ...` — making it easy to locate. ## Edge cases & gotchas - **Transitive isn't automatic**: if `order` is allowed `inventory`, and `inventory` uses `shared`, that does *not* grant `order` access to `shared`. Each module's list is about *its own* direct references. - **Renaming a module** (its base-package simple name, or a `displayName`) means every allow-list referencing the old name breaks — the string must match the module's name. - **Adding the annotation is a breaking tightening**: introducing `allowedDependencies` to a previously-open module often surfaces a burst of pre-existing violations. That's expected and desirable. - The check is **compile-time/static** — dependencies expressed only via reflection or Spring bean lookups by type string won't be seen. ## When to use Use explicit `allowedDependencies` on modules where you want to hard-enforce an intended layering (e.g. a `web` module may talk to `order` but never `persistence` internals). Leave others open until you need the constraint.

  • If module A is allowed to depend on B, and B depends on C, can A use C?
    No. Allowed dependencies are not transitive. A's list only governs A's own direct references; to use C, A must list C (or a named interface of C) explicitly.
  • What happens to existing legal dependencies when you first add allowedDependencies to a module?
    The module becomes closed: anything not on the list — including formerly-fine dependencies — becomes a violation. Expect verify() to surface them until you either list or remove them.

saying these in an interview costs you the question

  • Assuming allowed dependencies are transitive
  • Thinking the allow-list is configured in application.yml rather than package-info.java
  • Believing an empty allowedDependencies list means 'allow everything' (it means allow nothing)

context