5. Building Block View¶
5.1 Whitebox Overall System¶
LQMS is a modular monolith (ADR-0005): one Kotlin/Quarkus application plus a separate Angular frontend.
graph TD
FE["Angular frontend<br/>(separate application)"]
subgraph Backend["Quarkus application (single deployable)"]
API["api<br/>REST + DTOs, SSE, DI wiring,<br/>bootstrap, schedulers,<br/>SPA hosting, future MCP"]
DOC["document<br/>documents, versions, IDs,<br/>derive; lifecycle state machine,<br/>release gate, review comments,<br/>attachments, PDF export"]
LC["lifecycle<br/>contracts: domain events +<br/>publisher, approvals, review-policy<br/>& workflow-role repositories"]
CAT["catalog<br/>roles, document types,<br/>activations, retention,<br/>user/role admin"]
SCOPE["scope<br/>mandators, scopes,<br/>tenancy/RLS,<br/>authorization resolution"]
IDENT["identity<br/>OIDC-subject users,<br/>invite/JIT provisioning"]
STOR["storage<br/>blob store interface<br/>(FS now, S3 later)"]
SEARCH["search<br/>indexing & retrieval<br/>(extraction candidate)"]
BAK["backup<br/>in-app pg_dump + blob archive,<br/>backup evidence, job heartbeats"]
AUD["audit<br/>append-only events"]
NOTIF["notification<br/>tasks & inbox, delivery<br/>channels, event fan-out"]
PERS["persistence<br/>Flyway migrations +<br/>generated jOOQ classes"]
COMMON["common<br/>shared kernel: the domain-exception<br/>vocabulary + requireValid/checkState"]
end
DB[("PostgreSQL")]
FS[("blob store<br/>file system / S3")]
FE --> API
API --> DOC & CAT & SCOPE & LC & SEARCH & BAK & NOTIF & IDENT & AUD & STOR
DOC --> SCOPE & CAT & LC & SEARCH & STOR & AUD
CAT --> SCOPE & IDENT & AUD
BAK --> SCOPE & STOR & AUD
SEARCH --> SCOPE & STOR & AUD
NOTIF --> SCOPE & LC & AUD
IDENT --> AUD
AUD --> SCOPE
DOC -. publishes events .-> NOTIF
DOC & CAT & LC & SCOPE & IDENT & SEARCH & BAK & NOTIF & AUD & API --> PERS
DOC & CAT & SCOPE & SEARCH & BAK & NOTIF & AUD & API --> COMMON
PERS --> DB
STOR --> FS
Diagram currency — re-derived 2026-08-22, with the ADR-0120 shared-kernel split. Every solid edge above is a real compile-time dependency, derived by expanding the
import ch.lqms.*statements of every module'ssrc/main— not an intention sketch. The solid set is exactly the 48 module→module import edges the source exercises: no edge is asserted that the code does not have, and none is omitted. It is no longer a claim anybody has to re-check by hand —ModuleDiagramTest(backend/architecture-tests) parses this very Mermaid block on every build and diffs it against the imports in both directions. The single dotted edge (document ⇢ notification) is deliberately not a compile edge — it is the ADR-0034 runtime event flow:documentpublishes through thelifecyclepublisher,notificationsubscribes to thelifecycleevent types, andapiwires the subscription at startup. Emitting modules never reference consumers.The 2026-08-22 delta (ADR-0120). The eight
… → commonedges are new, and they are the whole change: extracting the shared domain-exception vocabulary out ofscopeadded edges and removed none, because every module that reached intoscopefor aNotFoundExceptionalso reaches into it forAuthorizedRequestContextorEffectiveAuthzRepository. What the split bought is not a thinner graph but an honest one — a… → scopeedge now means authorization, and the count behind it fell accordingly (api → scope22 importing files → 5,audit → scope3 → 1).commonitself imports nothing: it is a leaf, so it can never participate in a cycle.Two Gradle-declared dependencies are deliberately not drawn because no source uses them:
lifecycle → {scope, catalog, audit}andbackup → document(KDoc links only).lifecycle's only real outbound edge ispersistence— the visual proof of the contracts-only split described in the note below.test-supportand the test-onlyarchitecture-testsmodule are omitted (no production edges).Pending delta of 2026-08-18 — APPLIED 2026-08-22. The paragraph that stood here predicted that
PooledConnections.kt(then uncommitted) would make theapi → persistenceedge real and invalidate thepersistencerow's "no hand-written Kotlin". It landed ind646b6da— an ancestor of the very commit that corrected this diagram — and the delta sat unapplied untilModuleDiagramTest(the durable §5 checker, built 2026-08-22 from the Fable re-check's finding 1) failed on it during its first run. Both statements are now corrected on this page. The ADR-0036 question the old paragraph flagged was ruled by Nicolas on 2026-08-21 (fix-log decision 6):PooledConnectionsSTAYS inpersistence— it holds no queries, native or jOOQ, only JDBC connection lifecycle — whileRlsScopeContextdeliberately stays inscope, because those 76 lines are the tenant-separation rule expressed as code, not plumbing. From this date the diagram is enforced against the source graph by the checker, so the next drift fails the build instead of waiting for a reader.
| Module | Responsibility | Notes |
|---|---|---|
scope |
Mandators, scopes, scope codes, capability flags, tenancy/RLS integration (RlsScopeContext, AuthorizedRequestContext) and the effective-authorization resolution (EffectiveAuthzRepository) — authorization and tenancy only since ADR-0120 moved the shared domain-exception set out to common. RlsScopeContext and TransactionPostCommit deliberately stay here (fix-log decision 6: those 76 lines are the separation rule as code, not plumbing) |
Core of separation (ADR-0002, ADR-0026); depends only on common and persistence |
identity |
User identities (stable OIDC subject), invite/JIT provisioning, activate/deactivate | ADR-0011/0013; global (no RLS); extends the ADR-0005 module set |
catalog |
Role/document-type catalogs, per-scope activations & assignments, directory lookups, retention rules & legal holds, user/role administration | ADR-0004, ADR-0016 |
document |
Documents, versions, document IDs, provenance, derive & instantiate; the tree-record content model (PM_DOC tree + derived projection, server-side PmDocText extraction); and the lifecycle state machine (LifecycleService, incl. the DIRECT single-person directRelease transition, state-conditional transitions in VersionRepository, ReleaseGate), review comments (CommentService), PDF export (PdfExportService/Apache FOP), attachments, acknowledgements, questionnaires, periodic review, coverage rules, disposal, the training plan/compliance read models, the derived traceability read models (RequirementChainService, RiskTraceabilityService, the TraceCoverage edge-type-set engine, TraceabilityReportService, and — since Stage C, ADR-0084 — the validation & regulatory axes' ValidationMatrixService + GsprConformityService plus the per-scope design-kind vocabulary DesignKindService), recurring obligations (RecurringObligationService; the next-due rule itself is the shared SQL function lqms_obligation_next_due, V070 — one encoding for the oversight derivation and both sweep passes, a fold over the completion history), and orphan-blob GC (BlobGcSweepService/BlobGcRepository). Since 2026-07 DocumentService is split: the metadata cluster lives in DocumentMetadataService, and one WorkflowActionEvaluator seam computes both the getVersion action reflection and the transition enforcement (§8.10) |
ADR-0001/0002, ADR-0003, ADR-0057/0058, ADR-0074, ADR-0084, ADR-0085/0086/0087/0088, ADR-0089/0091/0092/0094 |
lifecycle |
Contracts only, no state machine and no comments: the domain-event types + DomainEventPublisher (ADR-0034), the append-only approval repository, and the review-policy + per-document workflow-role binding repositories/resolvers that document's state machine consumes — the resolved policy now carries the STANDARD/DIRECT release_flow (ADR-0091), an axis orthogonal to the AUTOMATIC/EXPLICIT release_mode |
ADR-0003, ADR-0030, ADR-0034, ADR-0064, ADR-0091 |
storage |
Content-addressed blob store behind an interface (ContentStore + FS implementation); the interface also enumerates a scope's stored blobs (list(scope) / listScopes()) so the orphan-blob GC can compute stored-minus-referenced (ADR-0092) |
Extraction-ready (FS → S3); no module dependencies |
search |
Full-text indexing, retrieval, and disposal-driven purge | Designated extraction candidate |
backup |
The in-app system-maintenance surface (ADR-0116): the EXPORT_BACKUP-gated backup act, executed asynchronously one-at-a-time as an image-pinned pg_dump -Fc plus a blob archive into the app's own volume (BackupService, PgDumpExecutor/ProcessPgDumpExecutor), the backup evidence read model and NO_VERIFIED_BACKUP finding (BackupRecordRepository, V102), and scheduled-job heartbeats (SystemJobHeartbeatService) |
ADR-0116, ADR-0060, ADR-0018; shipped v0.9.12 (~1,027 main-source lines; REQ-BAK-006/-007 verified, REQ-ADM-012). Restore stays deploy-level by decision — the deploy/ scripts remain the restore and rehearsal path (ADR-0060); per-scope export (REQ-BAK-005) is still future |
audit |
Append-only audit events; the shared AuditChanges before/after builder |
|
notification |
Tasks & inbox; pluggable delivery channels; acknowledgement/training fan-out — a consumer of the lifecycle domain events (compile-time dependency on lifecycle for the event types + LifecycleTaskHandler subscriber — the ADR-0005-legal edge blessed in ADR-0034) |
ADR-0019/0034/0035; event-driven |
api |
REST resources + request/response DTOs, exception mappers, security headers, CDI producers, SSE push/presence, bootstrap & deployment checks, the scheduled sweeps & GC (four advisory-lock-elected schedulers — task-sweep + notification delivery, certification-renewal, recurring-obligation, and orphan-blob GC; §8.4), and SPA hosting (Quarkus Quinoa); future MCP extension point | Only externally reachable module; depends on all other backend modules |
persistence |
Technical module: Flyway migrations + jOOQ classes generated from the migrated schema, plus exactly one hand-written file — PooledConnections.kt, the physical connection-eviction helper (no queries; JDBC lifecycle only, decision 6 of the 2026-08-18 fix log) |
ADR-0008; exposes the jOOQ API to domain modules |
common |
The shared kernel (ADR-0120): the cross-module domain-exception vocabulary in one file — PermissionDeniedException, NotFoundException, GlobalScopeMissingException, InvalidStateException, DomainValidationException — plus the requireValid/checkState guards that raise the last two. Nothing else lives here |
Depends on nothing (not even persistence), so any module may depend on it without a cycle; the api layer maps each type to its HTTP status in ONE place (ScopeExceptionMappers) |
test-support |
Test fixture module (LqmsTestDb): one PostgreSQL container + one migration pass per JVM, per-boot pristine clones via CREATE DATABASE … TEMPLATE … |
Main source set, consumed via testImplementation; closes the parked F33 item (2026-07-16) |
Dependency rules are build-enforced (Gradle module graph) and verified by architecture tests; see ADR-0005. The authoritative rule set lives in the build configuration; the diagram above shows the edges the source actually exercises, which is the tighter set (see the currency note).
Note (2026-07-16, arc42-currency sweep): ADR-0005's initial decomposition placed the state machine, review policies, approvals, and comments in
lifecycle. As the code grew, the transaction-heavy state machine and comments settled indocument(they operate directly on versions and content), whilelifecycledistilled down to the contracts those flows depend on — event types, approvals, and policy/workflow-role bindings — keeping thedocument → lifecycleedge one-directional and cycle-free. This section is the authoritative current split (ADR-0005 §Consequences delegates the living decomposition to arc42 §5).Open code-comment defect, re-verified 2026-08-18 — still present. The KDoc on
backend/lifecycle/src/main/kotlin/ch/lqms/lifecycle/LifecycleModule.ktstill describes the module as "life-cycle state machine, review policies, approvals, review comments", repeating the superseded ADR-0005 ownership claim: the state machine and comments live indocument. First flagged here on 2026-07-16 and open for 33 days; the fix is a Kotlin edit and is therefore out of scope for a docs sweep. Registered as D6 in review-2026-08-18-architecture.md. Until it lands, this section — not the KDoc — is authoritative on thedocument/lifecyclesplit.
A test-only module backend/architecture-tests (not a production building block)
enforces, via Konsist, the boundary rules the compile graph cannot express:
production code resides under ch.lqms; only api declares JAX-RS resources;
only storage performs file-system access; no module depends on api. The check
always re-runs (Konsist reads sources off disk, invisible to Gradle's up-to-date
tracking).
5.2 Content record architecture (tree record + projection)¶
The document content model is format PM_DOC only (ADR-0057,
ADR-0058; see §8.1 for the full concept). The
authoritative content is a ProseMirror document tree; markdown is a derived projection
computed by ONE shared projection function. That function is JavaScript and lives with the
frontend, split browser/Node for bundle hygiene:
webui/fidelity/projection.mjs— browser-safe:importMarkdown/projectMarkdown/canonicalizeTree/extractText; the editor and reader import it.webui/fidelity/projection-node.mjs— jsdom-backedcreateProjection, harness/seed only (kept out of the app bundle); the demo seed wraps it viadeploy/md-to-tree.mjs, andwebui/fidelity/verify-stored-trees.mjsis the offline projection-integrity audit (ADR-0057 §4).
The backend document module owns the format-versioned storage (V039/V040), validates the
submitted tree, and computes the search extraction server-side (PmDocText, the Kotlin port of
extractText) — it never depends on the client projection for search (ADR-0057 §3). The reading
view renders PM_DOC from the tree via a headless DOMSerializer behind the unchanged policy
layer (content-policy.ts); the markdown-it path is retained only for transient
instantiation-born MARKDOWN drafts (ADR-0058).
5.3 Level 2¶
TBD — and the premise of the original placeholder has expired. This section said the
Level-2 views would follow "once the modules take shape during implementation"; they have long
since taken shape. As of 2026-08-18 the backend is 31.6k lines in document alone (98 files),
10.4k in api (81 files) and 7.2k in catalog (19 files), while the other eight production
modules are each under 1.1k lines.
So this is a real documentation gap, not a not-yet: the only module that genuinely needs a
whitebox view is document — 57% of the backend, ~20 responsibility clusters in one flat
package (the §5.1 table row is currently doing that job in prose, and is straining). The
remaining modules are small enough that the §5.1 table plus their ADRs suffice, so a full
Level-2 sweep would be make-work. Registered as D6/S1 in
review-2026-08-18-architecture.md; the
decomposition of document is a design question (how it should be split), not a
transcription job, so it is deliberately not answered here by a docs sweep.