DDD: Phần 9 – [Series] Communication Patterns trong Domain-Driven Design

Đây là chương đặc biệt quan trọng trong kiến trúc DDD vì nó mở rộng từ nội tại một Bounded Context sang giao tiếp giữa nhiều Bounded Context – một thách thức phổ biến trong hệ thống phân tán hiện đại.

Xem lại phần 8 tại: DDD: Phần 8 – Các pattern kiến trúc trong Domain-Driven Design

Nội dung sẽ chia làm 5 phần nhỏ:

PhầnNội dung chính
1Model Translation – Stateless
2Model Translation – Stateful
3Outbox Pattern – đảm bảo publish event đáng tin cậy
4Saga Pattern – quy trình nghiệp vụ đa aggregate
5Process Manager – quy trình phức tạp có điều kiện

🧠 Phần 1: Model Translation – Dịch mô hình giữa các bounded context


🔄 Tại sao cần dịch mô hình (Model Translation)?

  • Mỗi bounded context định nghĩa mô hình nghiệp vụ riêng, sử dụng ngôn ngữ chung (ubiquitous language) riêng.
  • Khi nhiều context phải tương tác, mô hình của context này thường không tương thích với context khác.

➕ Nếu có thể hợp tác giữa các team: dùng Partnership hoặc Shared Kernel.
➖ Nếu không thể hợp tác chặt chẽ: cần có lớp dịch (translation layer) giữa hai context.


🧱 Các mẫu dịch mô hình:

Tình huốngGiải pháp
Downstream không dùng được model của upstreamDùng Anticorruption Layer (ACL)
Upstream muốn bảo vệ internal modelDùng Open Host Service (OHS)
Nhiều bên cần dùng mô hình đã dịchTạo Interchange Context (một Bounded Context chuyên để chuyển đổi mô hình)

⚙️ Hai kiểu dịch mô hình:

  1. Stateless Translation – không cần lưu trạng thái, dịch “ngay tại chỗ”
  2. Stateful Translation – dịch phức tạp hơn, cần lưu trạng thái và đôi khi là lưu trữ trung gian

📬 Stateless Translation

🔁 Dịch qua Proxy (Proxy Pattern)

  • Synchronous communication (giao tiếp đồng bộ): dịch inline trong mã hoặc tại API Gateway (như Kong, KrakenD, AWS API Gateway…).
    • Dùng để chuyển đổi giữa mô hình internal và published language.
    • Cho phép xuất bản nhiều version của API cùng lúc.
  • Asynchronous communication (giao tiếp bất đồng bộ): dùng message proxy để intercept và transform các message → gửi đến consumer.

❗ Quan trọng: không nên public domain events “raw” → hãy dịch thành published language để ẩn đi chi tiết nội bộ!


📦 Interchange Context

  • Khi một bounded context chỉ làm nhiệm vụ dịch mô hình cho nhiều nơi khác → gọi là interchange context.
  • Đây là một bounded context thực thụ (real), với logic chuyển đổi riêng biệt.

💡 Notes nhanh:

Khái niệmVai trò
ACLGiúp downstream “dịch” mô hình của upstream sang định dạng riêng
OHSUpstream công khai API theo published language, bảo vệ mô hình nội bộ
API GatewayCó thể dùng để dịch API layer mà không cần viết mã trong codebase. Dùng trong Routing, Aggregation, Auth, Throttling, Logging,…
Message ProxyDùng cho async messaging, dịch sự kiện gửi đến
Interchange ContextMột bounded context chuyên để xử lý translation

🧠 Phần 2: Model Translation có trạng thái (Stateful Translation)


🤔 Khi nào cần dịch có trạng thái?

Khi translation phức tạp hơn việc “map trực tiếp” giữa hai object:

  • Phải tổng hợp (aggregate) nhiều dữ liệu đến → để batch xử lý.
  • Phải gộp thông tin từ nhiều nguồn (multiple upstreams).
  • Phải phục vụ UI hoặc API front-end cần “tổng hợp” model (VD: Backend-for-Frontend pattern).

📦 1. Batching / Aggregation

Ví dụ:

  • Một context nhận nhiều yêu cầu nhỏ rải rác trong thời gian ngắn → cần gom lại để xử lý batch.
  • Hoặc cần gom nhiều message nhỏ → thành một message lớn gửi tiếp đi (ví dụ như hình).

➡️ Phải dùng persistent storage để ghi lại trạng thái nhận được → khi đủ điều kiện thì mới thực hiện hành động tiếp theo.

📌 Không thể dùng API Gateway cho kiểu này → vì gateway không lưu trữ được trạng thái trung gian.


⚙️ 2. Kết hợp nhiều nguồn (Multiple Source Aggregation)

Đặc biệt dùng trong mô hình Backend for Frontend (BFF):

  • UI cần dữ liệu từ nhiều context → BFF đóng vai trò aggregate và expose endpoint phù hợp cho client.

💡 Có thể tách logic tích hợp này ra thành một Anticorruption Layer phía trước bounded context chính, để tách biệt xử lý giao tiếp và xử lý nghiệp vụ.


🛠️ Triển khai thực tế:

  • Có thể tự viết hệ thống lưu trạng thái chuyển đổi.
  • Hoặc dùng tools/platform:
    • Stream Processing: Kafka Streams, AWS Kinesis, Apache Flink…
    • Batching Tools: Apache NiFi, AWS Glue, Spark…

📌 Tóm tắt nhanh:

Tình huốngCần stateful translation?
Dịch đơn giản, 1:1
Gom nhiều request thành 1
Tổng hợp từ nhiều nguồn (BFF)
Dịch dùng event/message queue✅ nếu cần nhớ trạng thái để phản ứng sau
Dùng API Gateway❌ không đủ khả năng

💡 Ứng dụng thực tế:

Trong hệ thống Microservices:

  • UI cần hiển thị dashboard → BFF context aggregate nhiều service → dùng stateful ACL.
  • Hệ thống nhận nhiều sự kiện từ thiết bị IoT → gom thành batch xử lý định kỳ → dùng batching aggregator.

🔄 Bối cảnh:

  • Các Aggregate trong DDD thường không trực tiếp gọi các thành phần bên ngoài.
  • Thay vào đó, chúng phát ra domain events → thành phần khác (subscriber) lắng nghe và phản ứng.
  • Nhưng publish domain events như thế nào cho đúng?

Sai lầm phổ biến: Publish ngay trong Aggregate

🔥 Ví dụ sai:

public void Deactivate(string reason)
{
    ...
    var newEvent = new CampaignDeactivated(_id, reason);
    _events.Append(newEvent);
    _messageBus.Publish(newEvent); // ❌ Gửi luôn sự kiện
}

😱 Hệ quả:

  1. Không nhất quán trạng thái:
    • Event có thể đến subscriber trước khi Aggregate được commit vào database.
    • Subscriber nhận được "CampaignDeactivated" → nhưng DB vẫn chưa thay đổi!
  2. Rủi ro rollback không đồng bộ:
    • Nếu commit DB thất bại (do race condition, lỗi logic, hoặc hạ tầng) → event đã được publish không thể thu hồi.

Giải pháp thứ hai – Chuyển việc publish sang Application Layer

🧪 Ví dụ:

var campaign = repository.Load(id);
campaign.Deactivate(reason);
repository.CommitChanges(campaign); // ✅ Commit DB trước

var events = campaign.GetUnpublishedEvents(); // ✅ Lấy các event chưa gửi
foreach (var e in events)
{
    _messageBus.Publish(e); // ❌ Publish sau commit
}
campaign.ClearUnpublishedEvents();

😬 Vấn đề:

  • Nếu message bus gặp sự cố hoặc server chết sau commit DB nhưng chưa publish → Event sẽ không được gửi, hệ thống không nhất quán.

Giải pháp đúng: Dùng Outbox Pattern

🧠 Phần 3: Outbox Pattern – Đảm bảo phát tán sự kiện đáng tin cậy


💣 Vấn đề thường gặp: Publish sự kiện trước khi commit DB

Sai lầm phổ biến:

_event.Append(newEvent);  
_messageBus.Publish(newEvent);

➡️ Nếu DB chưa commit mà đã publish → dữ liệu và sự kiện không khớp
➡️ Nếu transaction thất bại nhưng event đã được gửi đi → sự kiện “ma” xảy ra, không thể thu hồi.


✅ Giải pháp: Outbox Pattern

📌 Cách hoạt động:

  1. Aggregate thực hiện hành động → sinh event.
  2. Ghi event vào một bảng outbox riêng, trong cùng transaction với DB chính.
  3. Một relay process sẽ:
    • Lấy event từ outbox
    • Gửi lên message bus
    • Đánh dấu là đã gửi hoặc xóa

