Ascentic Logo Dark
ENSV
  • Home

  • About Us

  • Services

  • Case Studies

  • Knowledge Hub

  • Career

  • News

Contact Us

hello@ascentic.se

+(94) 112 870 183

+(46) 855 924448

Locations

Onyx Building,
475/4,
Sri Jayawardenepura Kotte

Get Directions

Convendum, Vegagatan 14,
113 29 Stockholm

Get Directions

©2021 Ascentic (Pvt) Ltd. All Rights Reserved

Back to Knowledge Hub

Designing idempotent APIs: why it matters and how to avoid costly mistakes

Pasindu Prabhashitha
Pasindu Prabhashitha
Software Engineer @ Ascentic

Imagine this: you’re on a shopping app, you hit “Place Order,” and the network drops. The button spins. Nothing seems to happen. So, naturally, you tap it again.

Without idempotency in the backend, you’ve just bought the same item twice. Inventory is deducted twice. You get double confirmation. And the warehouse? It’s now shipping you two identical packages.

This is exactly why idempotency in APIs isn’t just a “nice to have” it’s essential to building reliable, user-safe systems.

Warehouse worker holding duplicate packages due to repeated API order submission; shipping system displays duplicate orders caused by network error and lack of idempotency.

 

What Is Idempotency in the Context of APIs?

At its core, an operation is idempotent if performing it multiple times results in the same outcome as performing it once. In practical terms: calling the same API repeatedly (intentionally or not) should not cause unintended side effects.

For example:

  • Deleting a user should still return success if you try to delete the same user again.
  • Updating a profile with the same data shouldn’t rewrite history every time it’s submitted.

 

Why Idempotency Matters in Real Systems

Systems don’t live in perfect environments. They deal with:

  • Flaky networks
  • Mobile clients with auto-retry logic
  • Payment processors sending webhook retries
  • Users mashing the refresh or submit button

Without safeguards, this leads to:

  • Duplicate orders
  • Double payments
  • Inventory miscounts
  • Confused customers and stressed engineers

With proper idempotency in place:

  • The first request is processed.
  • Any duplicate requests return the same response without triggering the business logic again.

This simple guarantee brings massive stability to systems.

HTTP Methods and Idempotency Know What to Expect

Some HTTP methods are naturally idempotent by design. Others… not so much.

 

Method

Idempotent?

Notes

GET

✅

Always idempotent. It just retrieves data.

PUT

✅

Replaces a resource. Sending it multiple times is safe.

DELETE

✅

Deleting something that’s already gone is fine.

POST

❌

Creates new resources. Retrying can create duplicates.

PATCH

⚠️

Depends on the logic. Not always idempotent (If you are updating an array as an example).

 

So if you’re working with POST or PATCH, you'll need to go the extra mile to build in idempotency.

 

How to Implement Idempotency

There are a few common ways to handle idempotency. It really depends on how the system is structured and where the risk of duplication comes from. Here are three approaches that are often used.

 

1. Idempotency Keys (Most Common for APIs)

Clients generate a unique key for each request (often a GUID or hash) and attach it to the Idempotency-Key HTTP header.

How it works:

Server receives the request, checks if it’s seen the key before.

  • If yes, return the stored response.
  • If no, process the request, store the response with the key, and return it.

This is the industry standard for avoiding duplicate transactions in financial systems, e-commerce platforms, and REST APIs.

 

2. Database Upserts (INSERT or UPDATE Logic)

Use SQL operations like:

INSERT INTO orders (...) 
ON CONFLICT (idempotency_key) DO UPDATE ...

This ensures the request inserts a new row if it’s new, or updates the same row if retried. Many modern ORMs or raw SQL support this.

 

3. Message ID Tracking (For Messaging/Event Systems)

In event-driven or message queue architectures, you may receive the same message more than once.
 Solution: Track message IDs. If one is already processed, just skip it.

Out of the approaches mentioned above, this article focuses on the Idempotency-Key pattern which is especially useful for making HTTP APIs safe against duplicate requests. The following sections walk through how this was implemented in a .NET-based application using Minimal APIs and Redis for fast lookup.

Implementing Idempotency in .NET using the Idempotency-Key Pattern

This implementation is done in a typical client-server application using .NET, with a relational database like SQL Server and Redis added for faster access. The client sends a request with a unique Idempotency-Key in the header. On the server side, the key is checked in Redis to see if the request was already handled. If it was, the saved response is returned directly. If not, the request is processed, and the response is stored in Redis along with the key, so any duplicate request can be safely ignored.

Diagram showing Idempotency-Key implementation: client generates an idempotency key, sends it with the request; server checks Redis for the key before executing logic and interacting with the database; response is cached in Redis to prevent duplicate operations.

Redis is used here primarily for performance it allows quick access to previously handled requests without the overhead of hitting a relational database for every check. The same concept can be applied across different backend stacks as well. The key principle remains the same: prevent duplicate side effects by caching responses for repeat requests.

In the following implementation, this is done in a .NET Web API project using Minimal APIs. Since Minimal APIs are becoming more common, the idempotency key checking logic is implemented as an EndpointFilter. This allows the logic to be cleanly separated and reused it can be easily plugged into any endpoint that needs to be made idempotent. 

