Idempotency là gì? Cách thiết kế API chống double submit và retry lỗi
Idempotency giúp backend xử lý retry, double click và timeout mà không tạo đơn hàng, giao dịch hoặc side effect bị nhân đôi. Bài viết giải thích cách thiết kế idempotency key, lưu trạng thái request và vận hành an toàn trong production.
Idempotency là cơ chế giúp một request có thể được gửi nhiều lần nhưng hệ thống chỉ tạo ra một kết quả logic duy nhất. Với backend production, đây không phải chi tiết “nice to have”. Nó là lớp bảo vệ quan trọng cho các API tạo đơn hàng, thanh toán, nạp tiền, gửi email, tạo ticket, hoặc bất kỳ thao tác nào có side effect.
Vấn đề thực tế thường bắt đầu rất bình thường: người dùng bấm nút hai lần, mobile app mất mạng rồi retry, gateway timeout nhưng server vẫn xử lý xong, hoặc job queue chạy lại sau khi worker chết giữa chừng. Nếu API không idempotent, cùng một hành động có thể tạo hai đơn hàng, trừ tiền hai lần, hoặc gửi nhiều email giống nhau.

Idempotency là gì trong backend API?
Một operation được gọi là idempotent khi chạy một lần hay nhiều lần vẫn tạo ra cùng một trạng thái cuối. Trong HTTP, GET, PUT, DELETE thường được kỳ vọng idempotent theo ngữ nghĩa. Nhưng các endpoint POST /orders, POST /payments, POST /withdrawals lại không tự nhiên idempotent, vì mỗi lần gọi thường tạo một resource hoặc side effect mới.
Để biến các endpoint kiểu này thành an toàn khi retry, client gửi thêm một Idempotency-Key. Backend dùng key đó như dấu vân tay của một ý định nghiệp vụ: “tạo đơn hàng này một lần”. Nếu request bị gửi lại với cùng key, backend không tạo thêm đơn mới mà trả về kết quả đã lưu trước đó.
Vì sao retry dễ gây lỗi production?
Trong môi trường thật, timeout không có nghĩa là request thất bại. Có thể API gateway trả timeout ở giây thứ 30, nhưng service phía sau vẫn commit database ở giây thứ 31. Client không biết điều đó, nên retry. Nếu backend chỉ nhìn request như một lệnh mới, side effect sẽ bị nhân đôi.
Các nguồn retry phổ biến gồm mobile network chập chờn, browser double submit, payment gateway retry webhook, message broker redelivery, worker crash trước khi ack, hoặc circuit breaker mở rồi đóng lại. Vì vậy, retry policy chỉ an toàn khi đi kèm idempotency strategy.
Thiết kế Idempotency-Key thế nào?
Key nên do client sinh ra cho từng ý định nghiệp vụ, ví dụ một UUID v4 cho thao tác checkout. Backend cần scope key theo user, tenant, endpoint hoặc business action. Không nên dùng key global đơn giản, vì hai user khác nhau có thể vô tình gửi cùng giá trị.
Một bản ghi idempotency thường cần các trường: key, user_id, route, request_hash, status, response_body, resource_id, expires_at. Quan trọng nhất là unique constraint ở database, ví dụ unique trên (user_id, route, key). Nếu chỉ check rồi insert ở application layer, race condition vẫn có thể tạo duplicate.

Khi retry thì trả gì?
Cách an toàn nhất là lưu response thành công đầu tiên và trả lại response đó cho các retry cùng key. Nếu request đầu tiên đang xử lý, backend có thể trả 202 Accepted, 409 Conflict, hoặc hướng client polling theo resource ID. Chọn mã nào phụ thuộc vào UX và loại API, nhưng phải nhất quán.
Nếu retry dùng cùng key nhưng payload khác, backend nên từ chối bằng 400 hoặc 409. Đây là lý do cần lưu request_hash. Idempotency key không được trở thành lối tắt để một key đại diện cho nhiều ý định khác nhau.
Pseudo-code cho endpoint tạo đơn hàng
POST /orders
Idempotency-Key: 7f6b9a9e-...
begin transaction
record = find_or_create_idempotency_key(user, route, key)
if record.completed?
return record.saved_response
if record.processing? and record.locked_by_other_request?
return 409, { error: "request_is_processing" }
ensure request_hash == record.request_hash
mark record as processing
order = create_order_once(params)
save response into record
mark record as completed
commit
return order_response
Trong triển khai thật, phần khó không nằm ở vài dòng pseudo-code mà ở transaction boundary. Nếu tạo order thành công nhưng lưu idempotency response thất bại, retry sau đó vẫn có thể tạo thêm order. Vì vậy, side effect chính và idempotency record nên nằm trong cùng transaction nếu có thể. Với side effect ngoài database như gửi email hoặc gọi payment provider, cần thêm outbox pattern hoặc provider-side idempotency.
TTL nên đặt bao lâu?
Không nên giữ idempotency key vô hạn. TTL phụ thuộc vào nghiệp vụ: checkout có thể giữ vài giờ đến vài ngày; payment hoặc ledger có thể cần lâu hơn; webhook từ bên thứ ba nên theo retry window của provider. Tuy nhiên, nếu TTL quá ngắn, retry trễ có thể vượt qua lớp bảo vệ và tạo duplicate.
Với dữ liệu nhạy cảm, không nhất thiết phải lưu toàn bộ response body. Có thể lưu resource ID và reconstruct response khi retry. Nhưng cần đảm bảo resource đó vẫn truy xuất được và response không làm lộ dữ liệu của tenant khác.
Checklist production

- Yêu cầu
Idempotency-Keycho các endpoint tạo side effect quan trọng. - Scope key theo user/tenant/route để tránh đụng key ngoài ý muốn.
- Dùng unique constraint hoặc distributed lock đúng cách, không chỉ check ở memory.
- Lưu request hash để phát hiện retry cùng key nhưng khác payload.
- Lưu trạng thái processing/completed/failed rõ ràng.
- Đảm bảo transaction không tạo resource thành công nhưng mất idempotency record.
- Định nghĩa TTL theo retry window thực tế.
- Log
idempotency_key,request_id,resource_idđể debug incident. - Viết test cho double submit, concurrent request, timeout-after-commit và worker retry.
Kết luận
Idempotency là một trong những khác biệt rõ nhất giữa API chạy demo và API chịu được production. Khi hệ thống bắt đầu có payment, order, workflow automation hoặc message queue, câu hỏi không còn là “có retry không”, mà là “retry có an toàn không”.
Nếu team của bạn đang thiết kế backend API, hãy đọc thêm REST API Design Checklist, Authentication và Authorization trong Backend và Deploy Backend lên Production Checklist để hoàn thiện chuỗi thiết kế API, bảo mật và vận hành.
Bắt đầu hành trình thành Software Engineer hôm nay
Đăng ký để đội ngũ Cole.vn liên hệ tư vấn miễn phí trong vòng 24 giờ.
Chúng tôi không spam. Thông tin của bạn được bảo mật tuyệt đối.