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.
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.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.