Here’s what a typical request looks like when a client includes an Idempotency-Key in the header:

POST /api/items HTTP/1.1
Host: example.com
Content-Type: application/json
Idempotency-Key: 58fa4d7c-b92f-4c9a-ae32-6f4fc5311d77

{
  "name": "Wireless Mouse",
  "quantity": 2,
  "price": 29.99
}

The following code shows how the core logic is handled inside the custom Idempotent Filter.

using System.Text.Json;
using Idempotency.Api.Idempotency;
using Idempotency.Api.Services;

namespace Idempotency.Api.Filters;

internal sealed class IdempotencyFilter(ICacheService cache, int cacheTimeInMinutes = 60)
    : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Parse the Idempotence-Key header from the request
        if (!TryGetIdempotenceKey(context.HttpContext, out Guid idempotenceKey))
        {
            return Results.BadRequest("Invalid or missing Idempotence-Key header");
        }

        if (await cache.ExistsAsync(idempotenceKey.ToString()))
        {
            var cachedRequest = await cache.GetAsync<IdempotencyRequest>(idempotenceKey.ToString());

            if (cachedRequest == null)
            {
                // Cache exists but couldn't deserialize - remove corrupted entry and continue
                await cache.RemoveAsync(idempotenceKey.ToString());
            }
            else
            {
                var deserializedValue = DeserializeValue(cachedRequest.Value);
                return new IdempotentResult(cachedRequest.StatusCode, deserializedValue);
            }
        }

        object? result = await next(context);

        await CacheSuccessfulResponse(result, idempotenceKey);

        return result;
    }
}

Here’s an explanation of the above code:

  • Step 1: Validate the Idempotence-Key
     The filter checks if the request contains a valid Idempotence-Key header. If it's missing or invalid, the request is rejected with a 400 Bad Request.
  • Step 2: Check for Cached Response
     If the key exists in the cache:
  • It tries to deserialize and return the previously stored response.
  • If deserialization fails, the corrupted cache entry is removed, and the request continues as normal.
  • Step 3: Call the Actual Endpoint Logic
     👉 object? result = await next(context);
     This is where the actual endpoint is invoked. It's the core of the pipeline everything before this line is just a pre-check to decide whether to skip execution.
  • Step 4: Cache the Response
     If the endpoint processed successfully, the response is cached against the idempotency key so future identical requests return the cached result instantly.

 

Plugging the Idempotency Filter into an Endpoint

To apply the IdempotencyFilter, you can register it directly on an endpoint like this:

public static class ItemRoutes
{
    public static void MapItemRoutes(this IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("/api/items").WithTags("Items");

        group.MapPost("/", CreateItem)
            .WithName("CreateItem")
            .WithSummary("Create a new item")
            .WithDescription("Creates a new item with the provided details")
            .AddEndpointFilter<IdempotencyFilter>(); // 👈 Plug in the filter here
    }

    private static async Task<IResult> CreateItem(CreateItemDto createItemDto, IItemService itemService)
    {
        var createdItem = await itemService.CreateItemAsync(createItemDto);
        return Results.Created($"/api/items/{createdItem.Id}", createdItem);
    }
}

 

Here, MapGroup("/api/items") helps organize related routes, and .AddEndpointFilter<IdempotencyFilter>() attaches the idempotency logic to this specific POST endpoint.

 

Things to Consider

  • Client Responsibility:
     The client is responsible for generating and including a valid Idempotency-Key header with the request. The server only validates and uses it and it doesn’t generate one on the client’s behalf.
  • In-Memory vs Distributed Caching:
    If an in-memory cache like MemoryCache is used, keep in mind that it won’t work reliably in multi-instance setups (like when your app is running behind a load balancer). For proper scaling, use a shared or distributed cache like Redis.
  • Same Key with Different Request Bodies:
    If a client reuses the same Idempotency-Key but changes the request body, the request will still be ignored because the key matches an existing response. To prevent this, store a hash of the request body along with the key. On repeated requests, compare both the key and the hashed payload to ensure the content hasn't changed.

 

Conclusion

In real-world systems, things break, networks flake out, users double-tap buttons, and retries happen whether you like it or not. Idempotency isn’t just a backend best practice; it’s a safety net that keeps your application sane under pressure. By building idempotent endpoints, especially for sensitive operations like payments or order creation, you ensure that a glitchy moment doesn’t become a costly mistake. No matter the tools or stack, the principle is simple: handle it once, and make it safe to try again. Your users (and your future self) will thank you.

Read next article
Ascentic footer logo
  •  social media icon
  • ascentic_life social media icon
  • company social media icon
  • Case Studies
  • Services
  • About us
  • Careers
  • News
  • Blog

Visit us

Colombo, Sri Lanka

Onyx Building, 475/4, Sri Jayawardenepura Kotte

+94 11 2870 183

Stockholm, Sweden

Convendum, Vegagatan 14

+46 855 924448

© 2023 Ascentic AB. All Rights Reserved.

Privacy Policy