Contributing to distributed-lock-lab
Thank you for considering a contribution. This document tells you how to find work, how to claim it, what “finished” means here, and the handful of rules that are not negotiable.
Read CODE_OF_CONDUCT.md before participating. If you are an AI agent, or you
are driving one, AGENTS.md carries additional obligations and you must read it too.
1. What makes this project unusual
The design is finished before the code. There are 63 implementation task specifications in
tasks/, each naming its preconditions, its deliverable files, its acceptance criteria, and
the exact commands that verify it. Five contract documents pin every identifier the
codebase is allowed to use.
This exists because of a specific failure mode. Nine modules and two lock backends compile against one
vocabulary, and no single contributor sees more than a slice of it. A plausible synonym —
fencing_token where the contract pins fence — compiles fine, passes its own module’s tests, and
then fails at every integration point three milestones later, by which time twenty queries are written
against the wrong name. The contracts are how parallel work converges instead of diverging.
The practical consequence for you: most design questions already have written answers. Before
proposing an alternative, check docs/adr/ — fourteen decisions are recorded there with
their context and consequences, and reopening one costs more than it usually returns.
2. Ways to contribute
| Contribution | Where it starts |
|---|---|
| Implement a task | The task board; open or claim the matching issue |
| Fix a bug | Bug report |
| Improve documentation | Docs issue; small fixes may go straight to a PR |
| Challenge a contract | Contract change — never a silent PR |
| Challenge a decision | A new ADR proposing to supersede the old one; see ADR-000 |
| Propose new scope | Open a discussion first. Check non-goals — the answer may already be “deliberately not” |
We do not accept: contributions that add real company, customer, partner, or product names; contributions containing real payment data, personal data, or credentials; and benchmark numbers without the command, environment and date that produced them.
3. Finding work you can actually start
Tasks are not all available at once. Each specification lists preconditions, and a task whose preconditions are unmet cannot be implemented — the files it edits do not exist yet.
Three ways to find something ready:
- The
status: readylabel on issues. A maintainer sets it when every precondition is merged. Start here. docs/12-parallelization-map.md— the dependency graph, showing which tasks can proceed simultaneously and which form the critical path. Several lanes run independently; the documentation and Terraform lanes in particular need no Java at all.good first issue— tasks scoped to be completable without holding the whole architecture in your head.T-002,T-023andT-068are deliberately shaped this way.
The board’s ledger is the durable record of what is done; GitHub issues are the working surface. If the two disagree, the ledger is authoritative and the discrepancy is a bug worth reporting.
There is also a project board with every task on it.
Group it by Milestone for the delivery plan, or filter label:status:ready for work with no
unmerged blockers.
4. Claiming a task
To avoid two people building the same thing:
- Comment
/claimon the issue (or say so in plain words). A maintainer assigns it to you. - One task at a time per contributor, until you have landed one. This is not a trust issue — it keeps the board honest about what is actually in flight.
- A claim lapses after 14 days without a linked draft PR or a progress comment. No explanation is owed and no fault is implied; life happens. Say so and reclaim it whenever you like.
- If a task turns out to be bigger than its specification, say so in the issue rather than silently expanding scope. Splitting is normal — see §9.
Unclaimed tasks are fair game. Claimed ones are not; open a second issue if you want to work adjacent to someone.
5. Setting up
Until milestone M0 lands there is no build to run — creating it is M0. From M0 onward:
git clone https://github.com/rednavis/distributed-lock-lab.git
cd distributed-lock-lab
./gradlew build # all modules, full test suite
./gradlew spotlessApply # format before committing
docker compose up # local stack, no cloud account required
Requirements: JDK 25 (the Gradle toolchain will fetch one if your default differs), Docker for Testcontainers, and roughly 8 GB of RAM for the full local stack. No Google Cloud account is needed for anything up to and including the fencing experiment — milestones M0 through M4 are entirely local, by design (NFR-15).
Milestones M5 and M6 provision real infrastructure that accrues real charges. Do not run them
casually; read docs/05-infrastructure.md first, and use a
dedicated Google Cloud project you are willing to delete.
6. The working agreement
Branches and commits
GitHub Flow. master is the only long-lived branch. All work happens on a short-lived feature
branch off master and is squash-merged back through a reviewed pull request. Nothing is ever pushed
directly to master — branch protection enforces this.
Name the branch after its task:
<type>/<task-id>-<slug> feat/T-011-pg-tryacquire
docs/T-068-runbook
fix/token-regression-on-restore
Commits follow Conventional Commits and must be signed off under the Developer Certificate of Origin:
git commit -s -m "feat(lock-server): implement PostgresLockStore.tryAcquire
Implements the single atomic INSERT .. ON CONFLICT pinned in C1 #ct1-acquire,
including the nextval() in the DO UPDATE branch so takeover mints a strictly
greater token.
Closes #47"
git commit -s appends the Signed-off-by trailer, which is your statement that you wrote the patch
or otherwise have the right to submit it under Apache-2.0. There is no separate CLA. A DCO check runs
on every PR.
If your contribution was generated with AI assistance, add a Co-Authored-By trailer naming the tool.
This is a disclosure requirement, not a disqualification — see AGENTS.md.
Pull requests
One task, one pull request. Open it as a draft early if you want feedback in progress; mark it ready when the definition of done in §7 holds.
The PR template asks you to paste the output of the verification commands from your task specification’s §6. Please actually paste it. “Tests pass” is not evidence; the terminal output is.
Review
- Every PR needs one maintainer approval; PRs touching a contract, an ADR, or
lock-apineed two (GOVERNANCE.md). CODEOWNERSroutes review requests automatically.- Reviewers are asked to respond within five working days. If yours has gone quiet, a polite nudge on the PR is welcome and will not annoy anyone.
- Review comments are about the code, never the author. Reviewers: see the Code of Conduct, which binds you as much as it binds contributors.
7. Definition of done
A task is done when all six hold. Five out of six is not done.
| # | Gate | How you know |
|---|---|---|
| 1 | It builds | ./gradlew build succeeds from a clean checkout |
| 2 | Style passes | ./gradlew spotlessCheck is green. Do not reformat files your task does not touch — it buries the real change in noise |
| 3 | The specified tests pass | Exactly the tests your specification names, plus everything that was already green. A previously green test that is now red is a failure, not a flake |
| 4 | The observability actually emits | Scrape it and look. If your task adds a metric, curl /actuator/prometheus and confirm the name and tags match C4 §4.2. If it adds a log event, trigger it and read the JSON. “The code calls the meter” is not evidence |
| 5 | The ledger is updated | Your PR updates the task’s row in tasks/README.md with status, the command that proves it, and anything the next contributor should know |
| 6 | Deviations are recorded | Any difference between the specification and what you built is written down — see §10 |
Gate 4 is the one that gets skipped, and it is the one that fails silently. The named-port trap in C4 §4.9 is the canonical example: everything looks correct, the code is right, and nothing is ever scraped. Verify by observation, in the order that contract gives.
8. Rules that are not negotiable
Each of these has cost real projects real money.
| Rule | Why |
|---|---|
| Never disable a fencing check to make something work. | payment.fencing.enabled and rail.proxy.fencing.enabled exist only to demonstrate corruption inside a named experiment. Flipping either to get past a failure destroys the one thing this project is for, and it destroys it quietly — the build goes green and the safety property is gone. If fencing rejects your write, the write is wrong. lock.fenced.out and rail.duplicate.attempted are must-be-zero counters (C4 §4.4) |
| Never weaken a test to make it pass. | Loosening an assertion, widening a tolerance, adding @Disabled, catching the exception the test exists to observe — all of these convert a real defect into a green build. The test is the requirement. If the test is wrong, that is a contract question: stop and open an issue |
| Never add a dependency outside the version catalog. | One catalog is the only place versions appear (C5 §5.3). A stray coordinate in a build.gradle.kts is a supply-chain and reproducibility hole. lock-api has zero third-party dependencies and that is a contract, not a preference |
| Never deviate from a contract silently. | If a contract looks wrong, is silent, or contradicts a task specification, open a contract change issue. Do not invent an alternative. Precedence rules are in 04 §4.4 |
| No real data, ever. | Synthetic fixtures only. No secrets in source, no secrets in Terraform state, no real company names |
| Never publish an unreproducible number. | Every benchmark figure carries the command, the environment and the date that produced it. An unreproducible number here is worse than no number |
9. When a task turns out to be bigger than it looked
Split it. Do not sprawl.
- Bring your branch to a buildable state. A broken build is the worst possible handoff.
- Open a follow-up issue for the remainder, written clearly enough that somebody else could pick it up without reconstructing your reasoning.
- Take the next reserved id in that milestone’s gap for the remainder —
T-009,T-018,T-019,T-028,T-029,T-035…T-039,T-048,T-049exist precisely for this. Do not appendT-017b,T-017c. - Update the ledger row to
split, pointing at the new id. - Land what you have.
If you are blocked — a contract conflict, a missing prerequisite, a genuine ambiguity — say so in the issue, state precisely what decision is needed and from whom, and stop. Do not unblock yourself by guessing. A guess that turns out wrong costs more than a stopped task, because everything downstream builds on it.
10. Recording a deviation
Any difference between what the specification said and what you built is a deviation, however small and however justified. Record it in the PR description and in the ledger row:
| Field | Content |
|---|---|
| What | The specification said X; the repository now has Y |
| Why | The specific reason — not “cleaner” or “more idiomatic” |
| Blast radius | Which later tasks, contracts or documents are now inconsistent |
| Contract impact | None, or: which contract needs an amendment |
An unrecorded deviation is indistinguishable from a bug for everyone who comes after you, and it is found by the person who trusted the document — which is the most expensive way to find it.
11. Decisions that are settled
These are recorded in docs/adr/ and are not reopened without a superseding ADR that
engages with the original reasoning:
Java 25 · Gradle Kotlin DSL with a version catalog (never Maven) · Spring Boot 4.1 · PostgreSQL 16 ·
etcd 3.6 · Flyway · Micrometer and Actuator · OpenTelemetry · Terraform · Testcontainers · JUnit 5 ·
Spotless with google-java-format · Lombok limited to @RequiredArgsConstructor and @Slf4j · GitHub
Actions.
Also settled: two databases on separate instances (one instance for both would fail the lock and
the resource together and destroy the failover experiment); both backends first-class, with
PostgreSQL taught first because its state is inspectable with SELECT, and etcd recommended for
production because its tokens are monotonic by construction rather than by procedure; GKE Autopilot
accepted, with its extra leader elections treated as a budgeted expense against the error budget.
Proposing a change to any of these is legitimate. Doing it inside an unrelated PR is not.
12. Getting help
Stuck, unsure whether something is a bug, or want to sanity-check an approach before writing code —
SUPPORT.md lists the channels. Asking early is cheaper than a rewrite, and no question
about this codebase is too basic to ask.