ADR-0005: Modular Monolith¶
- Status: accepted
- Date: 2026-06-04
- Deciders: Nicolas Burri
- Refined by: ADR-0036 (the module-internal layering realized: services carry rules, repositories carry jOOQ)
Context¶
The overall architecture style must fit: a small team; a deeply transactional domain (a life-cycle transition, its approvals, and the audit event must commit atomically); RLS-based tenancy in a single PostgreSQL database (ADR-0002); simple deployment and backup (one installation per operator); and a long-running, AI-assisted development process where clear internal boundaries help parallel work.
Options Considered¶
Option 1: Microservices¶
- Pros: independent deployment/scaling per service; technology freedom per service.
- Cons: the domain's core operations span what would be service boundaries, forcing distributed transactions or sagas for the most audit-critical paths; N services to operate, version, and back up contradicts the simple-deployment goal; no team-size pressure that would justify it.
Option 2: Unstructured monolith¶
- Pros: fastest start.
- Cons: boundary erosion is certain over a long project lifetime; parallel (AI) work needs explicit seams; the planned extractions (S3 storage, possibly search) need interfaces that an unstructured codebase will not preserve.
Option 3: Modular monolith¶
Single Quarkus application, single database, internal modules with build-enforced dependency rules.
- Pros: atomic transactions across the whole domain; one deployable, one backup story; module seams give parallel agents clear work boundaries; designated extraction candidates keep their interfaces honest from day one.
- Cons: module discipline must be actively enforced (tooling required); scaling is whole-application only (acceptable: a QMS is not a high-load system).
Decision¶
Option 3: modular monolith. One Kotlin/Quarkus application, one PostgreSQL database, one deployable artifact. Initial module decomposition (detailed in arc42 §5):
| Module | Responsibility |
|---|---|
scope |
Mandators, scopes, scope codes, capability flags, tenancy/RLS integration |
catalog |
Role and document-type catalogs, per-scope activations, role assignments |
document |
Documents, versions, document IDs, provenance, derive operation |
lifecycle |
State machine, review policies, approvals, review comments |
storage |
Blob store interface; file-system implementation, S3 later (ADR-0001) |
search |
Indexing and retrieval; designated extraction candidate |
backup |
Per-scope and full export/restore |
audit |
Append-only audit events |
api |
REST layer; future MCP extension point |
The Angular frontend is a separate application consuming the REST API.
Boundary enforcement:
- Each module is a separate Gradle module; allowed dependencies are declared explicitly.
- Architecture tests (Konsist) verify rules the build graph cannot express
(e.g. only
storagetouches blob paths; onlyapiis reachable from outside). Implemented in the test-only modulebackend/architecture-tests.
Consequences¶
- Module boundaries are part of code review: new cross-module dependencies require justification, not just a build-file edit.
searchandstoragecommunicate with the rest only via their interfaces, keeping later extraction (separate process / S3) a local change.- The arc42 building block view (§5) documents the module decomposition and is kept in sync as modules evolve.
- Scaffolding (Gradle multi-module setup) follows this decomposition.
Addendum (2026-07-16): the document/lifecycle split as built¶
The initial table above assigned the state machine, review policies, approvals, and review
comments to lifecycle. As implemented, the transaction-heavy state machine (LifecycleService,
the state-conditional transitions in VersionRepository, ReleaseGate) and review comments
(CommentService) live in document, because they operate directly on versions and content;
lifecycle holds the contracts those flows consume — the domain-event types and
DomainEventPublisher (ADR-0034), the append-only approval repository, and the review-policy +
per-document workflow-role binding repositories/resolvers (ADR-0030, ADR-0064). This keeps the
document → lifecycle dependency one-directional and cycle-free. Two later modules also joined the
set: identity (global OIDC-subject users, ADR-0011/0013) and test-support (LqmsTestDb
template-database fixture, 2026-07-16). The authoritative current decomposition is arc42 §5,
per the Consequences note above; this ADR records the original decision unchanged.