-
Notifications
You must be signed in to change notification settings - Fork 0
ADR 0001 Modular Monolith
Tiana_ edited this page May 30, 2026
·
1 revision
Status: Accepted Date: 2026-04-25 Decider: Maintainer
FinCore Engine spans multiple bounded contexts (Ledger, Payments, Compliance, Decision Engine, Webhooks, Outbox). Two architectural extremes:
- Microservices from day one - each context is a separate service, separate repo, separate deployment, separate database. Industry's go-to story for "scalable" architecture.
- Pure monolith - single application, single codebase, no boundaries. Fast to build, painful to scale or extract later.
For a v0.1 OSS release prioritizing developer adoption (target: docker compose up works in 30 seconds) and built by a single maintainer, both extremes are wrong:
- Microservices from day one means: 6+ services to operate, distributed sagas, gRPC tooling, eventual consistency surprises, debug-across-services overhead. Adopters give up trying to run it locally. The OSS dies in obscurity.
- Pure monolith means: when extraction is needed (Y1 H2 onward as services need independent scaling), the boundaries are entangled and refactoring is a multi-month nightmare.
We need the simplicity of a monolith with the boundary discipline of microservices.
Adopt modular monolith for v0.1 with an explicit roadmap to polyrepo + microservices when adoption demands it.
Concretely:
- All bounded contexts live as separate Gradle modules in the same repository
- Each module has its own DB schema (logical separation in single Postgres instance)
- Modules talk to each other only through published interfaces in a
api-prefixed module - no direct entity imports - Module-private classes use Kotlin
internalvisibility - ArchUnit tests in CI prevent cross-module entity leaks
- Inter-module calls in v0.1 are synchronous in-process Kotlin function calls
- Same interfaces drop-in replaceable with REST/gRPC clients post-extraction
- Outbox pattern + Kafka API used for cross-context events even in monolith mode (no shortcuts)
- After extraction (per-service repos
tiana-code/ledger-service, etc.), Helm umbrella chart intiana-code/fincore-helm-chartsorchestrates deployment
Triggers for extracting a service to its own repo:
- Independent scaling needs (one service hits CPU/memory ceiling separately)
- Independent deploy cadence (one service ships weekly, another monthly)
- Independent team ownership emerges
- Standalone library demand (e.g., decision-engine as a Drools-replacement consumer wants only that JAR)
-
docker compose upbrings the whole system up in 30 seconds, single JVM process - Local development is fast (no inter-service HTTP)
- Cross-context refactoring is one PR, one CI run
- Developers can read the entire codebase without context-switching across repos
- DB transactions span multiple aggregates when business invariants require it (e.g., Payment + Ledger Transaction in one tx for v0.1; saga later)
- Testing is fast - no need for service mesh in tests
- Tight coupling within JVM - careless changes can leak across contexts. Mitigated by ArchUnit tests + module visibility rules.
- Single deployment unit means one service's bug can affect all others. Mitigated by bulkhead patterns (Architecture-Resilience#bulkheads).
- Scaling means scaling everything. Acceptable until v1.x; horizontal scale of single process is fine for adopter workloads up to ~5000 tx/sec.
- "Microservices" is the trendy answer. We'll get pushback in some Hacker News comments. Many practitioners have written about modular monolith being the right starting point. Cite when responding.
- Extraction story is well-trodden - Sentry, Stripe, Shopify all started monolith-then-extracted. Frame as "we know how this evolves."
- Rejected: developer experience too heavy for solo maintainer + early adopters
- Rejected: docker-compose with 6+ images takes 90+ seconds to start
- Rejected: distributed tracing is required but not yet implemented
- May reconsider for v2.x when team grows beyond 1 engineer
- Rejected: boundaries are easier to enforce at design time than refactor in
- Rejected: Drives single-team thinking; we want polyrepo as eventual destination
- Rejected: cold starts hurt p99 latency
- Rejected: vendor lock-in conflicts with self-hosting goal
- Rejected: financial systems benefit from long-running JVM processes (caches, connection pools)
- Each release tag verifies: ArchUnit tests pass, all 6 contexts can be deployed independently from current code (dry-run extract)
- v0.5 milestone includes
extract-decision-engine.mdrunbook proving extraction is mechanical - Performance benchmarks demonstrate single-instance handles target load (1000 tx/sec sustained)
- ADR-0003 - outbox preserves cross-context event flow
- ADR-0004 - Hibernate works across modules without complication
- Architecture-Overview - visual diagrams of the architecture
- Future ADR-0010 - when sagas replace cross-module DB transactions post-extraction
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO