ADR-008: Spring Modulith Modular Monolith Over Distributed Microservices¶
- Date: 2026-08-07
- Status: Accepted
- Phase: Phase 6B (Architecture Review)
Context & Problem Statement¶
Payflow processes peer-to-peer financial transfers where a single operation involves debiting the sender, crediting the receiver, recording a transaction, writing ledger entries, and publishing domain events. Splitting these into separate microservices (e.g., User Service, Transfer Service, Ledger Service) would lose local database ACID transactions, requiring distributed saga orchestration or two-phase commit (2PC) to maintain consistency. This introduces significant complexity, dual-write bugs, and split-brain risks before the domain rules are even stable.
Considered Options¶
- Distributed Microservices (HTTP/gRPC): Each domain concern runs as a separate deployable service. Requires distributed transactions (sagas, 2PC) for cross-service state consistency.
- Plain Monolith (No Module Boundaries): Single deployable with no enforced package boundaries. Simple but leads to spaghetti coupling as the codebase grows.
- Spring Modulith Modular Monolith: Single deployable with strict, compile-time-verified module boundaries. Uses Spring's
ApplicationEventPublisherfor inter-module communication. Evolves to Kafka event streaming viaspring-modulith-events-kafkawithout domain code changes.
Decision Outcome¶
Chosen Option: Spring Modulith Modular Monolith
Rationale¶
- ACID Safety: Sender debit, receiver credit, transaction record, ledger entries, and domain event all execute in a single
@Transactionaldatabase transaction. Zero distributed transaction overhead. - Enforced Boundaries:
ApplicationModules.of(PayflowApiApplication.class).verify()test validates strict package encapsulation at compile time, preventing accidental cross-module coupling. - Event Publication Registry: Spring Modulith's
spring-modulith-starter-jpapersists domain events (e.g.,TransferCompletedEvent) to anevent_publicationtable atomically within the same DB transaction. This is a framework-managed transactional outbox. - Evolutionary Path: Adding
spring-modulith-events-kafkain Phase 9A auto-externalizes events to Kafka topics with zero changes toTransactionServicedomain code. The service continues to callapplicationEventPublisher.publishEvent()— the framework bridges to Kafka transparently. - Architectural Simplicity: Avoids premature microservice distribution and preserves single-database transaction boundaries until physical service isolation is explicitly required.
Consequences¶
- Positive: Single-database ACID safety, zero distributed transaction complexity, compile-time boundary enforcement, seamless Kafka evolution path.
- Negative / Trade-offs: All modules share a single database and JVM. Horizontal scaling is per-application-instance, not per-module.
- Risks & Mitigations: If individual modules need independent scaling (unlikely at Payflow's scale), Spring Modulith modules can be extracted to standalone services along their already-enforced API boundaries.