ADR 0004: Idempotency keys for awards

Status:

Accepted

Context

Clients retry award requests on network failure. A naive retry would award the same event twice.

Decision

Before inserting, the persistence layer derives an idempotency key from the first non-empty of eventId, idempotencyKey or correlationId (app/services/user_points/persistence.py) and short-circuits if a matching row already exists. The guarantee is enforced at the database by UniqueConstraint("userId", "taskId", "idempotencyKey") (app/model/user_points.py, constraint uq_user_points_user_task_idempotency), so a duplicate can never be persisted even if the read check races.

Consequences

  • Awards are safe to retry: a repeated request with the same key returns the existing assignment instead of creating a second one.

  • The dedupe scope is (user, task, key) - the same key under a different task is a different award. A null key is allowed (the un-deduped path).

  • Under a true concurrent retry race, the loser of the unique-constraint race receives 409 Conflict rather than the original success body. The no-double-award guarantee still holds. See Known limitations.

See also