Field Note 19 current
Annotate read-only and idempotent endpoints; make every mutation idempotent
TL;DR
Every endpoint declares its semantics, ideally in the schema (ZFN-14): is it read-only (safe — no side effects), idempotent (repeating it leaves the same state), or neither? Make those properties machine-readable so infrastructure can act on them: safely retry an idempotent call, route a read to a replica, cache a safe response, let automated tooling call read-only endpoints freely.
And the rule that makes the system resilient: every endpoint that changes state must be idempotent. Retries are inevitable — networks time out, clients re-send, queues redeliver — and a retry of a non-idempotent mutation double-charges, double-creates, double-sends. Use natural idempotency where you can (set-this-state operations) and idempotency keys where you can’t (create, charge, send). Idempotency is the precondition that “retry from the start” quietly depends on.
Context
Two facts about distributed systems make this non-optional:
- You can never tell a lost request from a lost response. When a call fails or times out, the client doesn’t know whether the server did the work. Its only safe move is to retry — which is exactly what ZFN-13 says to build in from the start. But a retry of an operation that isn’t idempotent applies it twice: two charges, two orders, two emails. “Exactly once” delivery is a fiction; what you actually get is at-least-once, plus idempotent processing = effectively once. The same is true for messages off a queue (ZFN-12), which redeliver by design and demand idempotent consumers.
- Infrastructure wants to know what’s safe, and currently has to guess. A gateway could retry a failed call, a router could send a read to a replica, a cache could serve a response — but only if it knows the call is safe/idempotent. Without a declared annotation, each client hand-codes that knowledge, inconsistently, and the safe optimisations either don’t happen or happen unsafely.
HTTP encodes some of this in method semantics (GET/HEAD safe; PUT/DELETE idempotent; POST
neither), but methods alone are too coarse and easy to misuse, and non-HTTP APIs have nothing. The
property needs to be declared explicitly, per endpoint, and trustworthy.
Recommendation
Declare endpoint semantics in the contract, and make every mutation idempotent.
- Annotate every endpoint, in the schema. Mark each as read-only/safe, idempotent, or idempotency-key-required, as part of the schema/IDL (ZFN-14) so it’s machine-readable and generated into clients and gateways — not a comment a human has to find.
- Don’t lie about it. A read-only annotation is a promise of no side effects — no writes, no state-changing downstream calls, no “read that also logs a mutation.” Tooling, routing, and retries will trust it; if it’s wrong, the bug is subtle and nasty.
- Make every state-changing endpoint idempotent. Design mutations so applying them twice equals
applying them once:
- Natural idempotency — prefer operations that set a state rather than increment it
(
PUT-style “make it so”), keyed by a client-known identifier, so a repeat is a no-op. - Idempotency keys — for operations with no natural key (create, charge, send), require a client-supplied unique idempotency key; the server records it and returns the original result on a repeat instead of doing the work again. Dedup within a sensible window and persist the outcome.
- Conditional writes — use optimistic concurrency (ETags / version numbers / compare-and-set) so a replayed or racing update can’t silently double-apply.
- Natural idempotency — prefer operations that set a state rather than increment it
(
- Let infrastructure use the annotations. Once semantics are declared and honest: gateways/clients retry idempotent calls automatically (closing the loop with ZFN-13), read-only traffic routes to replicas and caches, and automated callers (test harnesses, agents) can exercise read-only endpoints without fear of side effects.
Scope. “Idempotent” here means effect idempotence — the observable state ends up the same. It doesn’t forbid an endpoint from returning different representations over time (a GET reflects current state); it forbids a repeated mutation from applying more than once.
Consequences
Easier:
- Retries become safe, which is what makes the whole resilience story (ZFN-13) actually work without double-applying effects.
- Infrastructure can optimise safely on declared semantics — automatic retries, replica routing, caching — instead of guessing.
- Queue consumers can be at-least-once and still correct, because processing is idempotent (ZFN-12).
- Automated and AI tooling can call read-only endpoints freely, which is increasingly valuable.
Harder:
- Idempotency keys are real machinery: storing keys and outcomes, choosing a dedup window, handling the in-flight-duplicate race, and deciding how long to remember. Worth it; not free.
- Some operations resist natural idempotency and need careful key or conditional-write design.
- Honest annotations require discipline — a read-only endpoint that quietly writes (an audit log, a last-seen timestamp) is a trap, so even “harmless” side effects have to be accounted for.
- Declaring and maintaining the semantics is one more thing the contract must keep accurate.
References
- ZFN-13 — retries from the start are only safe if mutations are idempotent; this note is that precondition.
- ZFN-12 — queues redeliver (at-least-once); idempotent consumers are how you stay correct.
- ZFN-14 — declare read-only/idempotent in the schema so it’s machine-readable and generated, not folklore.
- RFC 9110 §9.2 — Safe and idempotent methods; idempotency-key patterns (e.g. Stripe’s) as a worked design.
Changelog
- 2026-06-12: First published as a Field Note.