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_keyoraccount_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 (
PROCESSINGvsCOMPLETEDvsFAILED):- 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.
- If state is
- 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
@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;
}