Express.js / Advanced: resource, write and release contracts / Lesson 16
Write idempotency, revisions and transaction boundaries
Design repeated HTTP writes around logical request identity and authoritative reconciliation.
On this page
Lesson goals
- Separate idempotency keys from object IDs and expected revisions.
- Explain why an HTTP error response does not establish rollback.
A missing response can follow a committed write
The server can commit a change and lose its connection before the client receives confirmation. Retrying the same POST without a domain protocol may repeat an effect. A logical request key identifies one intended attempt across transport retries; it is not the object ID, because one object can receive many edits. Bind it to principal scope, operation and canonical input fingerprint. Define retention, concurrent duplicate handling and how the recorded outcome is returned. A generic Express error handler cannot infer these semantics or decide that a retry is safe merely from a timeout code.
Pass distinct identities to a coherent domain operation
The fragment treats an idempotency header and expected revision as separate inputs. Both require validation before use, and the domain service must implement the durable protocol; merely forwarding strings does not make it safe. A repeated key with different canonical input should produce a stable conflict rather than silently replaying an unrelated result. Scope recorded outcomes so guessing another user’s key cannot disclose it. A canonical result lets the client reconcile server normalization and revision, while the operation’s documented status policy determines how first success and replay are represented.
app.post("/courses/:id/publish", async (req,res) => {
const requestKey = validatedRequestKey(req.get("Idempotency-Key"));
const input = validatedPublishInput(req.body);
const result = await catalog.publish({
principal:req.principal,id:req.params.id,requestKey,
expectedRevision:input.expectedRevision
});
return res.status(result.status).json(result.publicBody);
});
Validation and catalog methods are injected unexecuted dependencies. The route forwards distinct identities and does not implement durable replay.
Atomicity belongs where effects and replay outcomes are stored
Checking a key and then writing in separate unprotected steps admits a duplicate race. Coordinate the state change, uniqueness and recorded outcome using the selected storage protocol. External messages cannot automatically join a database transaction; a transactional outbox can record delivery intent alongside the change, while delivery still needs retries and deduplication. An expected revision prevents a new stale edit but does not replace replay identity. A replay may need to return its original successful result even though the object revision has since moved on, according to the documented contract.
Check identical replay and changed-input rejection locally
The synchronous in-memory model increments one synthetic effect counter once and returns the recorded result for an identical replay. A changed fingerprint under the same key is rejected. This is intentionally not durable, concurrent or crash-safe. A production design needs an atomic shared store and a clear recovery policy; the local map establishes none of those properties. Its purpose is to make the business distinction testable without running a database. Do not retain sensitive canonical input unnecessarily merely to simplify a fingerprinting implementation or expose it in logs.
const outcomes=new Map(); let effects=0;
function apply(key,fingerprint) {
if (outcomes.has(key)) {
const prior=outcomes.get(key);
if (prior.fingerprint !== fingerprint) throw new Error("KEY_REUSED");
return prior.result;
}
const result={effect:++effects};
outcomes.set(key,{fingerprint,result}); return result;
}
console.assert(apply("r1","c1@4").effect === 1);
console.assert(apply("r1","c1@4").effect === 1);
let rejected=false; try {apply("r1","c1@5");} catch {rejected=true;}
console.assert(rejected && effects === 1);
One effect occurs, identical replay returns it, and changed input is rejected. The map offers no durability or concurrent duplicate protection.
Verify effects as well as the public HTTP result
Test first success, identical replay, changed input, stale new edit, simultaneous duplicates and response loss after commit. Rejected authorization and validation must leave the write spy untouched. Include expiry of the replay record and state how clients reconcile after the retention window. Verify the authoritative object and operation state rather than relying on an optimistic client cache. This course executes only the map model, not a transaction, queue or HTTP retry. A well-formatted error is not evidence that a change rolled back, and a 200 replay is not proof that a new effect happened.
Common errors and fixes
A retry creates a second publication effect
- Why it happens
- Transport attempts lacked logical request identity.
- How to fix it
- Use a scoped durable replay and reconciliation contract.
- Prevent it next time
- Test lost response after commit.
Concurrent duplicates both write
- Why it happens
- Key check and effect were not atomically coordinated.
- How to fix it
- Apply storage uniqueness and transaction rules together.
- Prevent it next time
- Verify duplicate races in isolated integration.
A new edit is mistaken for an identical replay
- Why it happens
- The key was not bound to canonical input and operation scope.
- How to fix it
- Reject mismatched fingerprints under a stable conflict policy.
- Prevent it next time
- Test changed input with the original key.
Check your understanding
1. Does expectedRevision eliminate the need for requestKey?
Show worked solution
No. Revision checks address conflicting new edits, while a request key recognizes retransmission of the same logical operation. The first may already have succeeded when its response is lost. Define how replay retrieves that result before treating a now-stale revision as a new conflict. The domain storage protocol owns the ordering and atomicity.
2. What does the local replay model not prove?
Show worked solution
It does not prove persistence, concurrent uniqueness, crash recovery, tenant scoping or HTTP behavior. It tests one synchronous map’s treatment of identical and changed fingerprints. Keep that useful evidence separate from the authorized transaction and source integration required for a real write protocol.
Key takeaways
- Request identity, revision and transaction atomicity protect different concerns.
- HTTP error formatting cannot establish rollback or safe replay.