Isaac.

web

API Idempotency

Design APIs that safely handle duplicate requests.

By Emem IsaacApril 19, 20213 min read
#idempotency#api design#safety#deduplication
Share:

A Simple Analogy

Idempotent operations are like light switches. Flipping them once or 10 times gives the same result.


Why Idempotency?

  • Network failures: Retry safely without side effects
  • Client confusion: User can retry without worry
  • Consistency: No duplicate charges/records
  • Resilience: Graceful error handling
  • Best practice: Industry standard

Idempotency Key

[HttpPost("orders")]
public async Task<IActionResult> CreateOrder(
    [FromHeader(Name = "Idempotency-Key")] string idempotencyKey,
    CreateOrderRequest request)
{
    if (string.IsNullOrEmpty(idempotencyKey))
        return BadRequest("Idempotency-Key header required");
    
    // Check if we've seen this key before
    var existingOrder = await _db.Orders
        .FirstOrDefaultAsync(o => o.IdempotencyKey == idempotencyKey);
    
    if (existingOrder != null)
        return Ok(existingOrder);  // Return cached response
    
    // Create new order
    var order = new Order
    {
        IdempotencyKey = idempotencyKey,
        CustomerId = request.CustomerId,
        Amount = request.Amount,
        CreatedAt = DateTime.UtcNow
    };
    
    _db.Orders.Add(order);
    await _db.SaveChangesAsync();
    
    return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}

Safe vs Unsafe Methods

GET     - Idempotent (read-only)
HEAD    - Idempotent (read-only)
OPTIONS - Idempotent (read-only)

PUT     - Idempotent (full replacement)
DELETE  - Idempotent (removing same thing twice is safe)

POST    - NOT idempotent (creates new resources)
PATCH   - NOT idempotent (may have side effects)

Client Implementation

async function makeIdempotentRequest(method, url, data) {
  const idempotencyKey = crypto.randomUUID();
  const maxRetries = 3;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey
        },
        body: JSON.stringify(data)
      });
      
      if (response.ok) {
        return await response.json();
      }
      
      if (response.status >= 500 && i < maxRetries - 1) {
        await new Promise(resolve => 
          setTimeout(resolve, 1000 * Math.pow(2, i))
        );
        continue;
      }
      
      throw new Error(`Request failed: ${response.status}`);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => 
        setTimeout(resolve, 1000 * Math.pow(2, i))
      );
    }
  }
}

Storage Considerations

-- Table to track idempotency keys
CREATE TABLE IdempotencyKeys (
    Id INT PRIMARY KEY IDENTITY,
    IdempotencyKey NVARCHAR(MAX) NOT NULL UNIQUE,
    ResourceId INT,
    ResponseStatus INT,
    ResponseBody NVARCHAR(MAX),
    CreatedAt DATETIME DEFAULT GETUTCDATE(),
    
    INDEX IX_Key (IdempotencyKey),
    INDEX IX_CreatedAt (CreatedAt)
);

-- Cleanup old entries (e.g., after 24 hours)
DELETE FROM IdempotencyKeys 
WHERE CreatedAt < DATEADD(HOUR, -24, GETUTCDATE());

Best Practices

  1. Key generation: Use UUID v4 or similar
  2. Storage: Store key with response
  3. Timeout: Delete old entries (24 hours)
  4. Document: Specify which endpoints are idempotent
  5. Communicate: Document idempotency strategy

Related Concepts

  • Retry strategies
  • Circuit breakers
  • Error handling
  • Request deduplication

Summary

Design idempotent APIs using idempotency keys to safely handle retries and duplicate requests.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours