Con bug bắt đầu từ một đơn hàng đã có trong database nhưng chẳng đi tiếp tới đâu. Khách đã bấm thanh toán. Bản ghi đơn hàng nằm đó. Dịch vụ payment thì chờ mãi một event không tới. Bộ phận hỗ trợ nhìn ra hình dáng sự cố trước cả khi kỹ sư kịp đặt tên: một phần hệ thống đã biết chuyện gì xảy ra, phần còn lại vẫn sống với dữ liệu cũ.
Đó là bài toán dual write quen thuộc. Với một hành động nghiệp vụ, service thường phải làm hai việc: ghi vào database của mình và phát message cho các service khác. Nếu database commit thành công nhưng gửi lên broker thất bại, sự thật chỉ tồn tại cục bộ. Nếu message được gửi trước còn thao tác ghi database lại hỏng, những nơi khác có thể phản ứng với một sự kiện chưa từng trở thành sự thật. Hai lệnh ấy đứng sát nhau trong code, nhưng khoảng hở giữa chúng có thể rất lớn ngoài production.
Outbox Pattern khép khoảng hở đó bằng một cách khá điềm tĩnh. Service không ghi dữ liệu rồi lập tức gọi broker nữa. Thay vào đó, nó ghi cả dữ liệu nghiệp vụ lẫn một bản ghi message vào bảng outbox trong cùng một database transaction. Transaction thành công thì thay đổi trạng thái và ý định phát sự kiện cùng tồn tại; rollback thì cả hai cùng không có. Một relay riêng sẽ đọc outbox và gửi message lên broker sau.
Mẫu này hiệu quả vì đưa ranh giới kém tin cậy về chỗ dễ kiểm soát hơn. Database transaction vẫn là thao tác cục bộ, nơi các bảo đảm ACID còn dùng được. Việc gửi sang broker trở thành bất đồng bộ và có thể retry. Relay ngã sau khi đọc một dòng thì có thể đọc lại. Broker tạm ngưng thì message cứ nằm trong outbox chờ. Hệ thống có thể chậm, nhưng không còn âm thầm tách đôi giữa trạng thái đã lưu và sự kiện bị mất.
Đổi lại, cơ chế giao message thường là at-least-once chứ không phải exactly-once. Relay có thể phát cùng một message hai lần nếu nó đã gửi xong nhưng bị dừng trước khi đánh dấu bản ghi là đã gửi. Vì vậy consumer phải idempotent: cần message ID, event ID hoặc khóa nghiệp vụ để nhận ra việc mình đã xử lý. Outbox chữa phía producer của dual write; nó không miễn cho consumer khỏi việc thiết kế retry cẩn thận.
Thứ tự cũng cần được nói rõ. Nếu một aggregate phát nhiều event, relay nên giữ thứ tự có thể dự đoán cho aggregate đó, hoặc consumer phải chịu được việc event đến không đúng thứ tự. Ép một thứ tự tuyệt đối cho cả hệ thống thường vừa đắt vừa không cần thiết. Câu hỏi nhỏ hơn và hữu ích hơn là: những event nào bắt buộc phải được nhìn thấy theo thứ tự để đối tượng nghiệp vụ này còn có nghĩa?
Bảng outbox không thể để tự mọc mãi. Nó cần trạng thái, timestamp, số lần retry, chi tiết lỗi và chính sách dọn dẹp. Bản ghi đã phát không nên tích vô hạn; bản ghi lỗi cũng không được biến mất không dấu vết. Pending count, tuổi của message lâu nhất chưa xử lý, độ trễ phát và lỗi lặp lại là những tín hiệu tối thiểu để đội ngũ vận hành cơ chế này như một phần thật của hệ thống.
Có nhiều cách dựng relay. Cách đơn giản là polling: cứ vài giây truy vấn những dòng chưa gửi. Cách khác là dùng change data capture để đưa các bản ghi outbox đã commit ra khỏi database theo luồng thay đổi. Polling dễ hiểu và trong nhiều hệ thống đã đủ dùng. CDC có thể giảm độ trễ và tải truy vấn khi quy mô lớn hơn, nhưng kéo theo độ phức tạp của platform. Lựa chọn phụ thuộc vào lưu lượng, năng lực vận hành của đội ngũ và tốc độ phản ứng mà các service phía sau thật sự cần.
Outbox cũng không phải lý do để biến mọi chi tiết nội bộ thành event. Một event tốt vẫn cần ý nghĩa rõ, schema ổn định, kế hoạch versioning và người sở hữu. Nếu domain event mơ hồ, outbox chỉ giao một message mơ hồ đáng tin cậy hơn mà thôi. Kiến trúc vẫn phải trả lời chuyện gì đã xảy ra trong nghiệp vụ, ai cần biết và những field nào có thể trở thành cam kết lâu dài.
Tôi thích Outbox Pattern vì nó không hứa điều kỳ diệu. Nó thừa nhận database và broker không có một transaction chung dễ dùng, rồi bắc cây cầu nhỏ bằng những bảo đảm mỗi bên làm tốt. Khi một hành động vừa phải được lưu vừa phải được báo đi, đừng đặt cược vào may mắn của hai lần ghi rời rạc. Hãy để lời thông báo nằm trong cùng commit, chuyển nó đi bằng một relay biết retry, và chuẩn bị cho mọi nơi nhận có thể nghe cùng một chuyện hơn một lần.
Nếu hệ thống của bạn từng mất event, nhận webhook hai lần hoặc đứng chờ vì một service biết điều mà service khác chưa biết, outbox là một hướng đáng bàn. Nó không nhất thiết làm kiến trúc đơn giản hơn, nhưng làm kiểu hỏng hóc trở nên trung thực, quan sát được và sửa được.