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
}
}
}
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)
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