04 — The contract set (index)

4.1 What this file is

This is the index to the authoritative contract set under docs/contracts/. The contracts pin every name a downstream implementation session is allowed to use: tables and columns, SQL statements, Java signatures, HTTP paths and error codes, headers, metrics, log events, config keys, and cloud resource names. Nothing here is new — this file only routes.

Any change to a contract is a BREAKING change. A renamed column, a widened exception, a new error code, a changed retry-safety verdict: each invalidates the task specs that were written against it. The procedure is: amend the contract → add a §4.5 row → revisit every dependent task spec → then implement. Why: the whole point of freezing names before code is that eight modules and two lock backends compile against the same vocabulary. Failure mode: a silent contract edit leaves half the tasks written against the old name, and the mismatch surfaces as a green build with a broken fence.

4.2 Routing table

ID Path Pins Anchors defined
C1 docs/contracts/C1-database-schemas.md lockdb and paydb DDL, the acquire/renew/release statements, the fenced UPDATE, fencing_token_seq, Flyway naming #ct1-scope #ct1-lockdb #ct1-acquire #ct1-renew #ct1-paydb #ct1-fenced #ct1-seq #ct1-migrations
C2 docs/contracts/C2-java-api.md lock-api zero-dependency rule, LockService, LockHandle/LockInfo/LockOutcome, exception hierarchy, LockStore/SessionRegistry SPI, client SDK, token propagation, threading/nullability #ct2-zero-dep #ct2-lockservice #ct2-records #ct2-exceptions #ct2-spi #ct2-sdk #ct2-propagation #ct2-threading
C3 docs/contracts/C3-http-surfaces.md All four HTTP surfaces, error-code catalogue with retry-safety, the three custom headers, the ambiguous-outcome contract, per-hop timeouts and retry budgets #ct3-conventions #ct3-errors #ct3-lock #ct3-pay #ct3-railproxy #ct3-railstub #ct3-ambiguity #ct3-timeouts
C4 docs/contracts/C4-observability.md Metric names/types/tags, the cardinality budget, the two must-be-zero counters, structured log event schema and required fields, log-label promotion, log-based metrics, OpenTelemetry spans and attributes, the /actuator/prometheus scrape contract, health/readiness #ct4-scope #ct4-metrics #ct4-cardinality #ct4-zero #ct4-logs #ct4-promotion #ct4-lbm #ct4-traces #ct4-scrape #ct4-health #ct4-nonspec
C5 docs/contracts/C5-config-build-and-naming.md Config keys and defaults, the two kill switches, version catalog, module inventory, repo layout, naming conventions, environment variables, the deliberately-fixed list #ct5-config #ct5-killswitches #ct5-catalog #ct5-modules #ct5-layout #ct5-naming #ct5-env #ct5-fixed

Reading order for a fresh implementation session: C5 → C1 → C2 → C3 → C4. C5 tells you which modules exist and what they are called; C1 gives the state they operate on; C2 the in-process API; C3 the wire; C4 what every module must emit while doing it.

4.3 Where is X pinned?

