Dùng idempotency key do client sinh, server ghi nhớ kết quả và phát lại thay vì xử lý lại.
Luồng:
1. Client sinh key duy nhất cho mỗi ý định thanh toán (UUID v4, sinh lúc mở màn hình checkout, giữ nguyên khi retry) và gửi qua header Idempotency-Key.
2. Server nhận request, mở transaction, INSERT bản ghi khoá (user_id, key) với ràng buộc UNIQUE.
- Insert thành công → đây là lần đầu, xử lý thật, lưu lại status + body response vào bản ghi đó.
- Insert đụng unique → key đã tồn tại, đọc bản ghi cũ:
- đã có response lưu sẵn → trả nguyên response cũ (cùng status, cùng body).
- vẫn đang xử lý (request trước chưa xong) → trả 409 Conflict để client thử lại sau.
3. Đặt TTL cho key (Stripe giữ 24 giờ) rồi dọn bằng job nền.
create table idempotency_keys (
user_id bigint not null,
key text not null,
request_hash text not null,
status_code int,
response_body jsonb,
created_at timestamptz default now(),
primary key (user_id, key)
);Hai chi tiết hay bị hỏi thêm:
- Lưu request_hash (hash của body) để phát hiện client dùng lại key với payload khác — trường hợp đó phải báo lỗi, không được im lặng trả kết quả cũ.
- Ghi bản ghi khoá và ghi giao dịch phải nằm trong cùng một transaction DB, nếu không sẽ có khe hở làm tính tiền hai lần.
Khoá theo user + key chứ không chỉ theo key, để client này không đoán được key của client khác.