Nguyen Le PhongNguyen Le Phong

Thiết kế idempotent API

Một bài giải thích thực tế về idempotent API design: vì sao retry luôn xảy ra, duplicate request tạo rủi ro thế nào, và cách idempotency key, state transition ổn định cùng response contract rõ ràng làm distributed system bình tĩnh hơn.

Nút checkout chỉ được bấm một lần theo cảm nhận của user. Browser khựng lại, spinner quay mãi, và sau vài giây user bấm thêm lần nữa vì không thấy gì xảy ra. Ở phía sau khoảnh khắc rất đời đó, payment service nhận hai request, order service retry sau timeout, và support sau đó phải giải thích vì sao một người thấy hai khoản pending charge cho một lần mua.

Đây là lúc idempotency không còn là một từ kiến trúc khô khan mà trở thành lời hứa sản phẩm. Một operation idempotent có thể được lặp lại an toàn và vẫn để hệ thống ở cùng trạng thái mong muốn. Nếu client retry request vì network chậm, server không nên vô tình tạo order thứ hai, gửi email thứ hai, hoặc charge thẻ hai lần. Request lặp lại nên trả về cùng kết quả, hoặc báo trạng thái hiện tại mà không tạo thêm thiệt hại.

Retry không phải edge case hiếm. Nó là một phần bình thường của distributed system. Mobile network rớt. Browser refresh. Load balancer timeout. Message queue redeliver. Worker crash sau khi đã làm side effect nhưng chưa acknowledge job. User double-click. Client library tự retry. Nếu API giả định mọi request đều độc nhất chỉ vì caller có ý tốt, hệ thống đang phụ thuộc vào một thế giới không tồn tại.

Ví dụ dễ nhất là read operation. Gọi GET /orders/123 nhiều lần không nên làm order thay đổi. Phần lớn mọi người hiểu điều đó là safe. Việc khó hơn bắt đầu với operation tạo hoặc đổi state. Tạo order, bắt đầu refund, lên lịch transfer, publish message, hoặc gửi notification đều có hậu quả bên ngoài database. Những operation này cần một câu trả lời rõ: nếu cùng một ý định đến hai lần thì chuyện gì nên xảy ra?

Một công cụ phổ biến là idempotency key. Client gửi một key ổn định cùng request, thường được tạo khi user bắt đầu hành động. Server lưu key đó cùng request fingerprint và final response. Nếu cùng key đến lần nữa, server không lặp lại business action. Nó trả về kết quả đã lưu hoặc một response in-progress rõ ràng. Key biến vấn đề transport ồn ào thành một business attempt có thể nhận diện.

Nhưng key thôi chưa đủ. Server phải quyết định sameness nghĩa là gì. Nếu cùng idempotency key được dùng lại với amount khác, currency khác, hoặc order id khác, server không nên im lặng chấp nhận. Nó nên reject như một conflict. Nếu không, key chỉ là nhãn mơ hồ thay vì contract đáng tin. Idempotency tốt bảo vệ cả hai phía: nó ngăn duplicate execution, và ngăn reuse key cho một ý định khác.

State design cũng quan trọng. Một số action dễ idempotent hơn khi được model như transition thay vì command làm việc một cách mù. Đánh dấu invoice đã paid bình tĩnh hơn tạo thêm payment record mỗi khi webhook đến. Ghi nhận refund đã được request bình tĩnh hơn gọi payment provider thêm lần nữa. Một state machine có terminal state rõ thường làm duplicate handling hiện ra, thay vì bị giấu trong nhiều conditional rải rác.

External side effect cần cẩn thận hơn nữa. Nếu database commit thành công nhưng gửi email fail, retry nên tạo database row khác hay chỉ retry email? Nếu payment provider thành công nhưng local service timeout trước khi lưu kết quả, hệ thống reconcile thế nào? Những câu hỏi này là lý do các pattern như transactional outbox, provider-side idempotency key và reconciliation job có giá trị. Idempotency hiếm khi chỉ là một dòng code. Nó là một chuỗi thỏa thuận nhỏ qua nhiều boundary.

Response cũng nên nhàm và dễ đoán. Request lặp lại không nên làm caller bất ngờ bằng một shape hoàn toàn khác. Nó có thể trả lại success response ban đầu, resource representation hiện tại, hoặc status rõ như already processed. Điều quan trọng là client có thể xây retry behavior đơn giản mà không cần đoán response thứ hai có nghĩa là action thứ hai đã xảy ra hay không.

Chi phí là thật. Idempotency record cần storage, retention rule, cleanup, và đôi khi locking. Retention dài bảo vệ retry chậm nhưng lưu nhiều data hơn. Retention ngắn rẻ hơn nhưng duplicate đến muộn có thể nguy hiểm. API throughput cao phải nghĩ về contention. API nhạy cảm về security phải tránh leak việc key có tồn tại cho user khác hay không. Những chi tiết này không hào nhoáng, nhưng đó là nơi reliability trở thành thật.

Tôi thường nghĩ idempotent API là một dạng kiên nhẫn trong system design. Chúng chấp nhận rằng con người, network, worker và vendor sẽ lặp lại. Thay vì xem sự lặp lại là hành vi xấu, chúng làm sự lặp lại trở nên an toàn. Nếu product của bạn có operation nào mà làm hai lần sẽ khó giải thích, tốn tiền, hoặc làm team xấu hổ, operation đó xứng đáng có một cuộc nói chuyện về idempotency trước khi retry tiếp theo dạy bài học trong production.

Bạn thấy bài viết thế nào?