Hunting for Go to Note
lock_entry, lock_session, lock_revocation columns C1 #ct1-lockdb lock_entry_expiry_idx, lock_entry_session_idx pinned here too
account, payout, ledger_entry, rail_submission, rail_high_water columns C1 #ct1-paydb includes payout.state and direction/outcome enum values
The acquire statement (insert-or-steal-if-expired) C1 #ct1-acquire renew/release in #ct1-renew
The fenced UPDATE … WHERE fence < :token C1 #ct1-fenced fence point (a); rail high-water fence point (c) in C3 #ct3-railproxy
Why one global sequence, not a per-row version C1 #ct1-seq etcd ModRevision equivalence argued here
Flyway migration filenames C1 #ct1-migrations two separate migration trees, one per database
Java type signatures (LockService, LockHandle, LockInfo, LockOutcome) C2 #ct2-lockservice, #ct2-records signatures only; no bodies anywhere in the contract set
Exception semantics — LockLostException, FencedOutException, ContentionException, NotLeaderException C2 #ct2-exceptions which are retryable, which are terminal
LockStore / SessionRegistry SPI (the pg vs etcd seam) C2 #ct2-spi both backends are first-class; the SPI is what keeps them so
How the token reaches a callee C2 #ct2-propagation explicit parameter or header — never a thread-local
Thread-safety and nullability rules C2 #ct2-threading  
HTTP paths (lock-server, payment-resource, rail-proxy, rail-stub) C3 #ct3-lock, #ct3-pay, #ct3-railproxy, #ct3-railstub  
Error codes and whether a retry is safe C3 #ct3-errors the single source for retry-safety verdicts
X-Fencing-Token, X-Owner-Id, X-Idempotency-Key C3 #ct3-conventions which surfaces require which
RAIL_AMBIGUOUS handling / non-idempotent rail C3 #ct3-ambiguity why a timeout may never be blindly retried
Per-hop timeouts, retry budgets, lease-vs-timeout arithmetic C3 #ct3-timeouts interacts with lock.client.safety-margin
Metric names, types and tags (lock.acquire, lock.fenced.out, payout.execute, rail.submission, …) C4 #ct4-metrics tag values are closed sets; #ct4-cardinality is the budget that forbids per-payout tags
The counters whose healthy value is exactly zero C4 #ct4-zero rail.duplicate.attempted and lock.fenced.out with payment.fencing.enabled=true
Log event names (lock_granted, fenced_out, rail_ambiguous, …) and required fields C4 #ct4-logs field promotion to labels in #ct4-promotion; log-based metrics in #ct4-lbm
Trace span names and required attributes C4 #ct4-traces lock.token on every span is the load-bearing attribute
The scrape contract — /actuator/prometheus, port name http-metrics, PodMonitoring C4 #ct4-scrape the named-port trap fails silently; verification order given there
Health and readiness endpoints C4 #ct4-health what C4 deliberately leaves to docs/06-observability-and-slo.md: #ct4-nonspec
Config keys and defaults (lock.backend, lock.default.ttl, lock.session.ttl, lock.client.safety-margin, rail.stub.*, both datasource URLs) C5 #ct5-config  
The kill switches payment.fencing.enabled, rail.proxy.fencing.enabled C5 #ct5-killswitches they exist to demonstrate corruption, not to be shipped off
Module list and Gradle project names C5 #ct5-modules repo layout in #ct5-layout
Dependency versions (Java 25, Spring Boot 4.1, PostgreSQL 16, etcd 3.6, Terraform 1.15) C5 #ct5-catalog version catalog is the only place versions appear
GCP and Kubernetes names (dlock-lab, europe-central2, dlock-gke, dlock-pg-lock, dlock-pg-pay, service accounts, secrets, dlock-etcd) C5 #ct5-naming, #ct5-env  
What is deliberately hard-coded C5 #ct5-fixed if it is on that list, adding a config key is a contract change

4.4 Precedence

If a task spec and a contract disagree, the contract wins and the task spec is wrong.

Situation Correct action
Task spec names a column/path/metric the contract does not Stop. Report the mismatch, quoting both. Do not implement either version.
Task spec omits a field the contract requires Implement the contract; note the gap in the report.
Contract is silent on something the task needs Stop. Silence is not permission to invent a name — request a contract amendment (§4.5).
Two contracts disagree with each other Stop. Report; do not pick a winner.

Why: an implementer who “fixes” the discrepancy locally produces a module that compiles alone and fails at every integration point. Failure mode: a plausible synonym — fencing_token in paydb instead of the pinned fence — costs a schema migration and every query written against it.

Order within the set: contracts are peers. No contract overrides another; conflicts escalate.

4.5 Contract change log

