You should freeze optimistic concurrency before an agent writes any PATCH client, because agents default to last-write-wins and quietly drop concurrent edits. A small notes API makes the failure cheap to reproduce: two writers, one version column, and a contract test that rejects a missing or stale If-Match. Once that contract is red on purpose, you can let an agent fill in handlers without inventing timestamps, updated_at comparisons, or silent overwrites. This case study walks one project from background through tests, implementation, results, and the limits of the pattern.
Background: a notes resource two people can edit
The project is a single-resource notes service with GET /notes/:id and PATCH /notes/:id. You store id, body, and an integer version that only the server increments. The product rule is simple and non-negotiable: a write with a stale version must fail with 412 Precondition Failed, and a write with no version token must fail with 428 Precondition Required. Agents rarely infer that pair from a prose ticket, so they ship last-write-wins JSON and call the ticket done.
You are not building a CRDT, an append-only event log, or a view-counter. Those domains want different merge rules, and freezing If-Match on them would be the wrong contract. You are building a document that two humans edit in the same minute, which is the case where lost updates actually hurt. Keep the surface tiny so the frozen tests stay readable and the agent cannot wander into unrelated endpoints.
Goal for this case study
The goal is not a clever client. The goal is a red test suite that already encodes the version window, then an implementation that is not allowed to change the suite. You pin four behaviors before any model writes production code:
-
GETreturns the current body plus a strongETagderived fromversion. -
PATCHwith a matchingIf-Matchsucceeds, incrementsversion, and returns the newETag. -
PATCHwith a staleIf-Matchreturns412and leavesbodyunchanged. -
PATCHwithoutIf-Matchreturns428and does not create a default version of1.
If you skip step four, agents “helpfully” treat a missing header as “just write it,” which reintroduces lost updates for every retrying script. Freeze the 428 so the client is forced to GET first. That is the entire product decision; everything else is wiring.
The contract you freeze first
Save the suite under tests/test_note_if_match.py and run it against an empty app so every assertion fails for the right reason. Label this as a worked example you can copy, not a production dump from an unnamed company.
# tests/test_note_if_match.py
# Worked example: freeze optimistic concurrency before any PATCH client exists.
import json
from pathlib import Path
import pytest
from httpx import Client
BASE = "http://127.0.0.1:8088"
NOTE_ID = "note_42"
def etag_of(resp):
value = resp.headers.get("etag")
assert value, "GET/PATCH must send ETag"
assert value[0] == '"' and value[-1] == '"', "use a strong ETag, not W/"
return value
@pytest.fixture(scope="module")
def http():
with Client(base_url=BASE, timeout=2.0) as client:
yield client
def test_get_returns_strong_etag(http: Client):
resp = http.get(f"/notes/{NOTE_ID}")
assert resp.status_code == 200
body = resp.json()
assert body["id"] == NOTE_ID
assert "body" in body and "version" in body
assert etag_of(resp) == f'"{body["version"]}"'
def test_patch_requires_if_match(http: Client):
resp = http.patch(
f"/notes/{NOTE_ID}",
json={"body": "agent invented a write without a token"},
)
assert resp.status_code == 428
assert http.get(f"/notes/{NOTE_ID}").json()["body"] != (
"agent invented a write without a token"
)
def test_stale_if_match_returns_412_and_preserves_body(http: Client):
current = http.get(f"/notes/{NOTE_ID}")
original = current.json()["body"]
stale = '"0"'
resp = http.patch(
f"/notes/{NOTE_ID}",
headers={"If-Match": stale},
json={"body": "should not land"},
)
assert resp.status_code == 412
assert http.get(f"/notes/{NOTE_ID}").json()["body"] == original
def test_matching_if_match_bumps_version(http: Client):
current = http.get(f"/notes/{NOTE_ID}")
token = etag_of(current)
version = current.json()["version"]
resp = http.patch(
f"/notes/{NOTE_ID}",
headers={"If-Match": token},
json={"body": "second writer won the race fairly"},
)
assert resp.status_code == 200
assert resp.json()["version"] == version + 1
assert etag_of(resp) == f'"{version + 1}"'
def test_second_writer_with_old_token_loses(http: Client):
first = http.get(f"/notes/{NOTE_ID}")
token = etag_of(first)
ok = http.patch(
f"/notes/{NOTE_ID}",
headers={"If-Match": token},
json={"body": "writer A"},
)
assert ok.status_code == 200
lost = http.patch(
f"/notes/{NOTE_ID}",
headers={"If-Match": token},
json={"body": "writer B should 412"},
)
assert lost.status_code == 412
assert http.get(f"/notes/{NOTE_ID}").json()["body"] == "writer A"
Run the suite before any handler exists so you watch the intended failures, not a green suite that never executed the contract:
python -m pytest tests/test_note_if_match.py -q
# expected: connection errors or 404s, not a silent pass
What agents invent when the contract is missing
Give the same ticket to an unconstrained coding agent and you will usually get one of these substitutes. None of them preserve the two-writer rule you actually wanted.
- Compare
updated_attimestamps, which collide inside the same second and depend on clock skew. - Hash the body and treat equal hashes as “no write,” which hides concurrent edits that happened to agree.
- Retry the same
PATCHon500, which applies the write twice if the first attempt actually succeeded. - Accept a missing
If-Matchand defaultversionto1, which turns the first retry client into last-write-wins. - Emit a weak ETag (
W/"3") and then compare it with strong equality in the handler.
A decision table belongs in the repo next to the tests, because prose in a chat window is not a contract. Keep it short enough that a reviewer can reject a pull request against one row.
Incoming If-Match
|
Current version
|
Status | Body mutated? | Next ETag
|
|---|---|---|---|---|
| missing | any | 428 | no | unchanged |
"3" |
3 | 200 | yes | "4" |
"2" |
3 | 412 | no | "3" |
W/"3" |
3 | 412 | no | "3" |
* |
3 | 412 | no | "3" |
You freeze * as rejected on purpose. Wildcard success is a last-write-wins escape hatch, and agents love escape hatches. If you later need an admin overwrite, add a separate authenticated route instead of loosening PATCH.
Implementation after the suite is red
Only after the tests exist do you allow an agent to write app.py. Seed one note so GET is deterministic, store versions in process memory for the case study, and refuse every write that does not present a strong token. The snippet below is labeled example code for local reproduction, not a claim about a shipped service.
# app.py — example server that satisfies tests/test_note_if_match.py
from fastapi import FastAPI, Header, HTTPException, Response
from pydantic import BaseModel
app = FastAPI()
NOTES = {"note_42": {"id": "note_42", "body": "seed", "version": 1}}
class Patch(BaseModel):
body: str
def strong_etag(version: int) -> str:
return f'"{version}"'
@app.get("/notes/{note_id}")
def get_note(note_id: str, response: Response):
note = NOTES.get(note_id)
if note is None:
raise HTTPException(status_code=404)
response.headers["ETag"] = strong_etag(note["version"])
return note
@app.patch("/notes/{note_id}")
def patch_note(
note_id: str,
payload: Patch,
response: Response,
if_match: str | None = Header(default=None, alias="If-Match"),
):
note = NOTES.get(note_id)
if note is None:
raise HTTPException(status_code=404)
if if_match is None:
raise HTTPException(status_code=428, detail="If-Match required")
expected = strong_etag(note["version"])
if if_match != expected:
raise HTTPException(status_code=412, detail="stale version")
note["body"] = payload.body
note["version"] += 1
response.headers["ETag"] = strong_etag(note["version"])
return note
Start the server in one terminal and re-run the suite in another. You want green from the frozen file, not from a rewritten test that lowered 428 to 200.
uvicorn app:app --port 8088 --log-level warning
python -m pytest tests/test_note_if_match.py -q
The client the agent writes later must GET, copy ETag into If-Match, and on 412 re-fetch instead of retrying the same token. Put that in a second test file only after the server suite is green. Do not let the agent invent a retry loop inside the handler; retries belong to the caller that can see a fresh representation.
Where a coding agent belongs in this workflow
The agent is allowed to write app.py and a PATCH client, and it is not allowed to edit tests/test_note_if_match.py. That split is the whole method. If you need a place to run that loop without standing up a local GPU, MonkeyCode’s free model access and free server option can host the agent against the frozen suite. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Keep the product mention in that one role: it is a runner for an already-specified contract, not a source of HTTP semantics. Do not ask the model to choose between 412 and 409, strong versus weak ETags, or whether missing headers should succeed. Those choices are already in the table. If the agent submits a diff that touches the test file, reject the diff even when the new tests are greener, because green tests that encode last-write-wins are how lost updates ship.
Results you should expect from the example
On the seed note, GET returns version: 1 and ETag: "1". A PATCH with If-Match: "1" returns version: 2. A second PATCH that still carries "1" returns 412 and the body stays as writer A left it. A PATCH with no header returns 428. Those four outcomes are the results that matter; they are pass/fail signals, not traffic numbers.
If you deliberately break the handler by commenting out the 428 branch, the require-header test fails first. That is the result you want in review: the contract catches the agent’s most common shortcut before a human has to notice it in staging. If you break only the increment, the bump test fails and the stale-token test may still pass, which tells you the assertions are not duplicates.
Limitations and who should skip this
This pattern does not serialize writes across two resources, so checkout-plus-inventory still needs a transaction or an outbox. Integer versions also do not survive naive client merges; a client that concatenates two bodies after a 412 is doing conflict resolution, which you have not frozen. Weak ETags, CDN filtering of If-Match, and intermediaries that strip unknown headers can still make a correct handler look flaky, so test at the origin, not only through a cache.
Skip this approach when last-write-wins is actually the product, such as analytics counters, presence heartbeats, or true CRDT documents. Skip it when your store cannot atomically compare-and-swap the version column, because a read-modify-write in application memory will lose the race the tests claim to prevent. Skip it when an agent is also allowed to rewrite tests, because then you do not have a freeze, you have a conversation.
Lessons learned
- Freeze the version window in tests before any PATCH handler exists, or the agent will pick last-write-wins.
- Prefer
428on a missing token over a friendly default version; missing headers are how retry scripts bypass your rule. - Reject weak ETags and
*on this resource so the contract has one comparison, not four. - Keep retries in the client after a fresh
GET; do not retry the sameIf-Matchinside the server. - Treat a test-file edit from the agent as a failed review, even when coverage numbers go up.
You now have a complete small project: a seed note, a red-then-green suite, a handler that only mutates on a matching strong token, and a clear list of domains that should not copy it. Pin the table, run the commands, and only then let an agent type the boilerplate that used to invent lost updates.
Top comments (0)