Interview question
Implement a persisted idempotent POST
Stores an idempotency key, request hash, and response in the same transaction as the created resource.
TL;DR
Stores an idempotency key, request hash, and response in the same transaction as the created resource.
Idempotency keys, payload hashes, atomic persistence, replay behavior, races, and database constraints.
Practice the problem like a real interview: restate, reason, implement, and test.
Require Idempotency-Key for POST /orders. Reusing the key with the same payload replays the original 201 response; reusing it with different input returns 409.
Next in API Implementation Labs: If-Match Concurrency
The first request commits but its response is lost. The retry returns the same order id and does not charge or insert twice.
I resolve the caller first, hash normalized business input, and reserve the unique key inside the same transaction that creates the order. A concurrent request waits on that key, then replays the committed winner after its own insert loses the uniqueness race.
app.MapPost("/orders", async (CreateOrderRequest request, HttpContext http,
AppDbContext db, CancellationToken ct) =>
{
if (!http.Request.Headers.TryGetValue("Idempotency-Key", out var values) ||
values.Count != 1 || !Guid.TryParse(values[0], out var key))
return Results.ValidationProblem(new() {
["idempotencyKey"] = ["A GUID Idempotency-Key header is required."]
});
var userId = http.User.RequireUserId();
var hash = RequestHash.For(new { request.ProductId, request.Quantity });
await using var tx = await db.Database.BeginTransactionAsync(ct);
var reservation = IdempotencyRecord.Started(
userId, "create-order", key, hash);
db.IdempotencyRecords.Add(reservation);
try {
await db.SaveChangesAsync(ct); // Holds the unique key until this transaction ends.
} catch (DbUpdateException ex) when (
UniqueConstraints.IsViolation(ex, "UX_Idempotency_User_Operation_Key")) {
await tx.RollbackAsync(ct);
db.ChangeTracker.Clear();
var winner = await db.IdempotencyRecords.AsNoTracking().SingleAsync(
x => x.UserId == userId && x.Operation == "create-order" && x.Key == key, ct);
return Replay(winner, hash, http.Response);
}
var order = Order.Create(userId, request.ProductId, request.Quantity);
var response = new OrderResponse(order.Id, order.Status);
db.Orders.Add(order);
reservation.Complete(order.Id,
StatusCodes.Status201Created, JsonSerializer.Serialize(response));
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return Results.Created($"/orders/{order.Id}", response);
}).RequireAuthorization();
static IResult Replay(IdempotencyRecord record, byte[] requestHash, HttpResponse response)
{
if (!CryptographicOperations.FixedTimeEquals(record.RequestHash, requestHash))
return Results.Conflict(new ProblemDetails {
Title = "Idempotency key was used with different input",
Status = StatusCodes.Status409Conflict
});
response.Headers.Location = $"/orders/{record.ResourceId}";
return Results.Json(JsonSerializer.Deserialize<OrderResponse>(record.ResponseJson),
statusCode: record.StatusCode);
}
The reservation, order, and completed response commit together, so no pending key becomes visible. The unique index on (UserId, Operation, Key) serializes concurrent first use without a check-then-insert window.
Share the question with a concise Core-answer excerpt and invite other developers to add their perspective.
Help improve the interview library.
Say thanks with a standalone, one-time $5 contribution. No account required.
Aporeon is shaped by Aleksandar Tomovski, a software developer with experience on both sides of technical interviews. Content is reviewed for accuracy, natural spoken delivery, useful depth, and honest trade-offs.