skip to content

Spring Modulith

Spring Modulith brings module boundaries to a single deployable: declared application modules, verified dependencies, module events and per-module tests. Interviewers ask about it whenever the topic is 'monolith or microservices', because it is the credible middle.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

21

In Spring Modulith, what defines an 'application module' and how do you obtain the module model in code?

level: juniorimportance: must knowfreq 70%

answer

  1. Direct sub-package of main app package = one module
  2. Top-level types = API; .internal = hidden
  3. ApplicationModules.of(App.class)
  4. Test/build-time static model, not runtime
  5. Iterable<ApplicationModule>, .verify(), Documenter

basics

~10 s

By default an application module is a direct sub-package of your main application class's package. You get the model in a test with ApplicationModules.of(Application.class).

solid answer

~30 s

Spring Modulith treats each direct sub-package of the package containing your `@SpringBootApplication` class as one application module. So if your app lives in `com.example.app`, then `com.example.app.order` and `com.example.app.inventory` are two modules. The module's top-level types are its public API; types in nested sub-packages (e.g. `order.internal`) are considered internal and hidden from other modules. You build the module model programmatically with `ApplicationModules.of(Application.class)`, which scans the class's package and returns an `ApplicationModules` instance you can iterate, verify (`.verify()`), or render into documentation. This is a compile/test-time model derived from package structure and bytecode — there's no runtime container change.

code

java · 18 lines
java
// com.example.app.Application (main class)
@SpringBootApplication
class Application {}

// A test that builds the module model from the package structure
class ModularityTests {

    @Test
    void printsModules() {
        ApplicationModules modules = ApplicationModules.of(Application.class);
        modules.forEach(System.out::println); // one line per module
    }
}

// Packages:
// com.example.app.order        -> module "order" (public: OrderService)
// com.example.app.order.internal -> internal to "order" (hidden)
// com.example.app.inventory    -> module "inventory"

go deeper

for a junior

Know: module = direct sub-package of the main app package, and ApplicationModules.of(App.class) builds the model.

for a middle

Add the API-vs-internal rule: top-level types are exposed, nested sub-packages are hidden.

for a senior

Explain it's static test-time analysis (ArchUnit-based), no runtime change, plus Documenter/verify usage.

for a principal

Discuss simple vs advanced arrangement, exclusion predicates, and positioning this as a modular-monolith enforcement strategy vs microservices.

**Spring Modulith** is a Spring project that helps you structure a Spring Boot monolith into well-defined logical modules, then *verify* those boundaries automatically instead of relying on discipline alone. **What is an application module?** With the default (simple) arrangement, an application module is **one direct sub-package** of the package that contains your main `@SpringBootApplication` class (the 'application main package'). Example: with main class in `com.example.app`, the packages `com.example.app.order`, `com.example.app.inventory`, and `com.example.app.catalog` are each a distinct application module. The module *is* the package (plus its sub-packages). **API vs internals.** Types that sit **directly** in the module's base package (e.g. `com.example.app.order.OrderService`) form the module's **public API** — other modules may depend on them. Types in **nested sub-packages** (e.g. `com.example.app.order.internal.OrderRepository`) are treated as **module-internal** and are *not* allowed to be referenced from other modules. This is the core rule Modulith enforces: cross-module access is only legal against another module's exposed (top-level) types. **Getting the model.** You create the in-memory module model with the static factory: ```java ApplicationModules modules = ApplicationModules.of(Application.class); ``` This uses the class's package as the scanning root. The resulting `ApplicationModules` object is `Iterable<ApplicationModule>`; you can print it (`modules.forEach(System.out::println)`), verify boundaries (`modules.verify()`), or feed it to the `Documenter` to generate C4/PlantUML diagrams and module canvases. **Where it runs.** This model is built at **test/build time** by analyzing packages and bytecode (Modulith builds on **jQAssistant/ArchUnit-style** static analysis). It does *not* alter the runtime Spring `ApplicationContext` — your app still boots as a normal Spring Boot monolith. The value is the *verification*, not any runtime isolation. **Two arrangement styles.** The above is the **simple** arrangement (module = single package). Modulith also supports an **advanced** arrangement where a module base package may contain further nested application-level packages; there you use **named interfaces** and explicit configuration to expose more than just the base package. Most codebases start simple. **Excluding packages.** `ApplicationModules.of(Application.class, JavaClass.Predicates...)` accepts an exclusion predicate to drop packages (e.g. generated code) from the model. **When to use.** Reach for application modules when a Spring Boot monolith is growing and you want enforced internal boundaries (a 'modular monolith') without splitting into microservices — you get compile-safe encapsulation plus generated documentation.

  • If OrderService is in com.example.app.order.internal instead of com.example.app.order, can InventoryService use it?
    No. Anything under an `internal` (nested) sub-package is module-private, so a reference from the inventory module would be flagged as a boundary violation by `verify()`.
  • Does building the ApplicationModules model change how the app runs at runtime?
    No. It's a static, test/build-time analysis of packages and bytecode. The app still boots as an ordinary Spring Boot monolith; Modulith only verifies and documents structure.

