Idempotency and Retry Policy
This document defines where keyed idempotency is required in Evalium and how it must be implemented.
It is binding guidance for backend handlers, services, OpenAPI contracts, and regression tests.
1) Policy intent
Evalium is an append-only execution ledger. Duplicate writes create integrity debt.
For that reason, retries must be deterministic and safe:
- the same logical request must produce one durable outcome
- replaying the same request key must return the same response
- key reuse with different payload must be rejected
2) Required scope
2.1 Default rule
Idempotency-Key is required on mutating API operations (POST, PATCH, PUT, DELETE) unless explicitly exempted in this policy.
2.2 Durable/admin write surfaces (required)
Required for all write operations across:
- evaluations, evaluation versions, feedback, sections, items, buckets
- question and passage authoring
- programmes, requirements, enrolments
- users, roles, groups, subjects, and import commit flows
- assignments and assignment redemption/overrides/schedules
- submissions, evidence decisions/actions, review actions, hash, void, subject links
- claims and disputes (create + event append)
- engagements (create/update/event/ratify/hash)
- remediation batch create/apply/revert
- compliance job-creation surfaces
- reporting maintenance writes (projection enqueue, percentile recompute)
- delivery session write path (
sessionscreate, answer, submit)
2.3 Explicit exemptions (currently allowed without key)
Non-durable or auth/telemetry surfaces:
- auth session endpoints (login/verify/logout/revoke/revoke-all)
- preview/validate endpoints
- session telemetry event ingestion (
/sessions/\{sessionId\}/events) - import preview endpoints
- internal test-email endpoint
Any new exemption requires explicit documentation and architectural review.
3) Request contract
3.1 Header
- Header name:
Idempotency-Key - Required on all in-scope endpoints
- Maximum length:
200
3.2 Error behavior
- Missing key:
400 validation_error(idempotency key is required) - Key too long:
400 validation_error(idempotency key too long) - Same key + different payload:
409 idempotency_conflict - Same key while original request still in progress:
409 idempotency_in_progress - Same key + same payload after finalize: replay stored status/body
4) Persistence and scope
Idempotency records are stored in idempotency_keys and reserved before side effects.
Uniqueness scope is:
- tenant
- actor (
user_id; anonymous flows use sentinel user id) - endpoint key
- idempotency key
Request payload hash is stored and compared on replay.
5) Implementation standard
5.1 Route enforcement
Use route middleware to enforce presence/length at the HTTP boundary:
requireIdempotencyKeyinbackend/internal/http/handler/util.go
5.2 Handler flow
For keyed operations:
- Parse and validate request.
- Build a stable, typed idempotency payload.
- Compute canonical hash (
hashIdempotencyPayload). - Call
idemSvc.Begin(...). - If replay hit, return stored response.
- Execute business logic once.
- Call
Finalize(...)with status/body for both success and mapped domain errors.
5.3 Payload rules
Payload structs must be deterministic and include all fields that define logical uniqueness:
- request body fields that alter behavior
- resource path identifiers (for update/delete/sub-resource writes)
- actor or scope fields when behavior depends on actor identity
Avoid map-order instability and ad-hoc hash construction.
5.4 Endpoint key naming
Use explicit endpoint constants per handler, for example:
evaluations.createprogrammes.requirements.updatesubmissions.evidence.approve
Names must be stable across refactors.
6) Testing requirements
Each in-scope write surface must have idempotency coverage via:
- replay-success test: same key + same payload returns first result
- conflict test: same key + different payload returns conflict
- optional in-progress test for long-running paths
Coverage may be in focused smoke scripts plus integration/unit tests.
CI includes a route-enforcement guardrail:
scripts/check_idempotency_routes.sh(wired viamake lint-arch)scripts/check_openapi_idempotency.sh(wired viamake lint-arch)
7) OpenAPI requirements
OpenAPI must reflect runtime behavior:
- define reusable
Idempotency-Keyheader parameter - mark it required on all in-scope mutating operations
- keep it absent on explicit exemptions
- document idempotency conflict and in-progress error responses
8) Related references
docs/architecture/FOUNDATION.mddocs/architecture/architecturedocs/architecture/LEDGER-BOUNDARY-AND-ENFORCEMENT.mddocs/product/implementation-status-feature-matrix.md