ADR-007: Event-Based Inter-Module Communication
Field |
Value |
|---|---|
Status |
|
Date |
2026-05-12 |
Deciders |
Full Team |
Supersedes |
– |
Superseded by |
– |
Context
Within our Spring Modulith application (ADR-002), modules need a defined and consistent way to interact with each other. Without a clear rule, developers will default to direct Spring Bean injection across module boundaries, which silently couples modules and undermines the modularity we chose Spring Modulith for.
Decision Drivers
Inter-module coupling must be explicit, auditable, and enforceable
Communication pattern should align naturally with Spring Modulith’s model
The chosen pattern should not block a future extraction of modules into separate services
Simplicity preferred — low boilerplate for the common case
Considered Options
Option A – Spring Application Events (
ApplicationEventPublisher/@ApplicationModuleListener)Option B – Direct Spring Bean injection across module boundaries
Option C – REST / HTTP calls between modules (in-process or extracted)
Decision
Chosen option: Option A – Modules communicate exclusively through Spring Application Events. No module may directly inject or call a Spring Bean owned by another module.
Rationale
Spring Application Events are the communication primitive Spring Modulith is designed around.
Publishing a named event from api/ makes the contract between modules explicit in code:
any developer can open a module’s api/ package and immediately understand what that module
broadcasts to the rest of the system. @ApplicationModuleListener is used instead of
@EventListener so that Spring Modulith can verify, document, and trace all cross-module
interactions. This pattern also maps naturally to a message broker (e.g. Kafka, RabbitMQ)
if a module is ever extracted into a separate service — the event structure stays the same,
only the transport changes.
Convention on sync vs. async:
Spring’s default event dispatch is synchronous. For events where the consumer must not run
within the publisher’s transaction, @ApplicationModuleListener(async = true) or Spring
Modulith’s transactional event publication must be used explicitly. This decision must be made
consciously per listener and documented inline.
Pros and Cons of the Options
Option A – Spring Application Events
✅ Loose coupling — the publishing module has no compile-time dependency on consumers
✅ Explicit contracts — every cross-module interaction is represented by a named event class
✅ Verifiable — Spring Modulith documents and verifies event flows via
@ApplicationModuleTest✅ Migration-ready — event structure maps directly to a message broker if needed
❌ Synchronous by default — async behavior requires explicit opt-in per listener
❌ Slightly more indirection than a direct method call — debugging requires following events
Option B – Direct Spring Bean Injection Across Modules
✅ Familiar and straightforward — no new pattern to learn
❌ Creates invisible compile-time coupling between modules
❌ Violates Spring Modulith’s module boundary model — caught as a test failure
❌ Makes future service extraction significantly harder
Option C – In-Process REST / HTTP Calls
✅ Mirrors what inter-service communication would look like after extraction
❌ Unnecessary network stack overhead for in-process calls
❌ High boilerplate for a pattern that offers no benefit within a single JVM
Consequences
Positive:
Every cross-module interaction is traceable to a named event class in a module’s
api/package@ApplicationModuleTestverifies that no module directly accesses another module’s internalsThe event model is a direct stepping stone to async messaging if modules are ever extracted
Negative / Trade-offs:
Synchronous event dispatch (the default) provides structural decoupling but not runtime decoupling — publishers and consumers still run in the same thread and transaction unless explicitly configured otherwise
Developers unfamiliar with event-driven patterns may find debugging less straightforward than following a direct call stack
Follow-up actions:
Establish and document the team’s rule for when to use async vs. synchronous event dispatch
Add event flow verification to the
@ApplicationModuleTestsetup for every moduleDefine where event classes live within each module → ADR-004