Phần II: Chiến Thuật Thiết Kế (Tactical Design)

Chương 9: Các Mẫu Giao Tiếp

Model Translation, Outbox Pattern, Saga, và Process Manager trong Hệ Thống Phân Tán
Thời gian đọc: ~30 phút
Nguyên tác: Vlad Khononov (O'Reilly)
Từ khóa: Model Translation, Outbox, Dual-Write, Saga, Process Manager, Eventual Consistency
"Chỉ có dữ liệu nằm trọn vẹn bên trong ranh giới của một Aggregate mới được coi là nhất quán mạnh (strongly consistent). Mọi thứ nằm bên ngoài ranh giới đó — dù là giữa hai Aggregate hay giữa hai Bounded Context — đều là nhất quán sau (eventually consistent)." — Vlad Khononov, "Learning Domain-Driven Design"

Giới Thiệu: Mẫu Giao Tiếp Trong Hệ Thống Phân Tán

Trong các chương trước, chúng ta đã xem xét cách triển khai logic nghiệp vụ bên trong một thành phần và cách tổ chức kiến trúc nội bộ của nó. Tuy nhiên, không có phần mềm hiện đại nào tồn tại như một ốc đảo biệt lập. Trong kiến trúc hướng miền (Domain-Driven Design), các Bounded Contexts liên tục phải trao đổi thông tin, chia sẻ sự kiện và phối hợp thực hiện các quy trình nghiệp vụ xuyên suốt.

Chương này tập trung vào các mẫu thiết kế giao tiếp (Communication Patterns) — những giải pháp kỹ thuật đã được kiểm chứng qua thực tiễn giúp kết nối các thành phần một cách an toàn, tin cậy và linh hoạt, bất chấp những thách thức khắc nghiệt của môi trường mạng và hệ thống phân tán:

  • Dịch chuyển mô hình (Model Translation): Cơ chế chuyển ngữ giữa các Ngôn ngữ Toàn hiện diện (Ubiquitous Languages) khác nhau của các Bounded Context tích hợp (Open-Host Service, Anticorruption Layer).
  • Mẫu Hộp thư đi (Outbox Pattern): Giải pháp giải quyết triệt để vấn đề "Ghi kép" (Dual-write problem), đảm bảo mọi Domain Event được phát hành tin cậy và không bao giờ bị thất lạc.
  • Mẫu Saga (Saga Pattern): Quản lý các giao dịch phân tán dài hạn (Long-running distributed transactions) và cơ chế bù trừ (Compensating transactions) mà không cần giao thức khóa 2 pha (2PC).
  • Mẫu Quản lý Quy trình (Process Manager): Xây dựng các cỗ máy trạng thái (State Machines) điều phối những quy trình nghiệp vụ phức tạp có rẽ nhánh logic và tự động xử lý ngoại lệ.

Dịch Chuyển Mô Hình (Model Translation)

Trong Chương 4, chúng ta đã học rằng khi hai Bounded Context giao tiếp với nhau mà không thể áp dụng mẫu Conformist (Tuân thủ) hoặc Shared Kernel (Nhân dùng chung), một trong hai bên — hoặc cả hai — bắt buộc phải thực hiện việc chuyển đổi giữa hai mô hình nghiệp vụ khác biệt:

  • Phía Bên Cung Cấp (Upstream): Áp dụng mẫu Open-Host Service (OHS) để chuyển đổi mô hình nội bộ thành một Published Language công khai.
  • Phía Bên Tiêu Thụ (Downstream): Áp dụng mẫu Anticorruption Layer (ACL) để bảo vệ mô hình nội bộ của mình khỏi bị vấy bẩn bởi mô hình của bên ngoài.

1. Dịch Chuyển Mô Hình Phi Trạng Thái (Stateless Model Translation)

Trong dịch chuyển phi trạng thái, quá trình chuyển ngữ diễn ra "ngay tại chỗ" (on-the-fly) cho từng yêu cầu gửi đến hoặc phản hồi đi mà không cần lưu giữ thông tin trung gian nào. Logic dịch chuyển thường được đóng gói trong một đối tượng Proxy (Hình 9-1).

Dịch chuyển mô hình qua Proxy
Hình 9-1. Dịch chuyển mô hình thông qua Proxy nằm giữa hai Bounded Context

Giao Tiếp Đồng Bộ (Synchronous Communication)

Khi hai Bounded Context giao tiếp qua các giao thức đồng bộ (HTTP REST, gRPC), vị trí đặt bộ dịch chuyển phụ thuộc vào vai trò của hệ thống (Hình 9-2):

Giao tiếp đồng bộ
Hình 9-2. Giao tiếp đồng bộ: Dịch chuyển tại bên Upstream (OHS) hoặc Downstream (ACL)
  • Bên Cung Cấp (Upstream - OHS): Triển khai thông qua một API Gateway hoặc Reverse Proxy. Nó nhận các lời gọi API theo Published Language từ các client, chuyển đổi thành định dạng mà Bounded Context nội bộ hiểu được, và chuyển đổi phản hồi ngược lại. Nếu cần phục vụ nhiều thế hệ client khác nhau, API Gateway có thể phơi bày đồng thời nhiều phiên bản Published Language (v1, v2) mà không làm xáo trộn mã nguồn miền cốt lõi bên trong (Hình 9-3).
Phơi bày nhiều phiên bản Published Language
Hình 9-3. API Gateway phơi bày đồng thời các phiên bản v1, v2 của Published Language
  • Bên Tiêu Thụ (Downstream - ACL): Tầng ACL có thể được triển khai trực tiếp bên trong mã nguồn ứng dụng của Downstream (như một Adapter trong Ports & Adapters) hoặc tách thành một dịch vụ Proxy độc lập (Shared ACL Service) phục vụ chung cho nhiều Bounded Context tiêu thụ (Hình 9-4).
Tầng ACL dùng chung
Hình 9-4. Tầng Anticorruption Layer (ACL) dùng chung đóng vai trò dịch vụ trung gian

Giao Tiếp Bất Đồng Bộ (Asynchronous Communication)

Khi tích hợp bất đồng bộ qua Message Broker, bộ dịch chuyển mô hình đóng vai trò là một tiến trình lắng nghe thông điệp (Message Interceptor / Translator). Nó nhận sự kiện từ broker, chuyển đổi cấu trúc thông điệp, và chuyển tiếp thông điệp đã dịch vào hàng đợi nội bộ của Bounded Context (Hình 9-5).

Dịch chuyển mô hình trong giao tiếp bất đồng bộ
Hình 9-5. Dịch chuyển thông điệp bất đồng bộ thông qua bộ xử lý trung gian
Sự Kiện Miền Riêng Tư (Private) vs Sự Kiện Tích Hợp Công Khai (Public)

Một lỗi kiến trúc cực kỳ nghiêm trọng là phát sóng trực tiếp các Domain Events nội bộ của Aggregate ra ngoài hệ sinh thái doanh nghiệp!

Domain Events nội bộ được sinh ra để phục vụ việc bảo vệ bất biến và logic nội tại của một Bounded Context. Nếu bạn phơi bày chúng ra ngoài, bạn đang vô tình biến mô hình nội bộ của mình thành một hợp đồng công khai, biến toàn bộ hệ thống bên ngoài thành những kẻ phụ thuộc chặt chẽ (tightly coupled).

Quy tắc chuẩn: Áp dụng Published Language cho các sự kiện công khai (Integration Events). Bộ chuyển đổi sẽ lắng nghe các Domain Event nội bộ, lọc bỏ các dữ liệu mật, và đóng gói thành các Integration Events tinh gọn, chuẩn hóa phục vụ các Bounded Context khác (Hình 9-6).

Domain events trong Published Language
Hình 9-6. Dịch chuyển Domain Events nội bộ thành các sự kiện trong Published Language công khai

2. Dịch Chuyển Mô Hình Có Trạng Thái (Stateful Model Translation)

Không phải lúc nào việc dịch chuyển cũng có thể hoàn tất trong một cái chớp mắt theo tỷ lệ 1:1. Trong nhiều kịch bản phức tạp, bộ dịch chuyển bắt buộc phải duy trì trạng thái (Stateful) để tích lũy dữ liệu qua nhiều lần gọi:

Gom Nhóm Yêu Cầu Theo Lô (Batching Requests)

Giả sử hệ thống nguồn liên tục phát sinh hàng ngàn cập nhật nhỏ giọt từng giây, nhưng hệ thống đích chỉ hỗ trợ API xử lý theo lô (Bulk Import) để tránh quá tải. Bộ dịch chuyển cần lưu tạm các thay đổi vào bộ đệm (bộ nhớ hoặc CSDL tạm) và chỉ gửi đi khi đạt đủ số lượng bản ghi hoặc sau một khoảng thời gian nhất định (Hình 9-7).

Gom nhóm yêu cầu theo lô
Hình 9-7. Gom nhóm các yêu cầu đơn lẻ thành một lô (Batch) xử lý tập trung

Thống Nhất Các Sự Kiện Rời Rạc (Unifying Incoming Events)

Một hệ thống nguồn phát ra nhiều sự kiện hạt mịn (fine-grained events) riêng rẽ: HeaderUpdated, ItemAdded, DiscountApplied. Nhưng Bounded Context đích chỉ quan tâm đến một sự kiện tổng hợp duy nhất: OrderUpdated.

Bộ dịch chuyển có trạng thái sẽ lắng nghe từng sự kiện, cập nhật trạng thái tạm thời, và khi điều kiện thỏa mãn, nó phát ra một sự kiện phong phú (rich event) hoàn chỉnh duy nhất cho phía downstream (Hình 9-8 & Hình 9-9).

Thống nhất các sự kiện đến
Hình 9-8. Thống nhất chuỗi sự kiện rời rạc thành một sự kiện phong phú duy nhất
Biến đổi mô hình có trạng thái
Hình 9-9. Quá trình biến đổi mô hình có trạng thái duy trì dữ liệu qua trục thời gian

Đơn Giản Hóa Mô Hình Tích Hợp Bằng CSDL Riêng

Để hỗ trợ dịch chuyển có trạng thái mạnh mẽ, tầng Anticorruption Layer thường được trang bị cơ sở dữ liệu riêng biệt để lưu trữ bảng tra cứu (mapping tables), trạng thái tích lũy và cấu trúc dữ liệu trung gian mà không làm phiền đến CSDL chính của Bounded Context (Hình 9-10).

Đơn giản hóa mô hình tích hợp bằng ACL
Hình 9-10. Đơn giản hóa mô hình tích hợp: ACL sở hữu CSDL riêng để quản lý bảng ánh xạ và trạng thái

Mẫu Hộp Thư Đi (Outbox Pattern)

Trong kiến trúc hướng sự kiện, một thao tác nghiệp vụ điển hình thường phải làm hai việc: cập nhật dữ liệu vào cơ sở dữ liệu và phát hành Domain Event ra Message Broker.

Tuy nhiên, đây là nguồn cơn của một trong những lỗi kinh điển và nguy hiểm nhất trong lập trình hệ thống: Vấn đề Ghi Kép (Dual-Write Problem).

Vấn Đề Ghi Kép (Dual-Write Problem) & Những Lời Giải Ngây Thơ

Hãy cùng phân tích hai cách triển khai phát hành sự kiện thường gặp và lý do tại sao cả hai đều dẫn đến thảm họa:

Sai lầm 1: Phát hành sự kiện ngay bên trong Aggregate

Campaign.cs (Phát sự kiện trực tiếp từ Aggregate - SAI LẦM!)
public class Campaign
{
    private List<DomainEvent> _events;
    private IMessageBus _messageBus;

    public void Deactivate(string reason)
    {
        foreach (var l in _locations.Values)
        {
            l.Deactivate();
        }
        IsActive = false;

        var newEvent = new CampaignDeactivated(_id, reason);
        _events.Add(newEvent);

        // NGUY HIỂM TỘT CÙNG: Bắn message ngay tại đây!
        _messageBus.Publish(newEvent);
    }
}

Tại sao đoạn code trên là một thảm họa?

  • Sự kiện bị bắn ra mạng trước khi trạng thái mới của Aggregate được lưu vào CSDL. Các dịch vụ khác nhận được tin báo chiến dịch đã đóng, nhưng khi truy vấn CSDL thì chiến dịch vẫn đang mở!
  • Nếu sau đó câu lệnh commit CSDL thất bại (do xung đột tương tranh, đứt kết nối, lỗi validation), giao dịch CSDL bị Rollback. Nhưng sự kiện đã bắn đi thì không thể rút lại được! Toàn bộ các hệ thống bên ngoài đã hành động dựa trên một sự kiện ma chưa từng tồn tại!

Sai lầm 2: Phát hành sự kiện sau khi commit trong Application Service

ManagementAPI.cs (Commit xong mới bắn - VẪN CÓ THỂ MẤT DỮ LIỆU!)
public class ManagementAPI
{
    public ExecutionResult DeactivateCampaign(CampaignId id, string reason)
    {
        try
        {
            var campaign = _repository.Load(id);
            campaign.Deactivate(reason);
            _repository.CommitChanges(campaign); // Lưu CSDL thành công

            // Sau khi lưu mới gửi sự kiện
            var events = campaign.GetUnpublishedEvents();
            foreach (var e in events)
            {
                _messageBus.Publish(e); // NẾU MÁY CHỦ SẬP TẠI ĐÂY THÌ SAO?
            }
            campaign.ClearUnpublishedEvents();
        }
        catch (Exception ex) { ... }
    }
}

Dù có vẻ an toàn hơn, đoạn code trên vẫn không chịu đựng được lỗi hệ thống:

Điều gì xảy ra nếu ngay sau dòng _repository.CommitChanges(campaign), máy chủ bị cúp điện, tiến trình bị kill (OOM), hoặc Message Broker gặp sự cố mất kết nối mạng? Cơ sở dữ liệu đã cập nhật xong, nhưng sự kiện vĩnh viễn không bao giờ được gửi đi! Hệ thống rơi vào trạng thái bất nhất không thể cứu vãn.

Thuật Toán Outbox: Giải Pháp Tuyệt Đối Tin Cậy

Mẫu Hộp Thư Đi (Outbox Pattern) giải quyết triệt để vấn đề trên bằng cách biến thao tác lưu trạng thái và ghi nhận sự kiện thành MỘT GIAO DỊCH NGUYÊN TỬ DUY NHẤT (Single Atomic Transaction) trong cơ sở dữ liệu (Hình 9-11):

Mẫu Hộp Thư Đi (Outbox Pattern)
Hình 9-11. Mẫu Hộp Thư Đi: Lưu Domain Event vào bảng Outbox trong cùng Transaction CSDL, sau đó tiến trình Relay đẩy ra Message Bus

Quy Trình 4 Bước Của Outbox:

  1. Lưu nguyên tử: Trong cùng một Transaction CSDL, hệ thống cập nhật bản ghi của Aggregate và chèn đồng thời các Domain Event mới vào một bảng chuyên dụng gọi là Outbox (Hình 9-12). Hoặc cả hai cùng thành công, hoặc cả hai cùng rollback — không bao giờ có trạng thái lấp lửng!
  2. Đọc sự kiện chưa gửi: Một tiến trình chuyển tiếp độc lập (Message Relay) đọc các sự kiện chưa gửi từ bảng Outbox.
  3. Phát sóng: Message Relay phát hành sự kiện lên Message Broker.
  4. Xác nhận: Sau khi Message Broker xác nhận đã nhận tin thành công (ACK), Message Relay đánh dấu sự kiện là published = true hoặc xóa bản ghi khỏi bảng Outbox.
Bảng Outbox trong CSDL quan hệ
Hình 9-12. Bảng Outbox trong CSDL quan hệ: Lưu trữ thông điệp cần gửi kèm trạng thái published
Định dạng nhúng Outbox trong CSDL NoSQL Document
{
  "campaign-id": "364b33c3-2171-446d-b652-8e5a7b2be1af",
  "state": {
    "name": "Autumn 2017",
    "publishing-state": "DEACTIVATED"
  },
  "outbox": [
    {
      "event-id": "e12a4b88-1122",
      "type": "campaign-deactivated",
      "reason": "Goals met",
      "published": false
    }
  ]
}

Cơ Chế Chuyển Tiếp Sự Kiện (Message Relay): Pull vs Push

Tiến trình Message Relay có thể trích xuất sự kiện chưa gửi từ CSDL theo hai phương thức:

Tiêu chí Cơ Chế Kéo (Pull / Polling Publisher) Cơ Chế Đẩy (Push / Transaction Log Tailing)
Cách hoạt động Định kỳ chạy câu lệnh SELECT * FROM outbox WHERE published = false để lấy bản ghi mới. Lắng nghe trực tiếp từ Transaction Log / Write-Ahead Log (WAL) của CSDL (Change Data Capture - CDC).
Công nghệ tiêu biểu SQL Server Agent, Quartz.NET, Background Worker. Debezium, PostgreSQL WAL Replication, AWS DynamoDB Streams.
Ưu điểm Rất đơn giản, dễ cài đặt trên mọi cơ sở dữ liệu. Độ trễ gần như tức thì (near real-time), không tạo tải truy vấn quét bảng trên CSDL.
Nhược điểm Tạo tải định kỳ lên CSDL nếu polling liên tục; cần đánh chỉ mục cẩn thận trên cột published. Cấu hình phức tạp hơn, phụ thuộc vào công nghệ và quyền hạn cấp phát trên hệ quản trị CSDL.
Cam Đoan Chuyển Phát: At-Least-Once Delivery & Tính Idempotent

Mẫu Outbox đảm bảo mức độ tin cậy Chuyển phát ít nhất một lần (At-least-once delivery):

Nếu Message Relay vừa gửi sự kiện đến broker thành công nhưng bị sập trước khi kịp cập nhật published = true trong DB, khi khởi động lại nó sẽ gửi lại sự kiện đó một lần nữa!

Do đó, các bên tiêu thụ sự kiện (Consumers) BẮT BUỘC phải được thiết kế có tính Idempotent (Chống trùng lặp): kiểm tra mã event-id đã xử lý hay chưa để tránh thực hiện trùng giao dịch nghiệp vụ.

Mẫu Saga (Saga Pattern)

Trong kiến trúc hướng dịch vụ và vi dịch vụ (Microservices), mỗi Bounded Context sở hữu một cơ sở dữ liệu riêng biệt. Chúng ta không thể sử dụng các giao dịch phân tán kiểu Two-Phase Commit (2PC) vì chúng tạo ra sự tắc nghẽn nghiêm trọng, làm giảm khả năng mở rộng và khiến các hệ thống phụ thuộc chặt chẽ vào nhau.

Mẫu Saga (Saga Pattern) ra đời để quản lý các giao dịch phân tán dài hạn xuyên suốt nhiều Bounded Context mà vẫn bảo toàn tính tự trị (autonomy) của từng thành phần.

Khái Niệm Cốt Lõi: Chuỗi Giao Dịch Cục Bộ & Cơ Chế Bù Trừ

Một Saga thực chất là một chuỗi các giao dịch cục bộ (sequence of local transactions):

  • Mỗi giao dịch cục bộ chỉ cập nhật dữ liệu bên trong một Bounded Context duy nhất và phát ra một Domain Event.
  • Saga lắng nghe Domain Event đó và gửi Command kích hoạt giao dịch cục bộ tiếp theo ở Bounded Context kế cận (Hình 9-13).
Mẫu kiến trúc Saga
Hình 9-13. Mẫu Saga: Điều phối chuỗi giao dịch cục bộ giữa các dịch vụ phân tán

Giao Dịch Bù Trừ (Compensating Transactions)

Nếu một bước ở giữa quy trình thất bại (ví dụ: thẻ tín dụng bị từ chối), hệ thống không thể thực hiện câu lệnh ROLLBACK truyền thống xuyên suốt các database khác nhau được. Thay vào đó, Saga sẽ kích hoạt các Giao dịch Bù trừ (Compensating Transactions) theo chiều ngược lại để hoàn tác ngữ nghĩa của các bước đã thành công trước đó (ví dụ: hoàn tiền, giải phóng hàng giữ trong kho).

Triển Khai Mã Nguồn Chi Tiết: CampaignPublishingSaga

Hãy xem xét quy trình xuất bản một chiến dịch quảng cáo: khi chiến dịch được kích hoạt, hệ thống cần gửi thông tin quảng cáo sang dịch vụ xuất bản (Publishing Service). Khi dịch vụ xuất bản xác nhận hoặc từ chối, chiến dịch cập nhật lại trạng thái tương ứng:

CampaignPublishingSaga.cs (Tách rời trạng thái Saga và lệnh gửi đi)
public class CampaignPublishingSaga
{
    private readonly ICampaignRepository _repository;
    private readonly IList<IDomainEvent> _events;

    // Phản ứng khi có sự kiện CampaignActivated
    public void Process(CampaignActivated activated)
    {
        var campaign = _repository.Load(activated.CampaignId);
        var advertisingMaterials = campaign.GenerateAdvertisingMaterials();

        // Sinh sự kiện phát hành lệnh (CommandIssuedEvent) thay vì gọi trực tiếp mạng
        var commandIssuedEvent = new CommandIssuedEvent(
            target: Target.PublishingService,
            command: new SubmitAdvertisementCommand(activated.CampaignId, advertisingMaterials)
        );

        _events.Add(activated);
        _events.Add(commandIssuedEvent); // Lưu vào Outbox
    }

    // Phản ứng khi nhận được phản hồi PublishingConfirmed
    public void Process(PublishingConfirmed confirmed)
    {
        var commandIssuedEvent = new CommandIssuedEvent(
            target: Target.CampaignAggregate,
            command: new TrackConfirmation(confirmed.CampaignId, confirmed.ConfirmationId)
        );

        _events.Add(confirmed);
        _events.Add(commandIssuedEvent);
    }

    // Phản ứng khi nhận được phản hồi PublishingRejected (Bù trừ / Đánh dấu lỗi)
    public void Process(PublishingRejected rejected)
    {
        var commandIssuedEvent = new CommandIssuedEvent(
            target: Target.CampaignAggregate,
            command: new TrackRejection(rejected.CampaignId, rejected.RejectionReason)
        );

        _events.Add(rejected);
        _events.Add(commandIssuedEvent);
    }
}

Hãy chú ý cách thiết kế bậc thầy của tác giả: Saga không trực tiếp gọi HTTP hay gửi message ra ngoài! Thay vào đó, nó sinh ra đối tượng CommandIssuedEvent và đưa vào danh sách _events để Outbox Relay chịu trách nhiệm chuyển phát tin cậy. Điều này đảm bảo rằng ngay cả khi máy chủ sập ở bất kỳ bước nào, các Command vẫn sẽ được thực thi lại chuẩn xác và đầy đủ!

Tính Nhất Quán Trong Saga

Mặc dù Saga điều phối một giao dịch đa thành phần, trạng thái giữa các dịch vụ chỉ là Nhất quán sau (Eventually Consistent). Tuyệt đối không có tính nguyên tử tức thì (Atomic).

Cảnh Báo: Đừng Lạm Dụng Saga Để Chắp Vá Ranh Giới Aggregate

Nếu bạn nhận thấy mình đang phải viết Saga cho những thao tác nghiệp vụ đòi hỏi tính nhất quán tuyệt đối tức thì và không chấp nhận trạng thái trung gian, đó là dấu hiệu rõ ràng cho thấy bạn đã phân chia sai ranh giới Aggregate! Các dữ liệu bắt buộc phải nhất quán mạnh phải nằm chung trong cùng một Aggregate, chứ không phải giải quyết bằng Saga.

Mẫu Quản Lý Quy Trình (Process Manager)

Nhiều lập trình viên thường đánh đồng Saga và Process Manager là một. Mặc dù cả hai đều điều phối giao tiếp giữa các thành phần, chúng phục vụ hai mục đích hoàn toàn khác biệt:

Sự Khác Biệt Giữa Saga và Process Manager

Đặc tính Mẫu Saga (Saga Pattern) Mẫu Quản Lý Quy Trình (Process Manager)
Bản chất luồng Tuyến tính đơn giản (Simple linear flow): Ánh xạ 1-1 giữa sự kiện này với lệnh kia (Nếu có sự kiện A thì bắn lệnh B). Quy trình nghiệp vụ phức tạp: Có các câu lệnh điều kiện if-else, rẽ nhánh luồng, chọn đường đi thay thế, và timeout/retry.
Cách khởi tạo Khởi tạo ngầm định (Implicit): Tự động được kích hoạt khi xuất hiện một Domain Event phù hợp. Khởi tạo tường minh (Explicit): Được khởi tạo rõ ràng như một đối tượng nghiệp vụ với ID riêng biệt phục vụ một mục tiêu kinh doanh cụ thể.
Trạng thái lưu trữ Không có trạng thái hoặc trạng thái rất đơn giản. Là một Cỗ máy trạng thái (State Machine) có trạng thái bền vững được lưu vào CSDL (bằng State-based hoặc Event Sourcing).
Quy Tắc Thực Nghiệm Của Vlad Khononov

"Nếu một Saga của bạn bắt đầu xuất hiện các câu lệnh điều kiện if-else để quyết định xem bước tiếp theo nên làm gì, rất có thể bạn đang cần một Process Manager chứ không phải Saga!"

Mẫu Quản Lý Quy Trình (Process Manager)
Hình 9-14. Process Manager: Cỗ máy trạng thái trung tâm duy trì trạng thái quy trình và điều phối các bước kế tiếp

Triển Khai Thực Tế: BookingProcessManager

Hãy quan sát ví dụ điển hình về quy trình Đặt chuyến công tác cho nhân viên (Trip Booking):

  • Thuật toán định tuyến tìm đường bay tiết kiệm nhất và yêu cầu nhân viên phê duyệt.
  • Nếu nhân viên đồng ý, hệ thống tiến hành đặt vé máy bay.
  • Nếu nhân viên từ chối đường bay đó, hệ thống phải yêu cầu tìm đường bay khác và gửi quản lý trực tiếp phê duyệt.
  • Sau khi đặt vé máy bay thành công, hệ thống tiếp tục đặt phòng khách sạn. Nếu hết phòng, vé máy bay phải bị hủy.
Quy trình đặt chuyến du lịch
Hình 9-15. Quy trình đặt chuyến công tác được quản lý bởi BookingProcessManager
BookingProcessManager.cs (State Machine điều phối rẽ nhánh phức tạp)
public class BookingProcessManager
{
    private readonly IList<IDomainEvent> _events = new List<IDomainEvent>();
    private BookingId _id;
    private Destination _destination;
    private TripDefinition _parameters;
    private EmployeeId _traveler;
    private Route _route;
    private IList<Route> _rejectedRoutes = new List<Route>();
    private IRoutingService _routing;

    // Khởi tạo tường minh quy trình
    public void Initialize(Destination destination, TripDefinition parameters, EmployeeId traveler)
    {
        _destination = destination;
        _parameters = parameters;
        _traveler = traveler;
        _route = _routing.Calculate(destination, parameters);

        var routeGenerated = new RouteGeneratedEvent(_id, _route);
        var command = new CommandIssuedEvent(new RequestEmployeeApproval(_traveler, _route));

        _events.Add(routeGenerated);
        _events.Add(command);
    }

    // Xử lý khi nhân viên đồng ý đường bay
    public void Process(RouteConfirmed confirmed)
    {
        var command = new CommandIssuedEvent(new BookFlights(_route, _parameters));
        _events.Add(confirmed);
        _events.Add(command);
    }

    // Xử lý rẽ nhánh khi nhân viên TỪ CHỐI đường bay
    public void Process(RouteRejected rejected)
    {
        var command = new CommandIssuedEvent(new RequestRerouting(_traveler, _route));
        _events.Add(rejected);
        _events.Add(command);
    }

    // Xử lý khi đường bay mới được người quản lý duyệt
    public void Process(ReroutingConfirmed confirmed)
    {
        _rejectedRoutes.Add(_route);
        _route = _routing.CalculateAltRoute(_destination, _parameters, _rejectedRoutes);

        var routeGenerated = new RouteGeneratedEvent(_id, _route);
        var command = new CommandIssuedEvent(new RequestEmployeeApproval(_traveler, _route));

        _events.Add(confirmed);
        _events.Add(routeGenerated);
        _events.Add(command);
    }

    // Sau khi vé máy bay được đặt thành công, chuyển sang bước đặt khách sạn
    public void Process(FlightBooked booked)
    {
        var command = new CommandIssuedEvent(new BookHotel(_destination, _parameters));
        _events.Add(booked);
        _events.Add(command);
    }
}

Trong thực tế, Process Manager thường được mô hình hóa và triển khai như một Aggregate (có thể dùng State-based hoặc Event Sourcing). Nó có định danh riêng, quản lý trạng thái vòng đời, và kiểm tra tính hợp lệ trước khi chuyển đổi trạng thái của quy trình.

Tổng Kết Chương

Chương 9 đã trang bị cho chúng ta bộ công cụ hoàn chỉnh để giải quyết bài toán giao tiếp liên thành phần trong các hệ thống phức tạp:

Mẫu Thiết Kế Mục Đích Cốt Lõi Cơ Chế Trọng Tâm
Model Translation (OHS / ACL) Chuyển dịch ngôn ngữ và mô hình giữa các Bounded Context để tránh coupling và bảo vệ ranh giới. Proxy phi trạng thái hoặc bộ dịch chuyển có trạng thái (Batching, Unifying Events, ACL Database). Phân biệt Private Domain Events và Public Integration Events.
Outbox Pattern Đảm bảo mọi Domain Event được phát hành tin cậy ra Message Bus, giải quyết triệt để vấn đề Dual-Write. Lưu dữ liệu nghiệp vụ và sự kiện vào cùng một giao dịch ACID CSDL. Tiến trình Relay độc lập chuyển phát ra ngoài theo cơ chế At-least-once.
Saga Pattern Điều phối các giao dịch phân tán dài hạn theo luồng tuyến tính đơn giản. Chuỗi các giao dịch cục bộ độc lập kích hoạt bằng sự kiện và thực thi các giao dịch bù trừ (Compensating Transactions) khi có lỗi.
Process Manager Điều phối các quy trình nghiệp vụ phức tạp có rẽ nhánh logic và tự động chọn phương án thay thế. Cỗ máy trạng thái (State Machine) được khởi tạo tường minh, duy trì trạng thái quy trình lâu bền và xử lý các kịch bản rẽ nhánh điều kiện.

Như vậy, chúng ta đã đi trọn vẹn hành trình của Phần II: Chiến Thuật Thiết Kế (Tactical Design): từ việc mô hình hóa logic nghiệp vụ đơn giản đến phức tạp, tổ chức kiến trúc phần mềm, và kết nối giao tiếp phân tán.

Trong Chương 10: Các Nguyên Tắc Thiết Kế Thực Nghiệm (Design Heuristics), chúng ta sẽ được tiếp cận một "Bản đồ chỉ dẫn" tổng hợp cực kỳ giá trị, giúp bạn ngay lập tức đưa ra quyết định kiến trúc chuẩn xác nhất cho từng tình huống bài toán trong thực tế!

Bài Tập Ôn Tập & Thảo Luận Nghiệp Vụ

Hãy kiểm tra và củng cố tri thức về các mẫu giao tiếp qua 4 câu hỏi dưới đây kèm lời giải thích chính thức từ tác giả Vlad Khononov (Appendix B).

Câu 1: Mẫu tích hợp Bounded Context nào sau đây BẮT BUỘC phải triển khai logic biến đổi/dịch chuyển mô hình (Model transformation logic)?
A. Conformist (Tuân thủ)
B. Anticorruption Layer (ACL)
C. Open-Host Service (OHS)
D. Cả B và C
Đáp án chính xác: D (Cả B và C).
Giải thích chi tiết (Appendix B): Trong mẫu Conformist (A), bên tiêu thụ chấp nhận tuân thủ hoàn toàn mô hình của bên cung cấp nên không cần dịch chuyển mô hình. Ngược lại, cả Anticorruption Layer (B) và Open-Host Service (C) đều yêu cầu logic chuyển đổi mô hình: ACL dịch chuyển từ mô hình bên ngoài sang mô hình nội bộ của bên tiêu thụ; còn OHS dịch chuyển từ mô hình nội bộ của bên cung cấp sang ngôn ngữ xuất bản công khai (Published Language).
Câu 2: Mục tiêu cốt lõi của mẫu thiết kế Hộp Thư Đi (Outbox Pattern) là gì?
A. Tách rời hạ tầng gửi tin nhắn khỏi tầng logic nghiệp vụ của hệ thống
B. Phát hành thông điệp/sự kiện một cách tin cậy tuyệt đối (Reliably publish messages)
C. Hỗ trợ triển khai mẫu Event-Sourced Domain Model
D. Cả A và C
Đáp án chính xác: B (Phát hành thông điệp một cách tin cậy).
Giải thích chi tiết (Appendix B): Mục đích duy nhất và quan trọng nhất của Outbox Pattern là giải quyết bài toán Dual-Write, đảm bảo rằng một khi giao dịch cơ sở dữ liệu đã commit thành công thì thông điệp/Domain Event tương ứng chắc chắn sẽ được chuyển phát tới Message Broker mà không bao giờ bị thất lạc do sự cố hệ thống.
Câu 3 (Thảo luận thực tế): Ngoài việc phát hành sự kiện lên Message Bus, mẫu Outbox còn có thể được ứng dụng cho những trường hợp sử dụng nào khác trong thực tế?
Câu trả lời chính thức từ Vlad Khononov (Appendix B):
Mẫu Outbox có thể được sử dụng để triển khai việc thực thi bất đồng bộ và tin cậy của bất kỳ thành phần ngoại vi nào.

Ví dụ thực tiễn điển hình:
  • Gửi thư điện tử (Sending Email Messages) hoặc SMS: Thay vì gọi trực tiếp SMTP hay SendGrid API trong lúc xử lý đơn hàng (gặp nguy cơ nghẽn mạng hoặc lỗi API), hệ thống lưu thông tin email cần gửi vào bảng Outbox. Một tiến trình nền sẽ đọc và gửi thư tin cậy, tự động retry khi nhà mạng gặp sự cố.
  • Gửi Webhook sang hệ thống đối tác: Đảm bảo thông báo Webhook được bắn đi mà không làm chậm luồng giao dịch chính của khách hàng.
Câu 4: Những phát biểu nào sau đây là ĐÚNG về sự khác biệt giữa mẫu Saga và mẫu Process Manager?
A. Process Manager đòi hỏi phải được khởi tạo tường minh (Explicit instantiation), trong khi Saga được kích hoạt ngầm định khi có Domain Event phù hợp.
B. Ngược lại với Process Manager, Saga không bao giờ cần lưu trữ trạng thái thực thi.
C. Saga yêu cầu các thành phần tham gia phải triển khai Event Sourcing, trong khi Process Manager thì không.
D. Mẫu Process Manager phù hợp cho các quy trình nghiệp vụ phức tạp (có rẽ nhánh logic).
E. Cả A và D đều đúng.
Đáp án chính xác: E (Cả A và D đều đúng).
Giải thích chi tiết (Appendix B): Process Manager đại diện cho một tiến trình nghiệp vụ hoàn chỉnh nên được khởi tạo tường minh với định danh riêng (A đúng), và nó chứa các logic điều kiện phức tạp để định tuyến quy trình (D đúng). B sai vì Saga trong thực tế vẫn thường cần lưu trạng thái tối thiểu để theo dõi tiến độ; C sai vì cả hai mẫu đều không bắt buộc các thành phần liên quan phải dùng Event Sourcing.
Chương trước ← Chương 8: Các Mẫu Kiến Trúc
Chương tiếp theo Chương 10: Các Nguyên Tắc Thiết Kế Thực Nghiệm →