Preview Bản nháp chờ review — title, nội dung, link có thể sửa trước khi bỏ nhãn.

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

Checklist design

  1. Ai gen key — client hay server? Ai giữ khi retry?
  2. Hai path khác nhau (app + job) có cùng operation key không?
  3. TTL ≥ max retry window?
  4. Store chịu multi-instance?
  5. 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).