“Gọi API hai lần không được tạo hai đơn” nghe đơn giản, nhưng idempotency không phải thuộc tính của HTTP verb mà là thuộc tính của hợp đồng side-effect giữa client, server và lớp lưu trữ. Middle+ cần nói rõ: gọi lại an toàn nghĩa là gì, trong cửa sổ thời gian nào, và với boundary dedupe nằm ở đâu.

Phạm vi: HTTP API + message consumer; không chứng minh toán học distributed consensus. Không bàn: chi tiết từng DB vendor.

Tham chiếu nội bộ: outbox pattern gợi ý trong CDC và Outbox (thư mục database-optimize).


1. Định nghĩa lại: idempotency theo side-effect

Một thao tác idempotent (trong phạm vi hợp đồng) nếu lặp lại cùng input có kiểm soát không làm tăng thêm tác động không mong muốn:

  • PUT /users/1/email với cùng email: thường mong đợi một trạng thái cuối.
  • POST /payments không idempotent theo mặc định — trừ khi bạn định nghĩa dedupe.

Sai lầm phổ biến: nghĩ GET luôn an toàn side-effect (thường đúng), nhưng POST luôn nguy hiểm — thực tế nhiều POST được thiết kế idempotent bằng key.


2. Idempotency key: semantics, TTL, replay window

2.1. Semantics tối thiểu

Client gửi header hoặc field (ví dụ Idempotency-Key: <uuid>):

  • Server lưu kết quả đã commit (hoặc ít nhất “đang xử lý”) theo key.
  • Request lặp với cùng key → trả cùng outcome (cùng HTTP status/body hoặc mapping rõ ràng).

2.2. TTL và cửa sổ replay

Key không thể sống mãi mãi (storage, GDPR, độ phức tạp). Bạn phải chọn replay window:

  • Quá ngắn: client retry sau TTL → tạo duplicate thật.
  • Quá dài: bảng dedupe phình, hot key.

Hợp đồng phải ghi rõ: sau TTL, client phải dùng key mới hay chấp nhận rủi ro duplicate?

2.3. “Cùng key nhưng body khác”

Đây là edge case hay gây incident. Hai hướng hợp đồng rõ ràng (chọn một và document):

  1. First-write-wins: lần đầu body được chấp nhận; lần sau cùng key khác body → 409 hoặc lỗi có mã.
  2. Key chỉ định danh tính thao tác: body phải giống (hash) — khác thì từ chối.

Không chọn → client và server hiểu khác nhau.


3. Concurrency và double-submit

Hai request cùng key đến gần như đồng thời:

  • Cần unique constraint hoặc transaction isolation phù hợp để chỉ một “owner” thực sự thực thi side-effect nặng (charge, insert).
  • Pattern: “insert placeholder processing → chỉ winner chạy pipeline”.

4. At-least-once delivery và boundary dedupe

Message queue thường là at-least-once. Consumer sẽ nhận trùng. Idempotency ở đây là:

  • Dedupe key theo messageId hoặc business key.
  • Lưu “đã xử lý” ở boundary gần side-effect (thường là DB cùng transaction với business write).

5. Bảng: endpoint đồng bộ vs job vs outbox

Bề mặtĐiểm dedupe tự nhiênRủi ro đặc thù
HTTP syncIdempotency key + response cacheTimeout client → retry; server đã commit nhưng client không nhận được response
Queue consumermessageId / business idempotent tablePoison message; retry storm
Outbox → dispatcherKhóa theo outbox row / dedupe publishĐộ trễ publish; ordering

6. Checklist triển khai (copy-paste khi design)

  1. Side-effect “nặng” là gì (insert payment, trừ kho, gửi email billing)?
  2. Replay window bao lâu — ai chịu trách nhiệm khi quá TTL?
  3. Hành vi cùng key khác body?
  4. Concurrency: unique key DB ở đâu?
  5. Observability: log idempotency_key, request_id, outcome — không log PAN/CVV.
  6. Rollback: “undo” có idempotent không, hay cần compensation saga?

7. Luồng thời gian: client timeout và “server đã xong”

Đây là kịch bản hay nhất để hiểu vì sao idempotency key gắn với HTTP response cache trên server:

  1. Client gửi POST /payments với key K.
  2. Server xử lý thành công, commit DB, nhưng response bị mất trên đường về (mobile mất mạng giây lát).
  3. Client retry cùng K.
  4. Server không được charge lần hai; phải trả cùng kết quả (hoặc mã lỗi tương thích đã document).

Nếu bước 4 trả 404 hoặc body khác lần đầu, client có thể hiểu nhầm trạng thái thanh toán và gọi support — chi phí xã hội của hợp đồng lỏng.

Giải thích thêm: idempotency không chỉ bảo vệ DB, mà bảo vệ mô hình mental của client về trạng thái giao dịch.


8. Phạm vi “cùng outcome”: status code vs body

Một số API chọn:

  • Lần đầu: 201 Created + resource.
  • Lần lặp: 200 OK + cùng resource (hoặc 409 nếu bạn muốn client phân nhánh rõ).