Date Contracts Change Task specs to revisit
2026-08-21 C1, C2, C3, C5 Initial contract set. Schemas (two-database rule, acquire/renew/release, fenced write, one global sequence), Java API/SPI and exception hierarchy, four HTTP surfaces with error catalogue and ambiguity handling, configuration/build/naming inventory. All — this is the baseline.
2026-08-21 C4 C4 (observability) added — the initial set is now complete (C1–C5). Pins the metric catalogue and its tag value sets, the cardinality budget, the two must-be-zero counters, the structured log event schema and promoted fields, log-based metrics, OpenTelemetry span names/attributes, the /actuator/prometheus + PodMonitoring scrape contract (container port name http-metrics), and health/readiness. Supersedes the earlier instruction to treat the project brief’s identifier list as authoritative. Any task spec that emits a metric, log event or span — telemetry names are now contract, not free choice.
2026-08-21 C1, C2, C3, C4, C5 Cross-contract reconciliation — five identifier conflicts closed. (a) LockOutcome is now only the forceRevoke result record (C2 §2.3); C1 §1.3’s acquire column is renamed “Acquire result” (GRANTED/CONTENDED) and C3 L4’s 200 body is renamed LockGrant — acquire returns Optional<LockHandle> in Java. (b) The release/renew predicate is pinned to lock_key + fencing_token only: owner_id dropped from C1’s release DELETE and session_id + owner_id from C1’s renew UPDATE; C2 SPI narrowed to extend(String key, long fencingToken, Duration ttl) and deleteIfOwner(String key, long fencingToken); C3 L6 now reads (lock_key, fencing_token). Justification: the token comes from one global sequence (etcd: the cluster-wide revision), so it is unique per grant and owner/session are redundant for safety — this replaced a three-way conflict in which the pinned SQL bound a :owner_id the pinned signature could not supply. (c) C3’s FENCED_OUT resourceType enum account/ledger/payoutaccount/ledger/rail, identical to the resource tag on lock.fenced.out, with C4 §4.2 named as the source of truth for that value set. (d) Revoke response field newFloorToken (C3 L8) → newTokenFloor (C2’s record spelling). (e) SDK held-check isStillHeld() (C5 §5.4) → checkStillHeld() (C2 §2.6 wins; the call can raise LockLostException, so an is-prefixed boolean name is wrong). Also: C4 §4.11 said log-router Terraform lives in infra/deploy/terraform/ (C5 §5.5; no infra/ tree exists). Verified already correct, unchanged: RAIL_AMBIGUOUS as the only ambiguous-outcome value (no UNKNOWN), no PgLockStore reference (the class is PostgresLockStore), Terraform state bucket gs://dlock-tfstate, and the three custom health indicators lockBackend/payDb/railStub with the fail-closed readiness rule. Any task spec touching acquire/release/renew signatures or SQL, the revoke API, the FENCED_OUT body, the client SDK held-check, or Terraform paths — the old spellings are now wrong, not merely alternative.
2026-08-21 C2, C5 checkStillHeld semantics converged — the rename above fixed the name but left the return type split. C2 §2.6 pinned boolean checkStillHeld(LockHandle handle) while the task specs and ADR-006 were written against a call that throws. Resolved as an architect decision in favour of the throw: C2 §2.6 now pins void checkStillHeld(LockHandle handle) throws LockLostException, C2 §2.4’s LockLostException row records that the method throws directly rather than reporting a verdict, and C5 §5.4’s module note quotes the signature. Rationale: the design intent is that the application aborts loudly the moment the local deadline passes, and a boolean invites if (checkStillHeld(h)) { … } with no else — a silently skipped payout instead of a loud abort. A convenience boolean alongside the throwing method is the same defect under a second name and is likewise forbidden. Prose that read “returns true” / “is false” is restated as “returns normally” / “throws” in ADR-006 D3 and 03-technical-architecture §§3.4, 3.5. T-040 (SDK session: contracts row, §4 deadline arithmetic, AC-4, hazards) and any later spec that gates a side effect on the held check — a boolean return is now wrong, not merely alternative.

Back to top

Apache-2.0. The domain modelled here is fictional; every number is a labelled assumption. Not production-ready — see SECURITY.md.