Trong phát triển web/app, có một lỗi khá khó chịu nhưng rất thực tế: người dùng bấm nút “Thanh toán” hai lần, trình duyệt gửi lại request vì mạng yếu, hoặc mobile app tự retry sau timeout. Nếu backend xử lý mỗi request như một thao tác mới, hệ thống có thể tạo hai đơn hàng, trừ tiền hai lần, gửi hai email xác nhận hoặc cộng điểm thưởng sai. Đây là lúc idempotency trở nên quan trọng. Hiểu đơn giản, idempotency là khả năng cho phép cùng một thao tác được gửi nhiều lần nhưng kết quả cuối cùng vẫn như một lần thực hiện duy nhất. Ví dụ thực tế Giả sử app có API: POST /orders Client gửi thông tin giỏ hàng để tạo đơn. Nếu request đầu tiên đã tạo đơn thành công nhưng response bị mất do mạng, client không biết kết quả ra sao và gửi lại request. Nếu backend không kiểm soát, request thứ hai có thể tạo thêm một đơn giống hệt. Cách làm phổ biến là dùng Idempotency-Key. Khi client thực hiện một hành động quan trọng, nó tạo một key duy nhất, ví dụ UUID, rồi gửi kèm trong header hoặc body: Idempotency-Key: 8f4b0c9e-... Backend lưu lại key này cùng với kết quả xử lý. Nếu nhận lại request có cùng key, backend không tạo mới nữa mà trả về kết quả đã có. Nên áp dụng ở đâu? Không phải API nào cũng cần idempotency key. Hãy ưu tiên cho các thao tác có tác động lớn hoặc khó hoàn tác: - Tạo đơn hàng - Thanh toán, hoàn tiền - Đăng ký gói dịch vụ - Gửi form quan trọng - Cộng/trừ số dư, điểm thưởng - Tạo tài nguyên mà người dùng không muốn bị trùng Với các API đọc dữ liệu như GET /products, bản thân chúng thường đã an toàn hơn vì không thay đổi trạng thái. Vấn đề thường nằm ở các API ghi dữ liệu. Một thiết kế đơn giản có thể áp dụng Backend có thể tạo bảng idempotency_requests với các cột cơ bản: - key: khóa do client gửi - user_id hoặc client_id: để tránh key của người này ảnh hưởng người khác - request_hash: dấu vết của nội dung request - status: processing, completed, failed - response_body: kết quả đã trả về - created_at, expired_at Luồng xử lý: 1. Client gửi request kèm Idempotency-Key. 2. Backend kiểm tra key đã tồn tại chưa. 3. Nếu chưa có, tạo bản ghi trạng thái processing rồi xử lý nghiệp vụ. 4. Khi xử lý xong, lưu response tương ứng và chuyển sang completed. 5. Nếu request cùng key quay lại, trả về response đã lưu thay vì chạy lại logic tạo đơn/thanh toán. Một điểm quan trọng: thao tác kiểm tra và tạo bản ghi idempotency phải an toàn trước tình huống nhiều request đến cùng lúc. Nên dùng unique constraint ở database cho cặp key + user_id/client_id. Đừng chỉ kiểm tra bằng code rồi insert sau, vì hai request song song có thể cùng “thấy chưa tồn tại” và cùng tạo bản ghi. Cẩn thận với cùng key nhưng khác nội dung Nếu client dùng lại cùng Idempotency-Key cho hai request khác nhau, backend không nên âm thầm trả kết quả cũ. Cách tốt hơn là lưu request_hash. Khi nhận cùng key, backend so sánh hash của payload mới với payload cũ. Nếu khác nhau, trả lỗi rõ ràng, ví dụ: “Idempotency-Key đã được dùng cho một request khác”. Điều này giúp phát hiện bug ở client và tránh các hành vi khó đoán. Có nên lưu key mãi mãi không? Thường không cần. Có thể đặt thời hạn lưu, ví dụ vài giờ hoặc vài ngày tùy nghiệp vụ. Với thanh toán hoặc đơn hàng quan trọng, thời gian lưu nên đủ dài để bao phủ các tình huống retry, mất mạng, app mở lại sau khi treo. Sau thời hạn đó, hệ thống có thể dọn bản ghi cũ bằng background job. Lưu ý khi response đang “processing” Nếu request đầu tiên vẫn đang xử lý mà request thứ hai cùng key đến, backend có vài lựa chọn: - Trả về trạng thái 409 Conflict hoặc 202 Accepted, báo rằng request đang được xử lý. - Chờ một khoảng ngắn rồi trả kết quả nếu xử lý kịp. - Trả về thông tin để client gọi API kiểm tra trạng thái sau. Không nên để request thứ hai tự chạy lại nghiệp vụ chính. Lời khuyên áp dụng - Với nút bấm quan trọng trên frontend, vẫn nên disable sau lần bấm đầu tiên, nhưng đừng xem đó là lớp bảo vệ duy nhất. - Backend phải là nơi đảm bảo không tạo tác dụng phụ trùng lặp. - Dùng unique constraint để chống race condition. - Lưu cả kết quả thành công lẫn một số lỗi phù hợp, để retry không gây hành vi khác thường. - Ghi log theo Idempotency-Key để dễ điều tra khi có sự cố. - Đặt giới hạn kích thước và thời hạn cho dữ liệu response được lưu. Kết luận Idempotency không phải kỹ thuật quá phức tạp, nhưng nó giúp hệ thống web/app đáng tin cậy hơn rất nhiều, đặc biệt trong các luồng thanh toán, tạo đơn và cập nhật dữ liệu quan trọng. Người dùng có thể bấm lại, mạng có thể chập chờn, client có thể retry; hệ thống tốt là hệ thống dự đoán trước các tình huống đó và xử lý an toàn thay vì hy vọng mọi thứ luôn chạy đúng một lần.