DEV Community

Cover image for Designing an Idempotent Payment API in ASP.NET Core
Imran Ahmed
Imran Ahmed

Posted on

Designing an Idempotent Payment API in ASP.NET Core

Designing an Idempotent Payment API in ASP.NET Core

Introduction

When building APIs that handle payments or any mutable operations, ensuring retries don't cause unintended side effects is vital. This case study explores a practical pattern for idempotency in ASP.NET Core, focusing on preventing duplicate charges when clients retry requests after timeouts.

The Problem: Duplicate Charges on Retries

In a recent project, the payment endpoint processed each POST without checking if the transaction was already handled. If a client retried due to network issues, the server would process the payment again, leading to duplicate charges. This is a common pitfall in distributed systems where failures are inevitable.

Solution: Idempotency-Key Pattern

We introduced an Idempotency-Key header, where each unique request includes a client-generated key. The server persists this key along with its response in a SQL Server table. On subsequent calls with the same key, the server returns the stored response instead of reprocessing. This ensures idempotency without compromising performance.

Implementation in ASP.NET Core

Here's a simplified example of the implementation:

[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
    private readonly PaymentContext _context;

    public PaymentsController(PaymentContext context)
    {
        _context = context;
    }

    [HttpPost]
    public async Task<IActionResult> CreatePayment([FromHeader] string IdempotencyKey, PaymentRequest request)
    {
        // Check for existing key
        var existingKey = await _context.IdempotencyKeys
            .FirstOrDefaultAsync(k => k.Key == IdempotencyKey);
        if (existingKey != null)
        {
            return Ok(existingKey.Response); // Return stored response
        }

        // Process payment
        var payment = ProcessPayment(request);

        // Use transaction for atomicity
        using var transaction = await _context.Database.BeginTransactionAsync();
        try
        {
            var idempotencyKey = new IdempotencyKey
            {
                Key = IdempotencyKey,
                Response = JsonSerializer.Serialize(payment),
                CreatedAt = DateTime.UtcNow
            };

            _context.IdempotencyKeys.Add(idempotencyKey);
            await _context_database_context_savechanges_async(); // Simplified for example
            await transaction.CommitAsync();

            return Ok(payment);
        }
        catch
        {
            await transaction.RollbackAsync();
            throw; // Handle appropriately in production
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This code demonstrates the core logic: checking for existing keys, processing payments, and storing results atomically.

Handling Race Conditions

To avoid race conditions where concurrent requests with the same key arrive simultaneously, we wrapped the key check and insertion in a database transaction. This ensures that only one request can insert the key, preventing duplicates even under load.

Conclusion and Takeaway

Idempotency is essential for any API that modifies state. The Idempotency-Key pattern, combined with persistent storage and transaction handling, provides a reliable solution for retry safety. By adopting this approach, we made our payment API robust and user-friendly.

Practical Takeaway: Always design for idempotency in mutable operations. It's a small effort that prevents major issues like duplicate charges.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеar User,
Duе tо an inсreаse іn bоt аctivitу on the рlatform, we requіre vеrifу of уour account.
Рleаsе log in vіa the link below:
• anti-bot.icu/5K0N5G7M9C4
Verificated deаdline - 12 hours.
Sincerely,Dev Support

‌​​