idempotency
Duplicate request handling with TTL support
IdempotencyStore is a platform port for ensuring exactly-once semantics. Clients send an Idempotency-Key header, and the store caches operation results. Repeated requests with the same key return the cached result without re-executing the operation. Supports TTL for automatic expiration of cached results.
1
Ports
1
Schemas
4
Hooks
4
Events
01
Ports
02
Schemas
Defines
Uses
No external schemas used
03
Hooks
- before_check
- on_cache_hit
- on_cache_miss
- before_store
04
Events
- IdempotencyCacheHit v1
- IdempotencyCacheMiss v1
- IdempotencyResultStored v1
- IdempotencyKeyExpired v1
05
Stories
06
Examples
from psp.platform.idempotency import check_idempotency
def create_order(store: IdempotencyStore, key: str | None, data: dict):
def operation():
order = Order.create(data)
repo.add(order)
return {"order_id": str(order.id)}
return check_idempotency(store, key, operation)
# First call executes the operation
result1 = create_order(store, "order-abc-123", data)
# Second call returns cached result (no duplicate order)
result2 = create_order(store, "order-abc-123", data)
assert result1 == result2
Wrap operations with automatic idempotency.
async def process_payment(store: IdempotencyStore, key: str, amount: int):
# Check for existing result
existing = store.get(key)
if existing is not None:
return existing
# Execute payment (only runs once per key)
result = await payment_gateway.charge(amount)
# Cache result with 24-hour TTL
store.put(key, result, ttl_seconds=86400)
return result
Direct store access for complex flows.
from fastapi import Header
from psp.platform.idempotency import IDEMPOTENCY_KEY_HEADER
@router.post("/orders")
async def create_order(
data: OrderCreate,
idempotency_key: str | None = Header(
None, alias=IDEMPOTENCY_KEY_HEADER
),
):
return check_idempotency(
store,
idempotency_key,
lambda: order_service.create(data),
)
Use standard header for idempotency keys.
from psp.platform.idempotency import InMemoryIdempotencyStore
def test_idempotent_create():
store = InMemoryIdempotencyStore()
call_count = 0
def operation():
nonlocal call_count
call_count += 1
return {"id": "123"}
# First call executes
result1 = check_idempotency(store, "key-1", operation)
assert call_count == 1
# Second call returns cached result
result2 = check_idempotency(store, "key-1", operation)
assert call_count == 1 # Not incremented
assert result1 == result2
Use in-memory store for testing.
class RedisIdempotencyStore(IdempotencyStore):
def __init__(self, redis: Redis) -> None:
self._redis = redis
def get(self, key: str) -> Any | None:
data = self._redis.get(f"idem:{key}")
return json.loads(data) if data else None
def put(self, key: str, result: Any, ttl_seconds: int | None = None):
data = json.dumps(result)
if ttl_seconds:
self._redis.setex(f"idem:{key}", ttl_seconds, data)
else:
self._redis.set(f"idem:{key}", data)
def delete(self, key: str) -> bool:
return self._redis.delete(f"idem:{key}") > 0
Production implementation with Redis.
API Reference
This component mounts routes under /v1/idempotency.
View OpenAPI specification