Event Sourcing nhẹ với PostgreSQL cho nghiệp vụ phức tạp
10 tháng 6, 2026
Trong nhiều hệ thống, dữ liệu hiện tại không đủ để trả lời mọi câu hỏi nghiệp vụ. Một đơn hàng không chỉ có trạng thái đã giao, mà còn cần biết đã đổi địa chỉ, hoàn tiền, hủy lần nào và ai phê duyệt. Event Sourcing giải quyết vấn đề này bằng cách lưu chuỗi sự kiện thay vì chỉ lưu trạng thái cuối. Khi kết hợp với PostgreSQL, mô hình này có thể triển khai theo hướng nhẹ, dễ kiểm tra và không cần đầu tư hạ tầng phức tạp ngay từ đầu.
Khi nào nên dùng event sourcing
Event Sourcing phù hợp với các nghiệp vụ có lịch sử quan trọng ngang bằng trạng thái hiện tại. Ví dụ, hệ thống tài chính cần biết mỗi lần cộng trừ số dư, ứng dụng vận hành cần theo dõi thay đổi cấu hình, còn nền tảng thương mại cần truy ngược hành vi đơn hàng. Nếu miền nghiệp vụ chỉ cần giá trị cuối và không cần diễn giải quá trình, việc dùng event sourcing có thể làm hệ thống rườm rà hơn cần thiết.
Ưu điểm lớn nhất là khả năng truy vết. Mỗi thay đổi được biểu diễn bằng một sự kiện có thời điểm, phiên bản và dữ liệu đi kèm. Nhờ đó, đội phát triển có thể tái tạo trạng thái tại bất kỳ thời điểm nào, xây dựng báo cáo mới từ dữ liệu cũ và phát hiện lỗi bằng cách đọc lại dòng sự kiện. Mô hình này cũng tách rõ phần ghi nghiệp vụ với phần đọc cho giao diện, giúp hệ thống tiến hóa linh hoạt hơn.
Thiết kế bảng event_store
Ở mức đơn giản, bảng lưu sự kiện cần có định danh aggregate, loại aggregate, loại sự kiện, phiên bản, thời điểm phát sinh và payload. Aggregate là đối tượng nghiệp vụ được quản lý nhất quán, chẳng hạn đơn hàng, tài khoản hoặc hợp đồng. Version giúp phát hiện xung đột khi hai tiến trình cùng muốn thay đổi một aggregate.
CREATE TABLE event_store (
id BIGSERIAL PRIMARY KEY,
aggregate_id TEXT NOT NULL,
aggregate_type TEXT NOT NULL,
event_type TEXT NOT NULL,
version INTEGER NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
metadata JSONB NOT NULL DEFAULT '{}',
payload JSONB NOT NULL,
UNIQUE (aggregate_id, version)
);
Ràng buộc UNIQUE trên aggregate_id và version đóng vai trò như khóa đồng thời lạc quan. Nếu hai yêu cầu cùng đọc phiên bản ba và cùng ghi phiên bản bốn, chỉ một giao dịch thành công. Giao dịch còn lại phải đọc lại, áp dụng sự kiện mới và thử ghi lần nữa. Với nhiều hệ thống vừa, cơ chế này đủ an toàn và tránh việc khóa hàng quá lâu.
CREATE INDEX idx_event_store_aggregate
ON event_store (aggregate_id, version);
CREATE INDEX idx_event_store_type
ON event_store (event_type, occurred_at);
Index theo aggregate hỗ trợ tái tạo trạng thái. Index theo event_type hỗ trợ projection, báo cáo và tiến trình nền đọc sự kiện mới.
Quy trình ghi sự kiện
Một luồng ghi thường đi theo bốn bước. Thứ nhất, tải tất cả sự kiện của aggregate. Thứ hai, áp dụng chúng để xây dựng trạng thái hiện tại. Thứ ba, thực hiện nghiệp vụ và tạo sự kiện mới. Thứ tư, ghi sự kiện trong cùng một transaction với mọi thay đổi liên quan.
type: order_address_changed
aggregate_id: ord_1001
version: 5
metadata:
actor: customer
request_id: req_789
Payload nên mô tả điều đã xảy ra, không nên chỉ chứa lệnh đã nhận. order_address_changed rõ nghĩa hơn change_address_command. Command là yêu cầu từ bên ngoài, còn event là kết quả đã được xác nhận bởi miền nghiệp vụ. Khi tách hai khái niệm này, hệ thống dễ kiểm thử hơn vì projection và báo cáo chỉ phụ thuộc vào sự kiện đã xảy ra.
Tái tạo trạng thái từ dòng sự kiện
Để lấy trạng thái hiện tại, ứng dụng đọc sự kiện theo thứ tự version và áp dụng từng hàm xử lý. Nếu gặp order_created thì khởi tạo đơn hàng. Nếu gặp address_changed thì cập nhật địa chỉ. Nếu gặp cancelled thì đánh dấu đã hủy. Logic này nên được viết như một reducer thuần, nhận trạng thái cũ và sự kiện mới rồi trả về trạng thái mới.
SELECT event_type, payload, version
FROM event_store
WHERE aggregate_id = :aggregate_id
ORDER BY version ASC;
Trong giai đoạn đầu, tái tạo từ đầu là đủ. Khi aggregate có hàng nghìn sự kiện, cần thêm snapshot. Snapshot lưu trạng thái tại một phiên bản nhất định, giúp quá trình tải nhanh hơn. Khi đó, ứng dụng đọc snapshot gần nhất rồi chỉ áp dụng các sự kiện sau version của snapshot.
Xử lý lỗi và đồng thời
Event Sourcing không loại bỏ lỗi đồng thời, mà làm chúng rõ ràng hơn. Nếu một giao dịch ghi thất bại do trùng version, không nên ghi đè. Thay vào đó, hãy tải lại sự kiện mới nhất, kiểm tra quy tắc nghiệp vụ và thử lại. Số lần thử nên giới hạn để tránh vòng lặp vô tận.
Cần đặc biệt chú ý idempotency. Khi client gửi lại cùng request vì mạng chập chờn, hệ thống không nên tạo hai sự kiện giống nhau. Một cách thực tế là lưu request_id trong metadata hoặc bảng riêng, rồi kiểm tra trước khi ghi. PostgreSQL hỗ trợ tốt việc này bằng transaction và ràng buộc duy nhất.
Đọc dữ liệu cho giao diện
Bảng event_store không phải lúc nào cũng là nơi truy vấn tốt cho màn hình người dùng. Ứng dụng nên xây dựng read model, tức bảng đọc được cập nhật từ sự kiện. Ví dụ, bảng order_read_model có thể lưu mã đơn, trạng thái, tổng tiền và thời gian cập nhật. Khi có sự kiện mới, worker đọc event_store rồi cập nhật bảng đọc.
CREATE TABLE order_read_model (
order_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
total_amount NUMERIC NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Kiến trúc này giúp giao diện phản hồi nhanh, còn miền nghiệp vụ vẫn giữ nguồn sự kiện làm sự thật. Nếu read model bị lỗi, có thể xóa và dựng lại từ event_store. Đây là lợi ích quan trọng khi phân tích yêu cầu báo cáo thay đổi theo thời gian.
Di chuyển và tương thích
Sự kiện là dữ liệu dài hạn, nên cần nghĩ đến tương thích. Khi thêm trường mới, hãy cho giá trị mặc định trong code xử lý. Khi đổi tên loại sự kiện, nên giữ mapping rõ ràng thay vì sửa toàn bộ dữ liệu cũ. Với thay đổi lớn, có thể viết upcaster để chuyển event cũ sang dạng mới khi đọc.
Metadata cũng cần kỷ luật. Không nên lưu token, mật khẩu hoặc dữ liệu cá nhân nhạy cảm vào payload. Nếu cần lưu thông tin người thực hiện, hãy dùng mã định danh và tách bảng bảo mật nếu bắt buộc. Event store có thể tồn tại lâu hơn nhiều module khác, vì vậy quyết định lưu hôm nay sẽ theo hệ thống trong nhiều năm.
Những cái bẫy phổ biến
Bẫy đầu tiên là biến event store thành log debug. Log phục vụ vận hành và có thể xoay vòng, còn event store là nguồn dữ liệu nghiệp vụ. Bẫy thứ hai là tạo quá nhiều sự kiện vụn vặt, khiến việc đọc lại rối và chậm. Bẫy thứ ba là bỏ qua snapshot khi aggregate phát triển nhanh. Bẫy cuối cùng là quên kiểm tra projection sau khi thay đổi logic áp dụng sự kiện.
Để bắt đầu an toàn, hãy chọn một aggregate có nghiệp vụ rõ ràng, lượng thay đổi vừa phải và yêu cầu truy vết cao. Đơn hàng, hồ sơ phê duyệt hoặc cấu hình chính sách là những ứng viên tốt. Không cần áp event sourcing cho toàn hệ thống ngay. Một mô đun chạy tốt sẽ cung cấp kinh nghiệm thực tế trước khi mở rộng.
Kết luận
Event Sourcing với PostgreSQL là lựa chọn hợp lý khi lịch sử thay đổi là tài sản nghiệp vụ. Thiết kế đúng không yêu cầu hạ tầng đồ sộ, nhưng đòi hỏi kỷ luật về phiên bản, idempotency, projection và tương thích dữ liệu. Khi được áp dụng có chọn lọc, mô hình này giúp hệ thống dễ kiểm tra, dễ truy vết và sẵn sàng cho các thay đổi nghiệp vụ trong tương lai.