Why idempotency matters

Retries are only safe when the system can distinguish a repeated request from a new operation.

RelPrim 0.8.0 adds an idempotency layer for external operations such as payment creation, webhook processing, job submission and writes to third-party APIs.

Stable idempotency keys

An operation can now derive a stable idempotency key directly from its arguments.

from relprim import resilient


@resilient(
    retries=2,
    timeout=10,
    idempotency_key=lambda request_id, amount: (
        f"create-payment:{request_id}"
    ),
    idempotency_ttl=3600,
)
async def create_payment(
    request_id: str,
    amount: int,
) -> str:
    return await payment_gateway.create(request_id, amount)

Repeated calls using the same key are treated as the same logical operation rather than unrelated executions.

Concurrent execution joining

When multiple callers use the same key while an operation is still running, RelPrim executes the underlying operation only once.

The first caller owns the execution. Other callers join it and receive the same result when it completes.

This prevents duplicate side effects caused by concurrent requests, repeated message delivery or callers retrying before the original request has finished.

Successful result replay

Successful results are retained for a configurable TTL.

first = await create_payment("request-123", 2499)
second = await create_payment("request-123", 2499)

print(first.report.metadata["idempotency_status"])
# executed

print(second.report.metadata["idempotency_status"])
# replayed

The second call returns the previously completed result without executing the payment operation again.

Failed executions are not cached, so a later call can execute the operation again.

Interaction with retries and fallbacks

Idempotency wraps the complete RelPrim execution lifecycle rather than an individual retry attempt.

That lifecycle can include:

  • retries;
  • timeouts;
  • validation;
  • circuit breaker checks;
  • fallback execution;
  • execution reports;
  • structured events.

A duplicate call therefore does not start a separate retry or fallback flow. It joins an execution already in progress or replays its successful result.

Storage

RelPrim 0.8.0 includes InMemoryIdempotencyStore and a public IdempotencyStore protocol for custom implementations.

The in-memory store coordinates calls within one Python process. It is useful for tests, local applications and single-process services, but it is not a distributed or durable idempotency store.