# Idempotency

> Use when implementing financial idempotency, unique key constraints, race condition prevention, and duplicate request resolution across REST APIs and batch processes.

- Skill: `sahilkhan30/idempotency` (Agent Skill)
- Install (CLI): `npx skillmds@latest add sahilkhan30/idempotency`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sahilkhan30/idempotency/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: SahilKhan30 (https://skillmd.com/u/sahilkhan30)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/sahilkhan30/idempotency

---


# Financial Idempotency & Duplicate Prevention Skill

## Purpose
Enforce strict financial idempotency across REST APIs and background ingestion pipelines using unique database constraints, scoped key ownership, and atomic state handling.

---

## 1. Core Principles

### MANDATORY
- **Atomic Persistence**: Back idempotency keys with unique database constraints (`CONSTRAINT uq_scope_idempotency UNIQUE(scope_id, idempotency_key)`).
- **Domain Scoping**: Idempotency keys MUST be scoped by entity ownership (e.g. `merchant_id + idempotency_key` or `account_id + idempotency_key`) unless globally unique by specification.
- **Race Condition Safety**: Catch database unique constraint violations (`DataIntegrityViolationException`) when concurrent requests arrive simultaneously with the same key.
- **State Handling**: Track idempotency state explicitly (`PROCESSING` vs `COMPLETED` vs `FAILED`):
  - If state is `PROCESSING`: Return HTTP 409 Conflict or 202 Accepted (Wait/Poll) based on domain requirement.
  - If state is `COMPLETED`: Return stored result payload immediately.
- **Frontend Key Semantics**: The idempotency key MUST represent the **logical operation** generated when the user initiates an action (`crypto.randomUUID()`), NOT a new key generated on every HTTP retry attempt.

---

## 2. Reference Implementation Pattern

```java
@Transactional
public TransactionResponse processTransaction(String merchantId, String idempotencyKey, TransactionRequest request) {
    // 1. Attempt to insert lock record or check existing
    Optional<IdempotencyRecord> existing = idempotencyRepository.findByMerchantIdAndKey(merchantId, idempotencyKey);
    if (existing.isPresent()) {
        IdempotencyRecord record = existing.get();
        if ("COMPLETED".equals(record.getStatus())) {
            return objectMapper.readValue(record.getResponsePayload(), TransactionResponse.class);
        } else if ("PROCESSING".equals(record.getStatus())) {
            throw new ConcurrentOperationException("Operation currently in progress. Please wait.");
        }
    }

    // 2. Persist INITIAL processing state
    IdempotencyRecord lockRecord = new IdempotencyRecord(merchantId, idempotencyKey, "PROCESSING");
    try {
        idempotencyRepository.saveAndFlush(lockRecord);
    } catch (DataIntegrityViolationException ex) {
        // Race condition: another thread inserted the key simultaneously
        return handleConcurrentDuplicate(merchantId, idempotencyKey);
    }

    // 3. Execute business logic atomically
    TransactionResponse response = executionService.execute(merchantId, request);

    // 4. Update idempotency record to COMPLETED
    lockRecord.setStatus("COMPLETED");
    lockRecord.setResponsePayload(objectMapper.writeValueAsString(response));
    idempotencyRepository.save(lockRecord);

    return response;
}
```