🧾 Dữ liệu mẫu:

{
  "campaign-id": "...",
  "state": {
    "publishing-state": "DEACTIVATED"
  },
  "outbox": [
    {
      "type": "campaign-deactivated",
      "reason": "Goals met",
      "published": false
    }
  ]
}

➡️ outbox chứa các sự kiện chưa được gửi. Khi relay chạy, nó sẽ gửi đi và cập nhật trạng thái.


🔁 Cơ chế gửi sự kiện:

CáchMô tả
PullRelay liên tục query bảng outbox tìm sự kiện mới
PushDựa trên database trigger / stream / change feed (ví dụ: DynamoDB Streams, Postgres Logical Decoding) để báo cho relay

➡️ Dù dùng cách nào thì outbox vẫn chỉ đảm bảo “at-least-once” delivery.


💥 Tình huống khó:

  • Nếu relay gửi thành công, nhưng chết trước khi update DB → sự kiện bị gửi lặp lại.
  • Do đó, subscriber cần xử lý idempotency (đảm bảo xử lý lại cũng không ảnh hưởng).

📌 Tổng kết nhanh:

Thành phầnVai trò
Outbox tableLưu trữ sự kiện trong cùng transaction
Message relayGửi sự kiện từ DB đến message bus
At-least-onceCó thể gửi lại, cần xử lý trùng lặp
Không nên publish trong AggregateVì có thể gây inconsistency nếu DB rollback

💡 Gợi ý áp dụng:

  • Trong các hệ thống microservices có event sourcing hoặc event propagation, luôn nên có outbox.
  • Có thể triển khai với các tools như:
    • Debezium (tailing transaction logs)
    • Kafka Connect
    • Custom poller chạy mỗi x giây

🧠 Phần 4: Saga Pattern – Quản lý quy trình nhiều Aggregate


📌 Vấn đề:

Trong DDD, ta giới hạn mỗi transaction trong 1 aggregate để đảm bảo tính nhất quán nội bộ.

❓ Nhưng nếu business process yêu cầu phối hợp nhiều aggregate thì sao?

➡️ Saga pattern ra đời để giải quyết:

Một long-running business process gồm nhiều bước nhỏ, mỗi bước là một transaction riêng biệt.


🧭 Cách hoạt động:

  • Saga lắng nghe các domain event, và gửi command tương ứng đến các bounded context khác.
  • Nếu có bước nào thất bại, saga sẽ gửi command bù trừ (compensating action) để rollback logic nghiệp vụ.

📑 Ví dụ: Quảng cáo được duyệt

  1. CampaignActivated → saga nhận được
  2. Saga gửi SubmitAdvertisement đến AdPublishing
  3. Nhận PublishingConfirmed → saga gửi TrackPublishingConfirmation
  4. Nếu nhận PublishingRejected → saga gửi TrackPublishingRejection

➡️ Saga không chứa trạng thái, chỉ lắng nghe và phản ứng.


🧠 Quan sát quan trọng:

❗ Một saga không đồng nghĩa với việc xử lý tất cả mọi logic trong cùng một aggregate.
kết nối nhiều aggregate với nhau theo logic business chứ không gộp chúng lại.


🔁 Saga nâng cao: Event-Sourced Saga

Khi cần ghi nhớ trạng thái (ai đã phản hồi, đã gửi gì chưa…), saga có thể được thiết kế như một event-sourced aggregate:

  • Nhận event → sinh CommandIssuedEvent (không chạy command trực tiếp)
  • Các command sẽ được outbox relay xử lý

📌 Mục tiêu: tách việc cập nhật trạng thái khỏi việc gọi hành động


✅ Ưu điểm của Saga:

Điểm mạnhLợi ích
Không cần distributed transactionMỗi bước là một transaction riêng biệt
Có thể rollback bằng logic nghiệp vụDễ dàng phục hồi trạng thái nếu lỗi
Rất phù hợp với event-driven systemCấu trúc rõ ràng và tách biệt
Hỗ trợ nhất quán cuối cùng (eventual consistency)Đúng với nguyên lý DDD

⚠️ Lưu ý khi dùng Saga:

  • Không dùng Saga để “vá lỗi” thiết kế aggregate sai.
  • Nếu nghiệp vụ cần tính nhất quán tuyệt đối, hãy xem lại cách define aggregate boundary.

