Summary
The codebase has deduplication scaffolding (src/deduplication/{fingerprint,service,middleware}.rs) but no end-to-end idempotency contract for mutating endpoints. A client that times out and retries POST /tips, a refund, or a scheduled-tip creation can double-execute the operation. For money-adjacent APIs, first-class Idempotency-Key semantics (à la Stripe) are table stakes.
Task
- Implement an
Idempotency-Key middleware for all mutating routes (POST/PUT/PATCH/DELETE, opt-in per route via router layer):
- key: client-supplied UUID header, scoped per authenticated principal + route
- first request: execute, persist
(key, request_fingerprint, response_status, response_body_hash + body) with TTL (default 24h)
- retry with same key + same fingerprint: replay the stored response (identical status/body) without re-executing
- same key + different request body:
422 Unprocessable Entity with a typed error
- Concurrency correctness — the hard part: two in-flight requests with the same key must not both execute. Use a Redis
SET NX lock (or Postgres advisory lock) so the second request either waits for and replays the first result, or receives 409 Conflict with Retry-After — choose, justify, and document the semantics.
- Storage: Redis primary with Postgres fallback table (new migration) so idempotency survives Redis eviction for financial endpoints; response bodies capped and compressed.
- Integrate with the existing deduplication module or explicitly retire it — no two half-overlapping systems may remain.
- Metrics: replay hits, conflict rejections, lock contention histogram.
- Tests: replay returns byte-identical response; fingerprint mismatch → 422; concurrent duplicate execution race test (spawn N parallel identical requests, assert exactly one execution via a side-effect counter); TTL expiry allows re-execution; Redis-down fallback path.
Acceptance criteria
PR requirements (mandatory)
- ✅ Your PR must pass all checks — PRs with failing or skipped checks will not be merged.
- 📸 You must attach a screenshot in the PR description demonstrating the result (for this issue: the race test output proving single execution under N concurrent retries).
- 🔗 You must link this issue number in your PR description (e.g.
Closes #<issue-number>). PRs without a linked issue will not be reviewed.
Summary
The codebase has deduplication scaffolding (
src/deduplication/{fingerprint,service,middleware}.rs) but no end-to-end idempotency contract for mutating endpoints. A client that times out and retriesPOST /tips, a refund, or a scheduled-tip creation can double-execute the operation. For money-adjacent APIs, first-classIdempotency-Keysemantics (à la Stripe) are table stakes.Task
Idempotency-Keymiddleware for all mutating routes (POST/PUT/PATCH/DELETE, opt-in per route via router layer):(key, request_fingerprint, response_status, response_body_hash + body)with TTL (default 24h)422 Unprocessable Entitywith a typed errorSET NXlock (or Postgres advisory lock) so the second request either waits for and replays the first result, or receives409 ConflictwithRetry-After— choose, justify, and document the semantics.Acceptance criteria
PR requirements (mandatory)
Closes #<issue-number>). PRs without a linked issue will not be reviewed.