Web API  

Idempotency 101: What It Is and Why Your C# API Needs It

Introduction

In web development, we often assume a request sent to a server will be delivered perfectly once. However, network timeouts, accidental double-clicks, or retry logic in distributed systems often result in the same request being sent multiple times.

Idempotency is a design principle ensuring that an operation can be performed multiple times without changing the final result beyond the first successful attempt. If a client sends the same request twice, the server's state should remain as if it received only one.

Real-World Use Cases

Idempotency acts as a safety net for several critical scenarios:

  • Payment Processing: The most common use case. If a "Pay" request times out and the client retries, idempotency prevents charging the customer twice for the same transaction.

  • E-commerce Order Creation: Prevents duplicate orders and shipments when a user clicks "Place Order" repeatedly due to a slow connection.

  • Notification Systems: Ensures a user doesn't receive the same "Welcome" email or SMS multiple times if the delivery service retries a successful but unacknowledged send.

  • Inventory Management: Safeguards against subtracting stock twice for a single purchase event during a retry.

  • User Registration: Prevents creating multiple accounts for the same email if a signup form is submitted twice.

Idempotency in HTTP Methods

Standard HTTP methods have built-in idempotency expectations:

HTTP MethodIdempotent?Description
GETYesReading data doesn't change anything on the server.
PUTYesReplacing a resource with the same data results in the same state every time.
DELETEYesDeleting an already-deleted resource leaves the state unchanged (it's still gone).
POSTNoTypically creates a new resource. Multiple POSTs usually create multiple records.

Implementing Idempotency in C# Web API

To make naturally non-idempotent methods (like POST) safe, we use an Idempotency Key. This is a unique identifier (like a GUID) sent by the client in the request header.

Simple Code Example

Here is how you might handle an idempotent "Update Profile" request in an ASP.NET Core controller using Redis for fast, distributed tracking:

[HttpPost("updateProfile")]
public async Task<IActionResult> UpdateUserProfile([FromBody] UserProfileModel profile)
{
    // 1. Extract the unique Idempotency-Key from headers
    if (!Request.Headers.TryGetValue("Idempotency-Key", out var idempotencyKey))
    {
        return BadRequest("Idempotency-Key header is missing.");
    }

    // 2. Check if this key has already been processed in your cache
    var cachedResponse = await _cache.GetStringAsync(idempotencyKey);

    if (cachedResponse != null)
    {
        // 3. If it exists, return the cached successful response immediately
        return Ok(JsonSerializer.Deserialize<UserProfileModel>(cachedResponse));
    }

    // 4. Otherwise, process the request normally
    var updatedProfile = await _userService.UpdateUserProfile(profile);

    // 5. Store the key and the response with a TTL (e.g., 24 hours) to manage storage
    var cacheOptions = new DistributedCacheEntryOptions { 
        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) 
    };
    await _cache.SetStringAsync(idempotencyKey, JsonSerializer.Serialize(updatedProfile), cacheOptions);

    return Ok(updatedProfile);
}

Key Takeaways

  1. Safety First: Idempotency prevents unintended side effects like duplicate orders or double payments.

  2. Use Headers: For custom idempotency, have the client send a unique Idempotency-Key.

  3. Choose Methods Wisely: Use PUT for updates and DELETE for removals to leverage their built-in idempotent nature.

Conclusion

Building idempotent APIs is a hallmark of a senior developer mindset. It acknowledges that systems fail and networks are unreliable, but ensures your application remains consistent regardless.