💡 Gợi ý ứng dụng:

  • E-commerce: thanh toán, đặt hàng, cập nhật kho
  • Booking system: vé máy bay, khách sạn, hoàn tiền nếu lỗi
  • Quy trình duyệt tài liệu: nhiều bước phê duyệt, có thể hồi lại

🧠 Phần 5: Process Manager – Điều phối quy trình nghiệp vụ phức tạp có trạng thái


🤔 Khác gì với Saga?

SagaProcess Manager
Phản ứng (reactive): nhận event rồi phát lệnhChủ động điều phối toàn bộ quá trình
Thường là luồng đơn giản, tuyến tínhCó logic rẽ nhánh (if-else), vòng lặp, điều kiện
Được kích hoạt bởi một eventPhải được khởi tạo rõ ràng như một thực thể (Process)
Thường không cần lưu stateCần lưu trạng thái, có ID, tồn tại dài hạn

📌 Rule of thumb:

Nếu có if-else trong saga → đó là process manager 😄


📑 Ví dụ: Đặt chuyến công tác

Quy trình gồm nhiều bước:

  1. Tính toán tuyến bay → gửi cho nhân viên duyệt
  2. Nếu từ chối → gửi cho quản lý phê duyệt lại
  3. Khi OK → đặt vé
  4. Tiếp theo → đặt khách sạn
  5. Nếu khách sạn full → huỷ vé máy bay

🎯 Đây là quy trình phức tạp, không có một event duy nhất khởi tạo, cần lưu trạng thái → phải dùng Process Manager.


🏗️ Triển khai:

  • Process manager được triển khai như một aggregate:
    • ID, state, event store hoặc snapshot
    • Nhận event → sinh CommandIssuedEvent → gửi đi bằng outbox

Ví dụ:

public class BookingProcessManager
{
    private Destination _destination;
    private Route _route;
    private IList<Route> _rejectedRoutes;

    public void Initialize(...) {...}
    public void Process(RouteConfirmed evt) {...}
    public void Process(RouteRejected evt) {...}
    ...
}

➡️ Lưu trữ các bước đã xử lý, các lựa chọn bị từ chối, quyết định tiếp theo.


💡 Khi nào nên dùng Process Manager?

Dấu hiệuHành động
Nhiều bước, có điều kiện rẽ nhánh✔ Dùng process manager
Cần lưu trạng thái giữa các bước✔ Dùng process manager
Quy trình phức tạp, kéo dài (ngày, tuần)✔ Dùng process manager
Chỉ có 1-2 bước đơn giản❌ Dùng saga là đủ

📌 Tổng kết so sánh:

Tiêu chíSagaProcess Manager
Phạm viGọn, tuyến tínhLinh hoạt, có điều kiện
StateThường không cầnBắt buộc cần lưu
Khởi tạoTự động từ eventPhải tạo mới qua command
LogicReactiveChủ động điều phối

💡 Gợi ý áp dụng:

  • Ngân hàng: duyệt tín dụng qua nhiều cấp, có điều kiện
  • Du lịch: đặt combo bay + khách sạn + visa
  • Quản lý hợp đồng: từ đề nghị, duyệt, ký, triển khai → mỗi bước là event → manager điều phối

🧪 Ôn lại: Câu hỏi cuối chương

  1. Pattern nào yêu cầu dịch mô hình?
    ✅ Anticorruption Layer
    ✅ Open-Host Service
  2. Mục tiêu của Outbox pattern?
    ✅ Đảm bảo publish message đáng tin cậy
  3. Outbox chỉ dùng để publish message?
    Không → còn dùng cho bất kỳ loại side-effect nào cần đảm bảo đồng bộ với DB
  4. Saga và Process Manager khác nhau gì?
    ✅ Saga kích hoạt bởi event; PM cần khởi tạo rõ ràng
    ✅ PM dùng cho luồng phức tạp có logic
    ❌ Không đúng: “Saga bắt buộc dùng event sourcing” – KHÔNG bắt buộc
    ❌ Không đúng: “PM không bao giờ cần lưu state” – thực tế PM luôn cần lưu state

Một bình luận cho “DDD: Phần 9 – [Series] Communication Patterns trong Domain-Driven Design”

Để lại một bình luận