ADR-0036: Repository layer — services carry rules, repositories carry jOOQ¶
- Status: accepted
- Date: 2026-07-02
- Deciders: Nicolas Burri
- Refines: ADR-0005 (module layering), ADR-0008 (jOOQ as the SQL layer), ADR-0026/ADR-0027 (the RLS transaction discipline this must preserve)
Context¶
Domain services (DocumentService, LifecycleService, TaskService, …) interleave business
rules (state machine, four-eyes, policy thresholds) with jOOQ queries in the same methods, and a
few service-layer classes with raw jOOQ live in the api module (SystemBootstrap,
AuthenticatedUserService). The JAX-RS resources themselves are clean. Nicolas asked for a
layered approach with a clear DB abstraction.
A hard constraint: the RLS discipline is load-bearing. Every content query must run inside
AuthorizedRequestContext.runAs's transaction (transaction-local lqms_app role + scope GUCs,
ADR-0026). Any DB layer must therefore receive the transaction's DSLContext — it must never
own connections or open transactions.
Decision¶
Repository classes per module. Options considered: (a) repositories — chosen; (b) keep jOOQ in services, treating jOOQ's typed DSL as the abstraction (rejected: leaves rules and SQL tangled, which is what prompted this); © ports & adapters with repository interfaces + jOOQ adapters (rejected: ADR-0001 commits to PostgreSQL as the single source of truth, so swapability buys nothing for the added indirection).
Rules:
- Repositories own ALL persistence access — generated-table imports
(
ch.lqms.persistence.jooq.*), query construction, row↔domain mapping, and the raw-SQL calls to thelqms_*SECURITY DEFINER functions. One repository per domain concept (table cluster), living in the module that owns the concept — e.g.AuditEventRepository(audit),ApprovalRepository(lifecycle),TaskRepository(notification). - Repositories are plain, stateless classes whose methods take the transaction's
DSLContextas the first parameter. No connection handling, no transactions, no authorization — the service'srunAsblock remains the only transaction/RLS boundary. - Services carry the rules — authorization checks, state-machine guards, policy evaluation, event publication, orchestration — and no persistence-model imports.
- The
apimodule contains no persistence access at all: composition-root classes that need the DB (SystemBootstrap,AuthenticatedUserService) orchestrate repositories from the owning modules. - Enforced by an architecture test (Konsist): outside the
persistencemodule, test code, and the two RLS-plumbing classes inscope(RlsScopeContext,AuthorizedRequestContext), only files named*Repositorymay importch.lqms.persistence.jooq. The rule lands with the final migration commit (intermediate commits stay CI-green).
Shared query homes: effective-roles/permissions resolution (lqms_effective_*) →
EffectiveAuthzRepository in scope (every module already depends on it); approvals →
ApprovalRepository in lifecycle (used by both document and notification).
Consequences¶
- Business logic becomes readable and unit-reviewable without SQL noise; queries become reusable and individually testable; the review-round class of bugs (a threshold rule diverging between two inline queries) gets a single home per query.
- Mechanical risk during migration is bounded by the existing green suite (behavior frozen; the refactor changes no SQL semantics).
- Repositories are deliberately not interfaces — introduce one only when a second implementation actually exists (e.g. a test fake proves valuable).
- The
lifecycle/documentmodule split gets slightly cleaner (approval queries move tolifecycle), but module boundaries themselves are unchanged (ADR-0005 stands).