saying these in an interview costs you the question

  • Thinking modules change runtime wiring or create separate contexts
  • Believing every package (including nested internal ones) is its own module
  • Assuming you need an annotation on every module for it to be recognized

context

open as a page

Why do Spring Modulith applications communicate between modules with published events, and what does @ApplicationModuleListener do?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Instead of one module directly calling another, it publishes an event. A method marked @ApplicationModuleListener in the other module receives that event asynchronously after the publisher's transaction commits, so the modules stay loosely coupled.

open as a page

What does ApplicationModules.verify() do in Spring Modulith, and how do you wire it into a test?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It scans your application's packages, discovers each top-level package as a module, and checks that modules only depend on ones they are allowed to. You run it from a JUnit test; if a rule is broken the test fails.

open as a page

How does Spring Modulith decide which types of a module are public API versus internal, and how is that enforced?

level: middleimportance: must knowfreq 65%

basics

~10 s

Types directly in the module's base package are public API; types in nested sub-packages are internal. Other modules referencing internal types cause ApplicationModules.of(App.class).verify() to fail.

open as a page

What is @ApplicationModuleTest and how does it differ from @SpringBootTest? Explain its bootstrap modes.

level: middleimportance: must knowfreq 55%

basics

~20 s

@ApplicationModuleTest is a Spring Modulith slice test that bootstraps only one module (plus chosen dependencies) instead of the whole app. Its BootstrapMode is STANDALONE (module only), DIRECT_DEPENDENCIES, or ALL_DEPENDENCIES, giving faster, isolated per-module integration tests.

open as a page

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%

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.

open as a page

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%

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.

open as a page

What is the Spring Modulith Event Publication Registry, what problem does it solve, and how does it work?

level: seniorimportance: must knowfreq 38%

basics

~20 s

Because async after-commit listeners run outside the publisher's transaction, a crash could lose the event. The registry persists a publication row (in the publisher's transaction) for each listener, marks it complete when the listener finishes, and leaves failed ones incomplete for replay — giving at-least-once delivery.

open as a page

Explain the Spring Modulith Scenario API for testing asynchronous inter-module event flows.

level: seniorimportance: must knowfreq 45%

basics

~20 s

Scenario is a fluent, Awaitility-backed test API (injected into @ApplicationModuleTest methods) that stimulates an action or publishes an event, then waits for an expected event to arrive or a state change to occur before verifying — removing flaky sleeps in async, event-driven tests.

open as a page

What is the Spring Modulith Moments component and which time-passage events does it publish?

level: juniorimportance: should knowfreq 30%

basics

~10 s

Moments is a Spring Modulith helper (spring-modulith-moments) that publishes application events as time passes: HourHasPassed, DayHasPassed, WeekHasPassed, MonthHasPassed, QuarterHasPassed and YearHasPassed. Modules subscribe with @EventListener to run scheduled-style logic.

open as a page

