Idempotency giải thích vì sao retry không được nhân side-effect. Bài này: phần hay fail thầm — key phạm vi nào, sống bao lâu, lưu ở đâu.
Flag “idempotent = true” không đủ. Sai scope ≈ không có lưới.
Scope hay dùng
request_id / Idempotency-Key
— một lần bấm “Thanh toán” (client UUID)
— retry cùng key → cùng effect + response cũ
operation_id
— “refund order 123” (server sinh / derive)
— mọi path (UI, admin, job) cùng một operation
user_id + action + day
— “check-in mỗi ngày một lần”
row version / etag
— optimistic concurrency (CAS), khác dedup request
Sai scope — triệu chứng
- Key = user_id only: thao tác sau bị chặn nhầm hoặc trả response lần đầu.
- Client gen key mới mỗi retry: double charge — đúng lúc cần dedup.
- Key quá rộng theo ngày: user không refund được hai lần hợp lệ trong ngày.
- Key chỉ trong body không bắt buộc header: proxy retry khác path thủng.
TTL
Cửa sổ dedup ≥ cửa sổ retry hợp lệ + clock skew + redelivery queue (lease tối đa × attempts).
- TTL quá ngắn: hết key giữa các lần retry → double.
- TTL quá dài + scope hẹp: “không làm lại được” operation hợp lệ.
Store
In-memory Map một process — multi-instance / restart thủng
Redis / DB unique — production mặc định
Cùng DB transaction — ideal khi effect cũng ở DB đó
Lưu kết quả (status + body tóm tắt), không chỉ “đã thấy key” — retry trả đúng response cũ (Stripe-style).
Cùng key, khác payload
Key K + body A → 200 + effect
Key K + body B → 409 Conflict (không apply B im lặng)
Tránh client bug “tái sử dụng key cho operation khác”.
Message / queue
-
messageId/ business id làm dedup key khi at-least-once — exactly-once thực dụng. - Inbox pattern: outbox & inbox.
Checklist design
- Ai gen key — client hay server? Ai giữ khi retry?
- Hai path khác nhau (app + job) có cùng operation key không?
- TTL ≥ max retry window?
- Store chịu multi-instance?
- Conflict body khác → 409 có test không?
Một câu để nhớ
Idempotency không phải flag — là key + scope + store + TTL.
Sai scope = retry vẫn nhân đôi (hoặc chặn nhầm).