Skip to content

Idempotency

The Idempotency module provides at-most-once execution guarantees for HTTP requests, webhook deliveries, and event processing. It uses a key-based claim mechanism with optional response caching, configurable via IdempotencyOptions.

Quick Start

using Deepstaging.Idempotency;

// In Program.cs — add middleware
app.UseIdempotency();

// Configuration (appsettings.json)
{
    "Deepstaging": {
        "Idempotency": {
            "HeaderName": "Idempotency-Key",
            "DefaultTtl": "1.00:00:00",
            "RequiredMethods": ["POST", "PUT", "PATCH"],
            "ReturnCachedResponse": true
        }
    }
}

Clients include an Idempotency-Key header; the middleware deduplicates automatically:

POST /api/orders
Idempotency-Key: order-abc-123

Features

Feature Description
IIdempotencyStore Core abstraction — TryClaimAsync, IsClaimedAsync, ReleaseAsync, response caching
IdempotencyMiddleware ASP.NET Core middleware that intercepts requests by header key
IdempotencyOptions [ConfigSection] — header name, TTL, required methods, excluded paths, response caching mode
Response caching TryGetCachedResponseAsync / StoreCachedResponseAsync — replay identical responses for duplicate keys
CachedResponse Captures status code, headers, and body for replay
Effects module IdempotencyModule for Eff<RT, T> composition
Test double TestIdempotencyStore with call recording
InMemory InMemoryIdempotencyStore with concurrent dictionary + TTL expiry

Core Interface

public interface IIdempotencyStore
{
    Task<bool> TryClaimAsync(string key, TimeSpan? expiry = null, CancellationToken ct = default);
    Task<bool> IsClaimedAsync(string key, CancellationToken ct = default);
    Task ReleaseAsync(string key, CancellationToken ct = default);
    Task<CachedResponse?> TryGetCachedResponseAsync(string key, CancellationToken ct = default);
    Task StoreCachedResponseAsync(string key, CachedResponse response, TimeSpan? expiry = null, CancellationToken ct = default);
}

Configuration

Property Default Description
HeaderName "Idempotency-Key" HTTP header to read the key from
DefaultTtl 24 hours How long claimed keys persist
RequiredMethods POST, PUT, PATCH HTTP methods that require an idempotency key
ExcludedPaths /health, /swagger Paths that bypass idempotency checks
ReturnCachedResponse true Return cached response for duplicates (true) or 409 Conflict (false)

How It Works

  1. Client sends request with Idempotency-Key header
  2. Middleware calls TryClaimAsync(key) on IIdempotencyStore
  3. First request: claim succeeds → request proceeds → response is cached via StoreCachedResponseAsync
  4. Duplicate request: claim fails → cached response is returned (or 409 if ReturnCachedResponse = false)
  5. Keys expire after DefaultTtl

Dispatch Attribute

Mark handlers as idempotent with [Idempotent]:

[CommandHandler]
[HttpPost("/orders")]
[Idempotent]
public static Eff<AppRuntime, OrderPlaced> Handle(CreateOrder cmd) => ...

See Attributes for details.

Effects Composition

Use IdempotencyModule for manual idempotency in event handlers and background jobs:

from claimed in IdempotencyModule.Idempotency.TryClaimAsync<AppRuntime>(
    $"webhook:{eventId}", TimeSpan.FromHours(24))
from _ in claimed ? ProcessEvent(event) : unitEff
select unit;

See Effects Composition for multi-step pipelines.

Sub-Pages

Page Description
Attributes [Idempotent] attribute and configuration options
Effects Composition Manual Eff<RT, T> idempotency — claim, release, recovery
Generated Code What [EffectsModule] emits — capability interface, effect methods
Testing TestIdempotencyStore — seeding claimed keys, asserting claims

Source

src/Core/Deepstaging.Runtime/Idempotency/