Idempotent APIs in ASP.NET Core — Why They Matter and How to Build Them
Building Trustworthy APIs for the Real World of Retries
APIs today live in a messy world: flaky networks, impatient users hitting refresh, and systems that retry requests automatically. Without careful design, this chaos can create serious problems — double-charged payments, duplicate orders, or inconsistent data. That’s where idempotent APIs come in.
🔹 What is Idempotency?
In plain terms, idempotency means you can repeat the same request many times and still get the same outcome. Whether the API is called once or ten times, the result is consistent and predictable.
Example
GET /orders/123→ always returns the same order. (Safe and idempotent by definition)POST /payments→ without idempotency, retries may charge the customer multiple times. With idempotency, retries return the same payment result.
📌 Architect Tip:
Think of idempotency as a safety net for retries — it guarantees consistency even in unreliable conditions.
🔹 Why Do We Need It?
Consider this scenario:
A user submits payment.
The request times out before the client receives the response.
The client retries.
Without idempotency, you now have two payments processed. This leads to:
Financial errors 💸
Duplicate data 📑
Frustrated customers 😡
🔹 The Objective of Idempotency
The goal is to make state-changing operations safe to retry. Specifically, it ensures:
Consistency — No accidental duplicates.
Reliability — Clients and load balancers can safely retry.
Trust — Users don’t fear “double charges.”
📌 Architect Tip:
Focus idempotency on
POST,PUT,PATCH, andDELETE.GETis already idempotent by design.
🔹 How Do We Achieve It?
The standard solution is an Idempotency Key:
Client generates a unique key (UUID).
Sends it with the request:
Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
Server checks the store:
If the key exists → return the stored response.
If not → process request, store response, return it.
Where Do We Store Responses?
Redis → Blazing fast, supports TTL (expire automatically after 24h). Ideal for retries.
SQL Database → Persistent, better for audits. Needs cleanup logic.
📌 Architect Tip:
Use Redis for performance. If compliance is required, log entries into SQL for audits.
🔹 ASP.NET Core Implementation
Here’s a middleware implementation that enforces idempotency:
public class IdempotencyMiddleware
{
private readonly RequestDelegate _next;
private readonly IIdempotencyStore _store;
public IdempotencyMiddleware(RequestDelegate next, IIdempotencyStore store)
{
_next = next;
_store = store;
}
public async Task Invoke(HttpContext context)
{
var method = context.Request.Method;
// Skip idempotency check for safe methods
if (method == HttpMethods.Get ||
method == HttpMethods.Head ||
method == HttpMethods.Options ||
method == HttpMethods.Trace)
{
await _next(context);
return;
}
var key = context.Request.Headers["Idempotency-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(key))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Missing Idempotency-Key header.");
return;
}
var cachedResponse = await _store.GetResponseAsync(key);
if (cachedResponse != null)
{
context.Response.StatusCode = cachedResponse.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(cachedResponse.Body);
return;
}
var originalBody = context.Response.Body;
using var memStream = new MemoryStream();
context.Response.Body = memStream;
await _next(context);
memStream.Position = 0;
var body = new StreamReader(memStream).ReadToEnd();
await _store.SaveResponseAsync(key, context.Response.StatusCode, body);
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
context.Response.Body = originalBody;
}
}
And the store interface:
public interface IIdempotencyStore
{
Task<IdempotentResponse?> GetResponseAsync(string key);
Task SaveResponseAsync(string key, int statusCode, string body);
}
public record IdempotentResponse(int StatusCode, string Body);
📌 Architect Tip:
Always set a TTL (e.g., 24h) on stored keys to prevent unbounded growth.
✅ Final Thoughts
Idempotency prevents duplicate transactions and inconsistent states.
Use an Idempotency-Key for state-changing requests.
Prefer Redis with TTL for speed and simplicity.
Add SQL logging only if audit compliance is required.
Middleware in ASP.NET Core keeps the controllers clean and focused.
Idempotency is not just about correctness — it’s about trust. When users know they won’t be double-billed or see duplicate orders, your system earns reliability points. In a world full of retries, idempotency is your best friend.
Happy Reading :)


