In financial backend systems, network retries represent a classic race-condition vector. If a client attempts to post a transaction, suffers a TCP timeout, and blindly retries, the backend risks posting the transaction twice.
To prevent duplicate financial operations, we must design an Idempotent API using Idempotency Keys.
The Architecture of Idempotency
sequenceDiagram
participant Client
participant API Gateway
participant Redis Cache
participant PostgreSQL
Client->>API Gateway: POST /journal (Idempotency-Key: X)
API Gateway->>Redis Cache: SetNX(Key: X, Status: IN_PROGRESS)
alt Key exists
Redis Cache-->>Client: 409 Conflict (Request In Progress) or Cached Response
else Key is new
API Gateway->>PostgreSQL: Execute Double-Entry Transaction
PostgreSQL-->>API Gateway: Transaction Committed successfully
API Gateway->>Redis Cache: Set(Key: X, Status: DONE, Response: Y)
API Gateway-->>Client: 201 Created (Transaction Details)
end
Core Database Schema & Unique Constraints
We enforce idempotency at the database level by binding the idempotency key directly to our journal entries table:
CREATE TABLE journal_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
idempotency_key VARCHAR(255) UNIQUE NOT NULL,
amount DECIMAL(18, 4) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
Python Implementation (FastAPI & Redis)
Here is a tested pattern for validating and caching requests in Redis:
from fastapi import FastAPI, Header, HTTPException, status
import redis
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
@app.post("/api/v1/journal", status_code=201)
def post_transaction(payload: dict, idempotency_key: str = Header(...)):
# 1. Atomic Check-and-Set in Redis
lock_acquired = r.set(f"idemp:{idempotency_key}", "IN_PROGRESS", nx=True, ex=3600)
if not lock_acquired:
status_val = r.get(f"idemp:{idempotency_key}")
if status_val == "IN_PROGRESS":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Transaction in progress. Please wait."
)
# Return cached response if done
return {"status": "success", "cached": True, "data": status_val}
try:
# 2. Process transaction inside a database transaction block
# (DB commit logic goes here)
result_data = {"transaction_id": "tx_abc123", "processed": True}
# 3. Update status to completed and store response
r.set(f"idemp:{idempotency_key}", str(result_data), ex=86400)
return {"status": "success", "cached": False, "data": result_data}
except Exception as e:
r.delete(f"idemp:{idempotency_key}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Transaction failed."
)
Testing for Concurrency
To ensure that concurrent requests with the exact same idempotency key are rejected, we can run multithreaded HTTP request tests using pytest and httpx.
You can view the full repository and test suites at secure-fintech-ledger.
Disclosure: This article was prepared with AI-assisted editing and research support. I reviewed the technical content, tested the code and take responsibility for the final article. #ABotWroteThis
Top comments (0)