Bỏ qua tới nội dung
Kiên

Engine duyệt nhiều cấp mà không module nào phải biết ruột nhau

Một engine duyệt cấu hình được, dùng chung cho nhiều nghiệp vụ — ranh giới module ép bằng lint, cấu hình snapshot lúc submit, và SLA từng cấp chạy trên hàng đợi delay.

Thời gian
2026
Vai trò
Thiết kế và sở hữu — backend, handbook tích hợp, sơ đồ luồng

Công nghệ

  • NestJS 10
  • Fastify
  • TypeScript
  • TypeORM
  • PostgreSQL
  • Redis
  • BullMQ
  • Kafka
  • Jest

Vấn đề

Nhiều nghiệp vụ khác nhau đều cần chung một luồng duyệt nhiều cấp có deadline từng cấp, mỗi công ty cấu hình một kiểu. Nhưng nếu engine đọc bảng của nghiệp vụ, hoặc nghiệp vụ import ruột engine, thì sáu tháng sau không ai tách ra được nữa.

Thêm ba chuyện khó: cấu hình duyệt vẫn bị sửa trong lúc đơn đang chạy; hai người duyệt bấm cùng lúc; và sếp duyệt một phát cả trăm đơn.

Cách làm

Một class công khai duy nhất là toàn bộ bề mặt của engine. Service bên trong không export, và lint chặn import sâu.

Đường về bị đảo chiều: engine phát domain event, nghiệp vụ subscribe. Nghiệp vụ tự đăng ký nguồn log vào một registry lúc boot, để lịch sử của nó ghép được vào timeline duyệt mà engine không cần biết bảng nào. Quan hệ giữa module dùng entity_type + entity_id có index, không dùng khoá ngoại xuyên module.

Lúc submit thì snapshot toàn bộ cây workflow kèm deadline, nên sửa cấu hình sau đó không viết lại lịch sử.

Approve và reject mang optimistic lock, trả 409 nếu ghi trên bản cũ; duyệt một cấp sẽ đánh dấu các peer là ignored và mở cấp kế tiếp trong cùng transaction.

Bulk là code path riêng: một transaction, gom câu lệnh theo bảng đúng thứ tự thay vì khoá lẫn nhau; item hỏng thì bỏ qua kèm mã lỗi chứ không giết cả lô.

SLA dùng hai job delay như đồng hồ báo thức, không phải nguồn sự thật: job nổ thì đọc lại DB, cấp đã xử lý rồi thì không làm gì — nên không bao giờ phải huỷ job. Id job tất định nên reschedule không đẻ mail trùng. Redis chết thì mất nhắc nhở nhưng không chặn duyệt.

Ranh giới của failure domain này đáng vẽ hẳn ra:

approve / reject quyết trong transaction của DB sau commit — dispatch fail-soft commit mail kèm đúng danh sách người nhận engine sở hữu, không ai phải đoán nhắc SLA — job delay trên Redis chỉ là đồng hồ, không phải nguồn sự thật Redis chết: mất nhắc nhở — duyệt không bao giờ bị chặn job nổ thì đọc lại DB, cấp đã được xử lý rồi thì đứng yên — không bao giờ phải huỷ job
Mọi thứ bên phải vạch commit đều fail-soft: hỏng cũng không undo được gì bên trái. DB vẫn là nguồn sự thật duy nhất — job nhắc nổ thì đọc lại DB chứ không tin hàng đợi.

Kết quả

50 file TypeScript đứng sau một class gateway duy nhất, 11 mã lỗi có tài liệu, 7 sơ đồ luồng, và 18 user story được viết ngược lại từ code đã ship.

Notification có chủ sở hữu rõ ràng: engine sở hữu toàn bộ mail thuộc luồng duyệt và trả về đúng danh sách người nhận để không ai phải đoán. Mọi dispatch chạy sau commit và fail-soft — mail lỗi không bao giờ undo được một lần duyệt.