Break down the semantics of @ApplicationModuleListener — what does each composed part mean, and what gotchas follow from async + AFTER_COMMIT + REQUIRES_NEW?

level: middleimportance: should knowfreq 40%

basics

~10 s

It stacks @TransactionalEventListener (fire after the publisher commits), @Async (run on another thread), and @Transactional(REQUIRES_NEW) (run in its own transaction). Gotchas: needs @EnableAsync, exceptions don't roll back the publisher, and delivery is eventually consistent.

open as a page

How do you deterministically test time-triggered logic with the Spring Modulith TimeMachine?

level: middleimportance: should knowfreq 35%

basics

~10 s

Enable the TimeMachine (spring.modulith.moments.enable-time-machine=true), inject it into the test, and call shiftBy(Duration) to fast-forward. Shifting past a boundary synchronously publishes the corresponding Moments events, so you can assert the reaction immediately.

open as a page

How do you generate module documentation and diagrams with Spring Modulith's Documenter, and what output do you get?

level: middleimportance: should knowfreq 40%

basics

~10 s

You create a Documenter from your ApplicationModules and call methods like writeDocumentation(). It emits PlantUML component diagrams (per module and an overview) plus a Markdown 'module canvas', into target/spring-modulith-docs by default.

open as a page

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

level: seniorimportance: should knowfreq 40%

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'.

open as a page

How does incomplete-event recovery work in Spring Modulith, and how would you operate it in production (restart replay, scheduled resubmit, cleanup, idempotency)?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Failed listeners leave incomplete publication rows. Recover them by enabling republish-outstanding-events-on-restart, or by scheduling a job that calls IncompleteEventPublications.resubmitIncompletePublicationsOlderThan(...). Purge completed rows with CompletedEventPublications. Because resubmits re-run listeners, handlers must be idempotent.

open as a page

How do you assert which application events were published during a module test using PublishedEvents / AssertablePublishedEvents?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Inject PublishedEvents (or AssertablePublishedEvents) into an @ApplicationModuleTest. After exercising code, query it: eventsOfType(X.class).matching(...) to filter, and with AssertablePublishedEvents use assertThat(events).contains(X.class)... to fluently verify the right events were published.

open as a page

How do named interfaces change what verify() allows, and why would you use them instead of relying on the default module API?

level: seniorimportance: should knowfreq 33%

basics

~20 s

By 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.

open as a page

You're introducing Spring Modulith into an existing Spring Boot monolith. How do you model modules, verify boundaries in CI, and roll it out without breaking the build on day one?

level: principalimportance: should knowfreq 30%

basics

~10 s

Reorganize code so each business area is a direct sub-package with internals under nested packages, add one test calling ApplicationModules.of(App.class).verify() in CI, and adopt incrementally — start permissive, then add allowedDependencies module by module.

open as a page

What does @Externalized do in Spring Modulith, and how does externalizing events to a broker relate to the Event Publication Registry?

level: principalimportance: should knowfreq 20%

basics

~20 s

@Externalized marks an application event to also be published to an external broker (Kafka, RabbitMQ, etc.). Its value 'target::key' sets the destination and a SpEL routing key. The same Event Publication Registry tracks externalization, giving durable at-least-once delivery to the broker.

open as a page

As an architect, how would you operationalize Modulith verification in CI, and what are its structural limits you must account for?

level: principalimportance: should knowfreq 24%

basics

~20 s

Put verify() in a fast unit test that runs on every build so boundary breaks fail the pipeline, and regenerate Documenter diagrams alongside it. Remember it's compile-time static analysis: reflection, config strings, and runtime wiring are invisible to it.

open as a page

When would you choose Spring Modulith Moments in production versus a real scheduler, and what distributed-deployment concerns arise?

level: principalimportance: nice to knowfreq 20%

basics

~10 s

Use Moments for coarse, in-process, module-local reactions to elapsed time (daily/monthly rollups) where testability via TimeMachine matters. Use Quartz/cron/ShedLock when you need cron precision, missed-fire recovery, or single-firing across a clustered deployment.

open as a page