T-004 — lock-api: the contract types
Picking this up? Read
CONTRIBUTING.mdfirst, then claim the matching issue and work on a branch. Finished means all six definition-of-done gates, not five. If anything below disagrees with a contract, the contract wins — open a contract change issue instead of implementing either version. Update this task’s row in the ledger in the same pull request.
Milestone: M0 Foundations · Estimate: 30 min
Preconditions — T-001 (module tree, dev/lock/api source root exists), T-002 (catalog),
T-003 (lock-api builds under dlock.library-conventions with Java 25, -Werror, Spotless, and a
compile classpath proven empty). lock-api/src/main/java/dev/lock/api/ currently holds only .gitkeep.
Goal — Author every type in C2 §2.2–2.5 as compilable signatures with full Javadoc and no method
bodies beyond what the language forces, while keeping lock-api’s dependency count at zero.
1. Why this task exists
lock-api is the one artifact both lock backends and all five services share; it is what makes “the
Postgres and etcd backends are observably identical” a type-checked statement instead of a hope
(#ct2-spi). Writing it before any implementation means the pg backend cannot quietly shape the SPI
around a DataSource. The zero-dependency rule is the load-bearing constraint here — everything else in
this task is transcription.
2. Contracts to obey
| What | Pinned by |
|---|---|
| Zero third-party dependencies, JDK-only types, no annotations at all | docs/contracts/C2-java-api.md#ct2-zero-dep |
LockService — six methods, exact signatures and return semantics |
docs/contracts/C2-java-api.md#ct2-lockservice |
LockHandle, LockInfo, LockOutcome — exact components and order |
docs/contracts/C2-java-api.md#ct2-records |
| The four exceptions and their retry verdicts | docs/contracts/C2-java-api.md#ct2-exceptions |
LockStore and SessionRegistry in package dev.lock.api |
docs/contracts/C2-java-api.md#ct2-spi |
| Token propagation: parameter or handle, never a thread-local | docs/contracts/C2-java-api.md#ct2-propagation |
| Thread-safety and nullability rules to document on each type | docs/contracts/C2-java-api.md#ct2-threading |
Package name dev.lock.api; module owns nothing else |
docs/contracts/C5-config-build-and-naming.md#ct5-modules |
Precedence: if this spec and a contract disagree, the contract wins — stop and report, quoting
both (docs/04-contracts.md#c-precedence). C2 shows the signatures verbatim; copy from C2, not from
this spec’s prose.
3. Deliverables
Path (under lock-api/src/main/java/dev/lock/api/) |
What |
|---|---|
LockService.java |
The six-method caller-facing interface |
LockHandle.java, LockInfo.java, LockOutcome.java |
The three records |
LockException.java |
Common supertype (see §4 for the “if C2 is silent” rule) |
LockLostException.java, FencedOutException.java, ContentionException.java, NotLeaderException.java |
The four failures |
LockStore.java, SessionRegistry.java |
The backend SPI |
package-info.java |
Package Javadoc: what this module is, the zero-dependency rule, the propagation rule |
lock-api/src/test/java/dev/lock/api/ApiContractTest.java |
Compile-level guard tests (see §4) |
lock-api/build.gradle.kts (modify) |
Must still declare no dependencies beyond the test bundle |
4. Specification
Transcribe, do not design. Method names, parameter names, parameter order, and return types come
from C2 §2.2 and §2.5 exactly. LockService has acquire, tryAcquire, renew, release, inspect,
forceRevoke. LockStore has tryInsert, extend, deleteIfOwner, read, revoke, reapExpired;
SessionRegistry has the session lifecycle including heartbeat and closeSession. Record components
appear in the contract’s order (LockHandle: key, ownerId, sessionId, fencingToken,
clientDeadlineNanos, serverExpiry).
Javadoc is the deliverable, not decoration. Carry over C2’s @return, @throws and @implNote
text, and make these four semantics unmissable in prose:
| Semantic | Must appear on |
|---|---|
Optional.empty() from acquire/tryAcquire = “not mine, do not proceed” — an expected outcome, never retried in a tight loop |
LockService.acquire, tryAcquire |
Optional.empty() from renew/extend = LOST NOW, terminal, must not be treated as transient |
LockService.renew, LockStore.extend |
inspect/read are advisory and never usable for a correctness decision |
LockService.inspect, LockStore.read |
clientDeadlineNanos is authoritative (monotonic); serverExpiry is informational wall-clock |
LockHandle |
Records must validate in a compact canonical constructor: non-blank key/ownerId/sessionId,
fencingToken > 0. That is the one place a body is allowed; keep it to argument checks throwing
IllegalArgumentException/NullPointerException — JDK types only.
Exception hierarchy. Each of the four classes documents, in its class Javadoc, the trigger, the
retry verdict from #ct2-exceptions (ContentionException and NotLeaderException retryable;
LockLostException and FencedOutException terminal), and the operator action. FencedOutException
carries the key, the presented token and the stored token as fields, because #ct2-exceptions requires
those three values in the fenced_out log event. If C2 does not name a common supertype or specify
checked vs unchecked, stop and request a contract amendment (docs/04-contracts.md#c-precedence,
§4.5) rather than choosing — this decision propagates into every catch in the project.
Tests in ApiContractTest assert, in prose terms: a record rejects a blank key and a zero or
negative fencing token; LockHandle equality is component-wise; the retryable exceptions are
distinguishable from the terminal ones by type alone (no boolean flag, no string matching); and
FencedOutException exposes both token values. Also assert reflectively that no type in the package
declares a method or field whose type name starts with a non-java./dev.lock. package — a cheap
in-module echo of the zero-dependency rule.
5. Acceptance criteria
./gradlew :lock-api:buildisBUILD SUCCESSFULunder-Werror../gradlew :lock-api:dependencies --configuration compileClasspathreports no dependencies.grep -rn 'import ' lock-api/src/main/javayields onlyjava.*imports — noorg.,com.,jakarta.,lombok..- All eleven main-source files listed in §3 exist; every public type and public member has Javadoc.
- No
ThreadLocalanywhere in the module (#ct2-propagation). ./gradlew :lock-api:testpasses; the tests named in §4 all exist../gradlew spotlessCheckpasses with the license header present in every file.- Every method signature matches C2 character-for-character in name, arity and types.
6. Verification
./gradlew :lock-api:build :lock-api:test spotlessCheck
./gradlew :lock-api:dependencies --configuration compileClasspath
grep -rn 'import ' lock-api/src/main/java | grep -v 'import java\.' # expect: no output
grep -rn 'ThreadLocal\|@Slf4j\|Lombok' lock-api/src/ # expect: no output
Expected: green build and tests; “No dependencies” for the compile classpath; both greps silent.
7. Out of scope
Any implementation of LockService, LockStore or SessionRegistry — the pg backend is M1
(T-010..017), etcd is M3 (T-030..034). The client SDK surface of #ct2-sdk
(LockClientSession, checkStillHeld, the conservative deadline) is lock-client in M4. HTTP DTOs,
error codes and headers are C3 and belong to the lock-server web task. No SQL, no config keys, no
metrics — even though #ct2-exceptions mentions lock.fenced.out.
8. Hazards
lock-apiis a wire-compatibility surface with six dependents, so this change needs two maintainer approvals (GOVERNANCE.md§2).- The single most damaging mistake in this task is one
@Nullable,@NonNullor@Slf4j— an annotation is a dependency, and#ct2-zero-depwarns that once an annotation leaks in the SPI becomes unimplementable outside that runtime. Express nullability in Javadoc andOptionalonly. - Conflating “empty because someone else holds it” with “empty because you lost it” is called out in
#ct2-spias the most common bug in home-grown lock clients. The Javadoc you write here is the artifact that prevents it downstream — do not compress it. - Adding a convenience overload (e.g.
acquire(String,String)) puts a name in the vocabulary that no contract pins; the module surface is closed.
9. On completion
Mark T-004 done in tasks/README.md. If you had to stop on the checked/unchecked or supertype question,
open a contract change issue with the proposed §4.5
amendment text, mark the ledger row Blocked, and stop (CONTRIBUTING.md §9).