Designing idempotent POSTs for a ledger API
A client posts an expense, the connection times out, and the client retries. Without protection you get two ledger entries and a balance that no longer matches reality. ReckonFlow treats that as the default failure mode of HTTP — not an edge case — and builds around it.
What the client sends
Mutating endpoints honor an Idempotency-Key header. The first
call with a given key runs the handler; a retry with the same key and the
same body replays the stored response and sets
Idempotency-Replayed: true.
The cache key is scoped by Redis prefix, method, path, and a hash of the body. Same key with a different body is not a replay — it is a different request. That stops one key from accidentally replaying an unrelated endpoint's response.
The claim: SET NX EX
On the first attempt Redis does an atomic SET key NX EX 86400.
If the set succeeds, this worker owns the key and runs the route. While it
is still working the value is a sentinel (__in_progress__);
concurrent callers with the same key get 409 IdempotencyConflict
and should retry shortly.
When the handler finishes, the middleware captures status, headers, and body, then overwrites the Redis entry with that snapshot (keeping the TTL). The next identical request rebuilds the response from cache and never re-runs the service layer.
Corrupt entries and background tasks
If the stored value is not valid JSON, ReckonFlow deletes it and reclaims
the key instead of serving garbage. Receipt uploads that return
202 with FastAPI BackgroundTasks keep those tasks
attached after the response is rebuilt — otherwise extraction would silently
disappear on the replay path.
Fail-open when Redis is down
When Redis is unreachable the middleware logs a warning and lets the request through. Availability wins over the retry guarantee for that moment. That is a deliberate trade-off for a public demo API (see ADR 003). Redis is not a hard dependency for serving traffic — only for the idempotency guarantee. Prefer fixing Redis over flipping this to fail-closed unless you are ready to shed load.
Why this belongs in a portfolio
CRUD demos rarely force you to answer: what happens on a retry? what if the cache is mid-write? what if the cache is gone? The interesting parts of finance software are money precision, safe retries, concurrent reconciliation, and untrusted model output. Idempotent POSTs are one of those four.