handling all key scenarios in a payment API using idempotency

Let’s sketch something solid without pretending to predict every edge case in the universe. You want a block of code that:


Accepts an idempotency key.


Prevents double-charging.


Handles timeout/retry.


Deals with “processing-in-progress.”


Returns the same final response for repeated calls.


Survives partial failures.



A practical pattern for payment APIs → idempotency store + status machine.


Below is a compact, production-friendly example in C# (ASP.NET Core) using Redis, covering the usual payment flow.



---


✅ Complete pattern: Idempotent Payment Endpoint


Idempotency record model


public class IdempotencyRecord

{

    public string Key { get; set; }

    public string Status { get; set; } // Pending, Completed, Failed

    public int HttpStatus { get; set; }

    public string ResponseJson { get; set; }

    public DateTime UpdatedAt { get; set; }

}



---


✅ Payment endpoint with full scenario handling


[HttpPost("pay")]

public async Task<IActionResult> ProcessPayment(

    [FromHeader(Name = "Idempotency-Key")] string key,

    [FromBody] PaymentRequest request)

{

    if (string.IsNullOrWhiteSpace(key))

        return BadRequest("Idempotency-Key header is required.");


    var record = await _store.GetAsync<IdempotencyRecord>(key);


    // ✅ Scenario 1: Request already completed — return cached response immediately

    if (record is { Status: "Completed" })

        return StatusCode(record.HttpStatus, JsonConvert.DeserializeObject(record.ResponseJson));


    // ✅ Scenario 2: Request still processing — let client retry later

    if (record is { Status: "Pending" })

        return StatusCode(409, "Payment is still being processed. Try again.");


    // ✅ Scenario 3: First call — create a pending record (locks the operation)

    var newRecord = new IdempotencyRecord

    {

        Key = key,

        Status = "Pending",

        UpdatedAt = DateTime.UtcNow

    };


    await _store.SetAsync(key, newRecord, ttl: TimeSpan.FromHours(12));


    try

    {

        // ✅ Call payment gateway (this may timeout or succeed)

        var paymentResult = await _paymentGateway.ChargeAsync(request);


        // ✅ Update success

        newRecord.Status = "Completed";

        newRecord.HttpStatus = 200;

        newRecord.ResponseJson = JsonConvert.SerializeObject(paymentResult);


        await _store.SetAsync(key, newRecord);


        return Ok(paymentResult);

    }

    catch (PaymentDeclinedException ex)

    {

        // ✅ Declined → store failure and return same failure on retries

        newRecord.Status = "Failed";

        newRecord.HttpStatus = 402;

        newRecord.ResponseJson = JsonConvert.SerializeObject(new { error = ex.Message });


        await _store.SetAsync(key, newRecord);

        return StatusCode(402, new { error = ex.Message });

    }

    catch (Exception ex)

    {

        // ✅ Unknown error → do NOT mark as failed, keep pending

        // This prevents double-charge if client retries.

        return StatusCode(500, new { error = "Temporary payment error.", detail = ex.Message });

    }

}



---


✅ What this code actually handles


✅ 1. User double-clicks “Pay”


Same idempotency key → same stored response → never double-charged.


✅ 2. Mobile network drops mid-payment


Client retries with same key →


If charge finished → same response returned


If charge still running → 409 processing


If gateway timed out but succeeded → stored success is returned → no double charge



✅ 3. Payment declined


Stored as Failed, repeated calls return the same decline message.


✅ 4. Server exception


Status stays Pending, so the client doesn’t get stuck with a false “Failed” state.


✅ 5. Race conditions


The “Pending → Completed → Failed” flow makes sure only one attempt can ever charge.



---


If you want, I can:


Add SQL Server instead of Redis


Add retry-safe gateway wrapper


Add middleware to handle idempotency globally.

Comments

Popular posts from this blog

Maxpooling vs minpooling vs average pooling

Best Practices for Storing and Loading JSON Objects from a Large SQL Server Table Using .NET Core

Generative AI - Prompting with purpose: The RACE framework for data analysis