Quan trọng là documenttest cả hai đường. Tránh “lần đầu 201, lần sau 500 vì unique violation” — đó là bug hợp đồng, không phải bug DB.


9. Idempotency trong thế giới partial failure

Ví dụ pipeline: validate → reserve quota → call partner → finalize.

  • Nếu fail sau reserve quota: retry cùng key phải không reserve thêm; hoặc reserve idempotent theo reservation_id sinh từ key.
  • Nếu partner không idempotent: bạn phải bọc bằng mã giao dịch đối tác riêng (correlation id) hoặc chấp nhận reconciliation.

Bảng nhỏ:

BướcCần idempotent với chính nóGhi chú
ValidateCó (đọc + rule)Thường rẻ
ReserveHay là chỗ race
Partner callTuỳ đối tácCó thể cần token đối tác
FinalizeCommit cuối

10. Consumer message: bảng so sánh dedupe key

Nguồn keyƯuNhược
messageId từ brokerĐơn giảnĐổi broker / replay manual đổi id
Business key (orderId + eventType)Ổn định semanticPhải đảm bảo uniqueness nghiệp vụ
Hash payloadTránh trùng nội dungPayload lớn, đổi nhỏ → hash đổi

Thực tế hay dùng business key + version hoặc messageId lưu map sang business id để debug.


11. Observability và compliance

Log nên có: idempotency_key, request_id, tenant_id (nếu có), outcome. Không log đầy đủ thẻ/OTP. Với GDPR/right to erasure: bảng dedupe có thể chứa PII suy ra từ key — cần policy TTL và ẩn danh hoá nếu cần.


12. Câu hỏi tự kiểm (self-review) trước khi merge

  1. Cửa sổ TTL có khớp với timeout/retry policy của client không?
  2. Hai tab cùng user double-click: có chặn được không?
  3. Migration DB có phá unique index dedupe không?
  4. Có test tự động cho “lần hai cùng key” không?

13. DELETE, PATCH và “idempotent theo RFC”

HTTP semantics: PUTDELETE thường được kỳ vọng idempotent ở lớp giao thức, nhưng implementation vẫn có thể vi phạm nếu handler làm thêm side-effect (ví dụ DELETE vừa xoá vừa ghi audit append-only — vẫn ok, nhưng DELETE hai lần không được throw 500 khác nhau vô lý).

PATCH theo JSON Patch (RFC 6902, ví dụ add/replace trên field counter) hoặc payload kiểu cộng dồn có thể không idempotent theo nghĩa “lặp lại cùng request cho cùng kết quả cuối” — khác với JSON Merge Patch (RFC 7396) thuần gán giá trị đích thường dễ suy ra idempotent hơn. Đừng nhầm verb HTTP với invariant nghiệp vụ bạn đã cam kết trong tài liệu API.


14. Sequence minh họa: retry middleware

Client                API             DB
  |-- POST key=K ---->|                 |
  |                   |-- BEGIN ------->|
  |                   |-- INSERT idem ->|
  |                   |-- COMMIT ----->|
  |<- 201 (mất gói) --|                 |
  |-- POST key=K ---->|                 |
  |                   |-- SELECT idem ->|
  |<- 200 + body trùng lần đầu --------|   (hoặc 201 — miễn hợp đồng ghi rõ)

Giải thích thêm: retry middleware phải giữ nguyên body và key; nhiều bug đến từ middleware “refresh UUID” mỗi lần gửi.


15. Khi nào nên dùng idempotency key thay vì unique business constraint?

Tình huốngƯu tiên
Client là mobile app không tin cậy clockIdempotency key + server clock
Natural key đã chắc (invoice number)Unique constraint business có thể đủ
Partner gửi trùng webhookDedupe theo event_id đối tác

Đôi khi cả hai (unique business + idempotency key) để phòng nhiều kênh tạo cùng tài nguyên.


16. Khi không nên “idempotent hoá mọi thứ”

  • Thao tác đọc phân tích nặng không cần dedupe nếu không có ghi.
  • Khi chi phí dedupe (I/O, schema) lớn hơn chi phí chấp nhận duplicate + reconciliation thủ công hiếm gặp — nhưng phải chủ động chọn và ghi rõ trong thiết kế, không phải vô tình bỏ qua.

Bài tập ngắn

Thiết kế hợp đồng cho POST /bookings: key là gì, TTL 24h hay 7 ngày, timeout client retry thế nào, và DB constraint nào đảm bảo không double booking?

Mở rộng: Viết pseudo-test: “first request returns 201, second same key returns 200 with same booking_id, DB chỉ một row”.


Tóm tắt cho người vội

  • Idempotency = hợp đồng side-effect + thời gian + concurrency boundary.
  • Timeout client là lý do tồn tại của replay; server phải có bảng/ghi nhớ có kiểm soát.
  • Message at-least-once đòi dedupe ở consumer; HTTP đòi key + semantics rõ.

Đọc thêm