Skip to main content

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 (sessions create, 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:

  • requireIdempotencyKey in backend/internal/http/handler/util.go

5.2 Handler flow

For keyed operations:

  1. Parse and validate request.
  2. Build a stable, typed idempotency payload.
  3. Compute canonical hash (hashIdempotencyPayload).
  4. Call idemSvc.Begin(...).
  5. If replay hit, return stored response.
  6. Execute business logic once.
  7. 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.create
  • programmes.requirements.update
  • submissions.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 via make lint-arch)
  • scripts/check_openapi_idempotency.sh (wired via make lint-arch)

7) OpenAPI requirements

OpenAPI must reflect runtime behavior:

  • define reusable Idempotency-Key header parameter
  • mark it required on all in-scope mutating operations
  • keep it absent on explicit exemptions
  • document idempotency conflict and in-progress error responses

  • docs/architecture/FOUNDATION.md
  • docs/architecture/architecture
  • docs/architecture/LEDGER-BOUNDARY-AND-ENFORCEMENT.md
  • docs/product/implementation-status-feature-matrix.md