Trong phát triển web/app, có một tình huống rất đời thường: người dùng bấm nút “Thanh toán”, mạng chập chờn, trình duyệt báo đang xử lý quá lâu, họ bấm lại lần nữa. Nếu hệ thống không được thiết kế cẩn thận, kết quả có thể là tạo hai đơn hàng, trừ tiền hai lần, gửi hai email xác nhận hoặc tạo hai bản ghi giống nhau. Đây là lúc khái niệm idempotency trở nên quan trọng. Nói đơn giản, một thao tác được gọi là idempotent nếu thực hiện nhiều lần vẫn cho kết quả cuối cùng như thực hiện một lần. Trong web/app, idempotency thường được dùng để đảm bảo những hành động nhạy cảm như đặt hàng, thanh toán, tạo booking, gửi yêu cầu hoàn tiền hoặc tạo tài khoản không bị xử lý trùng. Ví dụ thực tế Giả sử app thương mại điện tử 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 lỗi mạng, client có thể tự động retry. Backend nếu chỉ “nhận request là tạo đơn” thì sẽ sinh thêm một đơn mới. Cách xử lý tốt hơn là client gửi kèm một khóa định danh duy nhất cho lần thao tác đó, thường gọi là idempotency key. Ví dụ: Idempotency-Key: checkout_ user123_ 20260101_abcxyz Backend lưu lại khóa này cùng với kết quả xử lý. Nếu request có cùng key được gửi lại, hệ thống không tạo đơn mới mà trả về kết quả cũ: cùng mã đơn, cùng trạng thái, cùng thông tin cần thiết. Nên áp dụng ở đâu? Không phải API nào cũng cần idempotency key. Nên ưu tiên cho các thao tác có tác dụng phụ lớn hoặc khó đảo ngược: - Tạo đơn hàng, booking, giao dịch thanh toán. - Gửi mã giảm giá, voucher, quà tặng. - Tạo yêu cầu rút tiền, hoàn tiền, chuyển khoản. - Gửi email/SMS quan trọng nếu việc gửi trùng gây phiền toái. - Các thao tác “submit form” mà người dùng dễ bấm lại. Với các API chỉ đọc dữ liệu như GET /products thì thường không cần cơ chế này vì bản chất GET nên an toàn và không thay đổi dữ liệu. Thiết kế idempotency key như thế nào? Một số lời khuyên có thể áp dụng: - Key nên do client tạo cho từng ý định thao tác, không tạo lại mỗi lần retry. Nếu người dùng bấm “thử thanh toán lại” cho cùng một phiên xử lý, dùng lại key. Nếu họ tạo một đơn hoàn toàn mới, dùng key mới. - Key cần đủ khó trùng. Có thể dùng UUID hoặc chuỗi ngẫu nhiên có gắn ngữ cảnh như user ID, loại thao tác, thời điểm. - Backend nên ràng buộc key với user hoặc tài khoản. Không nên để người dùng A có thể dùng lại key của người dùng B. - Lưu cả trạng thái xử lý: đang xử lý, thành công, thất bại có thể retry, thất bại không nên retry. - Đặt thời hạn lưu key. Ví dụ với checkout có thể lưu vài giờ hoặc vài ngày tùy nghiệp vụ, không nhất thiết lưu mãi mãi. Một luồng xử lý gợi ý ở backend Khi nhận request có idempotency key: 1. Kiểm tra key đã tồn tại chưa. 2. Nếu chưa có, tạo bản ghi idempotency với trạng thái “processing”, sau đó thực hiện nghiệp vụ. 3. Khi xử lý xong, lưu kết quả quan trọng như order_id, status, response body tối thiểu. 4. Nếu request cùng key quay lại: - Nếu đã thành công, trả lại kết quả cũ. - Nếu đang processing, có thể trả trạng thái “đang xử lý” hoặc chờ trong thời gian ngắn. - Nếu thất bại do lỗi tạm thời, cho phép retry có kiểm soát. Điểm quan trọng là bước kiểm tra và tạo bản ghi idempotency phải an toàn trước race condition. Nếu hai request cùng key đến gần như đồng thời, database nên có unique constraint trên cặp user_id + idempotency_key. Đừng chỉ kiểm tra bằng code rồi insert, vì hai tiến trình có thể cùng thấy “chưa tồn tại” và cùng tạo dữ liệu. Những lỗi thường gặp - Chỉ disable nút trên giao diện và nghĩ là đủ. Đây là trải nghiệm tốt, nhưng không thay thế được bảo vệ ở backend. Người dùng có thể reload, retry, dùng app nhiều thiết bị hoặc request bị gửi lại bởi tầng mạng. - Dùng timestamp làm key nhưng tạo mới mỗi lần retry. Như vậy backend không biết các request thuộc cùng một ý định. - Trả kết quả khác nhau cho cùng một key. Nếu lần đầu tạo order A, lần sau cùng key lại tạo order B thì cơ chế idempotency coi như thất bại. - Không lưu trạng thái “processing”. Khi request đầu đang chạy chậm, request thứ hai có thể lọt vào và xử lý trùng. - Không giới hạn phạm vi key theo user hoặc tenant, dẫn đến rủi ro va chạm hoặc lộ thông tin. Kết hợp với frontend Frontend vẫn nên làm phần của mình: - Disable nút submit sau khi bấm. - Hiển thị trạng thái đang xử lý rõ ràng. - Tránh tự động retry vô hạn. - Nếu mất mạng, cho người dùng biết thao tác có thể đang được xử lý và cung cấp cách kiểm tra trạng thái. - Lưu idempotency key tạm thời cho phiên thao tác, đặc biệt với màn hình thanh toán hoặc form quan trọng. Kết luận Idempotency không phải là kỹ thuật hào nhoáng, nhưng rất đáng có trong các luồng nghiệp vụ quan trọng. Nó giúp hệ thống chịu được retry, lỗi mạng, double click và các tình huống bất định mà không gây hậu quả cho người dùng. Một nút bấm có thể được gửi nhiều lần, nhưng ý định của người dùng chỉ nên được xử lý một lần. Thiết kế được điều đó là dấu hiệu của một web/app trưởng thành hơn.