Ý tưởng: ghi thay đổi nghiệp vụ và bản ghi sự kiện vào cùng một transaction DB, sau đó một tiến trình riêng đọc bảng outbox và publish.
Không còn dual write vì chỉ còn một lần commit.
BEGIN;
INSERT INTO orders (id, status) VALUES ($1, 'created');
INSERT INTO outbox (id, aggregate_id, type, payload, created_at)
VALUES ($2, $1, 'OrderCreated', $3, now());
COMMIT;Hai cách đưa outbox ra broker:
- Polling publisher — job đọc
SELECT ... WHERE published_at IS NULL ORDER BY id LIMIT n FOR UPDATE SKIP LOCKED, publish rồi đánh dấu. Đơn giản, không thêm hạ tầng, dễ debug. Đổi lại: tải đọc liên tục lên DB, độ trễ bằng chu kỳ poll, và phải tự lo chuyện nhiều instance chạy song song (SKIP LOCKEDgiải quyết). - CDC / transaction log tailing (Debezium đọc WAL của Postgres, binlog của MySQL) — không tạo tải query, độ trễ thấp, đọc đúng thứ tự commit. Đổi lại: thêm một hệ thống phải vận hành (Kafka Connect), cần quyền replication, và xử lý sự cố phức tạp hơn.
Chọn: khối lượng vừa và đội nhỏ thì polling là đủ; hệ thống nhiều sự kiện, cần độ trễ thấp và đã có Kafka thì CDC đáng giá.
Chi tiết dễ bị hỏi tiếp:
- Outbox chỉ đảm bảo at-least-once — crash sau khi publish nhưng trước khi đánh dấu sẽ gửi lại, nên consumer vẫn phải idempotent.
- Bảng outbox phải được dọn định kỳ, nếu không index sẽ ngày càng lớn và job poll chậm dần.
- Thứ tự: publish theo id tăng dần trong cùng aggregate_id, và dùng aggregate_id làm partition key để giữ trình tự phía consumer.