“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/emailvới cùng email: thường mong đợi một trạng thái cuối.POST /paymentskhô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):
- First-write-wins: lần đầu body được chấp nhận; lần sau cùng key khác body →
409hoặc lỗi có mã. - 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
messageIdhoặ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ên | Rủi ro đặc thù |
|---|---|---|
| HTTP sync | Idempotency key + response cache | Timeout client → retry; server đã commit nhưng client không nhận được response |
| Queue consumer | messageId / business idempotent table | Poison message; retry storm |
| Outbox → dispatcher | Khóa theo outbox row / dedupe publish | Độ trễ publish; ordering |
6. Checklist triển khai (copy-paste khi design)
- Side-effect “nặng” là gì (insert payment, trừ kho, gửi email billing)?
- Replay window bao lâu — ai chịu trách nhiệm khi quá TTL?
- Hành vi cùng key khác body?
- Concurrency: unique key DB ở đâu?
- Observability: log
idempotency_key,request_id, outcome — không log PAN/CVV. - 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:
- Client gửi
POST /paymentsvới keyK. - 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).
- Client retry cùng
K. - 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ặc409nếu bạn muốn client phân nhánh rõ).
Quan trọng là document và test 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_idsinh 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ước | Cần idempotent với chính nó | Ghi chú |
|---|---|---|
| Validate | Có (đọc + rule) | Thường rẻ |
| Reserve | Có | Hay là chỗ race |
| Partner call | Tuỳ đối tác | Có thể cần token đối tác |
| Finalize | Có | Commit cuối |
10. Consumer message: bảng so sánh dedupe key
| Nguồn key | Ưu | Nhược |
|---|---|---|
messageId từ broker | Đơn giản | Đổi broker / replay manual đổi id |
Business key (orderId + eventType) | Ổn định semantic | Phải đảm bảo uniqueness nghiệp vụ |
| Hash payload | Tránh trùng nội dung | Payload lớn, đổi nhỏ → hash đổi |
Thực tế hay dùng business key + version hoặc messageId và 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
- Cửa sổ TTL có khớp với timeout/retry policy của client không?
- Hai tab cùng user double-click: có chặn được không?
- Migration DB có phá unique index dedupe không?
- Có test tự động cho “lần hai cùng key” không?
13. DELETE, PATCH và “idempotent theo RFC”
HTTP semantics: PUT và DELETE 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 clock | Idempotency key + server clock |
| Natural key đã chắc (invoice number) | Unique constraint business có thể đủ |
| Partner gửi trùng webhook | Dedupe 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
- Stripe API — Idempotency keys (thực tế thương mại)
- CDC và Outbox
- AWS Builder Library — idempotent consumer patterns (khái niệm chung)
- Feature flag và kiểm soát rollout — khi tách “triển khai” khỏi “commit side-effect”