Bỏ qua để đến nội dung
Knowledge Hub

Bounded context: cùng là "Order" nhưng checkout và shipping không cần chung model Trung cấp

Thời gian đọc ước tính: 9 phút

Một class Order dùng chung từ checkout đến kho vận. Ban đầu có total, paid, buyerId. Shipping thêm recipientId, packageWeight, carrierStatus; checkout thêm discount và payment state. Một thay đổi bên kho vận làm build checkout hỏng, hai đội phải deploy cùng nhau.

Nguyên nhân không phải tên Order sai. Cùng một tên đang chỉ hai góc nhìn nghiệp vụ khác nhau: checkout coi đơn là giao dịch cần xác nhận giá và tiền; shipping coi đơn là yêu cầu đóng gói và giao hàng. Bounded context là ranh giới trong đó một model và một ngôn ngữ có nghĩa nhất quán.

Subdomain và bounded context không phải một thứ

Phần tiêu đề “Subdomain và bounded context không phải một thứ”
Subdomain Bounded context
Thuộc về Problem space: miền nghiệp vụ có thật Solution space: model và code bạn chọn dựng
Ai quyết định Doanh nghiệp (cái đã có) Đội thiết kế (cái bạn chọn)
Phân loại Core, supporting, generic Không phân loại; có thể 1 subdomain gồm nhiều context
Ví dụ Shipping, invoicing, user accounts Model ShippingJob cùng ngôn ngữ của nó

Theo Microsoft Learn, ba loại subdomain quyết định mức đầu tư:

  • Core: tạo lợi thế cạnh tranh, đáng đầu tư model kỹ nhất.
  • Supporting: cần cho vận hành nhưng không tạo khác biệt (ví dụ invoicing).
  • Generic: bài toán ngành đã giải, nên dùng giải pháp có sẵn (ví dụ user accounts).

Mục tiêu hay gặp: mỗi subdomain được một bounded context; thực tế thường lệch, miễn là bạn biết mình đang lệch ở đâu.

Mỗi context có ubiquitous language riêng, dùng nhất quán trong hội thoại, tài liệu và code. Cùng một chữ có thể khác nghĩa giữa hai context; đó là tín hiệu của một ranh giới.

Câu hỏi Checkout Shipping
“Order” quyết định điều gì? Giá, thanh toán, xác nhận mua Chuẩn bị và giao hàng
Dữ liệu chính Buyer, amount, payment state Recipient, package, carrier state
Trạng thái paid ready-to-pack
Ai sở hữu quy tắc Checkout Shipping

Context map: các context nói chuyện với nhau thế nào

Phần tiêu đề “Context map: các context nói chuyện với nhau thế nào”

Ranh giới không biệt lập. Context map vẽ quan hệ giữa các context; mỗi quan hệ thuộc một pattern từ Evans (DDD) và các tài liệu sau đó.

Đang tải diagram…

Pattern Ý nghĩa Dùng khi
Partnership Hai đội cùng thành công hoặc cùng thất bại, phối hợp release Phụ thuộc hai chiều thật sự, đội gần nhau
Shared Kernel Chung một phần model nhỏ, thay đổi cần cả hai đồng ý Phần rất nhỏ, ổn định, có governance; tránh ở quy mô lớn
Customer-Supplier Upstream cung cấp, downstream có tiếng nói trong kế hoạch của upstream Hai đội trong cùng tổ chức, downstream cần thương lượng yêu cầu
Conformist Downstream chấp nhận model upstream, không thương lượng Upstream bên ngoài hoặc quá mạnh; chi phí dịch lớn hơn lợi ích
Anti-corruption layer (ACL) Downstream dựng lớp dịch để model của mình không bị upstream làm bẩn Upstream là legacy hoặc model khác hẳn; xem Adapter pattern
Open Host Service (OHS) Upstream công bố một API/protocol rõ ràng, có version, cho nhiều consumer Nhiều downstream cùng dùng
Published Language Định dạng dữ liệu chung, được tài liệu hóa (ví dụ OpenAPI, schema event) Đi cùng OHS
Separate Ways Không tích hợp Chi phí tích hợp lớn hơn giá trị

Hai thứ này ở hai phía khác nhau của cùng một đường nối:

  • Published Language (và OHS) là phía upstream: “đây là hợp đồng tôi công bố”. Trong demo là OrderPaidEvent.
  • ACL là phía downstream: “tôi dịch hợp đồng đó sang model của tôi”. Trong demo là phần tạo ShippingJob từ event.

Event công bố không phải ACL. ACL là code của người nhận. Theo Microsoft Learn, ACL chỉ nên làm phần dịch, không đặt business rule hay orchestration vào đó.

  1. Event storming hoặc phác thảo trên whiteboard cùng domain expert: liệt kê sự kiện nghiệp vụ và ai quan tâm đến chúng.
  2. Tìm đường nứt ngôn ngữ: chỗ cùng một chữ có hai nghĩa, hoặc hai nhóm dùng hai từ cho một thứ.
  3. Gom các chức năng gắn chặt; tách phần core khỏi supporting và generic.
  4. Kiểm tra bằng thay đổi thật: một yêu cầu phổ biến có nằm gọn trong một context không?
  5. Căn chỉnh với đội: mỗi context nên có một đội sở hữu rõ ràng. Theo Microsoft, nếu một đội phải nắm nhiều context không liên quan, hoặc một context cần phối hợp nhiều đội, hãy xem lại ranh giới hoặc cơ cấu đội (Conway’s law).

Theo Fowler, văn hóa tổ chức thường là yếu tố chi phối khi vạch ranh giới, vì ngôn ngữ khác nhau đòi model khác nhau.

Cách hoạt động: Checkout phát, Shipping dịch

Phần tiêu đề “Cách hoạt động: Checkout phát, Shipping dịch”

Đang tải diagram…

  • Checkout chỉ phát OrderPaid khi đơn đã thanh toán theo quy tắc của Checkout. OrderPaidEvent chứa orderId, recipientId, occurredAt, không chứa toàn bộ CheckoutOrder hay object ORM.
  • Shipping tạo ShippingJob với trạng thái pending. Nó không biết totalMinor hay discount.
  • buyerId và recipientId là hai vai trò khác nhau; nếu cả hai đều là string, TypeScript không bắt được nhầm lẫn, nên tên field và test phải phản ánh ngữ nghĩa.
  • Event không tự cấp quyền truy cập dữ liệu. Shipping cần địa chỉ đầy đủ thì phải thiết kế contract hoặc API riêng, tính PII và versioning.

Event hay API đồng bộ? Event hợp khi Shipping chấp nhận xử lý muộn; API đồng bộ hợp khi Checkout cần câu trả lời ngay nhưng buộc hai bên chung availability.

Với event, Shipping phải idempotent theo event ID để broker retry không tạo hai job. Phát event cùng transaction với dữ liệu cần transactional outbox.

Terminal window
npx --prefix examples ts-node examples/src/bounded-contexts/context-demo.ts

Output thật:

published: {
orderId: 'order-42',
recipientId: 'user-9',
occurredAt: '2026-10-01T00:00:00Z'
}
shipping: { orderId: 'order-42', recipientId: 'user-9', status: 'pending' }

Buyer là user-7, recipient là user-9; event chỉ mang recipient. ShippingJob không có totalMinor. Xóa recipientId khỏi event thì compiler báo lỗi ở chỗ tạo ShippingJob: contract đổi phải có chủ đích. Demo chạy trong một process, không có database hay broker.

  • Tách khi cùng một khái niệm bị hai đội kéo theo hai hướng, hoặc thay đổi của một bên làm vỡ bên kia.
  • Đừng tách quá sớm khi domain còn đang học: ranh giới sai khiến mọi workflow phải gọi chéo.
  • Gộp lại nếu hai context liên tục hỏi nhau mọi thứ; sơ đồ ban đầu không đáng bảo vệ hơn bằng chứng.

Model theo context

  • Mỗi phần giữ thuật ngữ, invariant và nhịp thay đổi của chính nó.
  • Contract nhỏ giảm tác động khi model nội bộ đổi.
  • Ranh giới rõ cho phép cân nhắc tách service bằng bằng chứng coupling.

Chi phí và rủi ro

  • Cần mapping, versioning và contract test; một số dữ liệu trông bị lặp.
  • Event hoặc API qua ranh giới thêm failure mode, retry và consistency lag.
  • Chia sai làm workflow gọi chéo quá nhiều.

Checklist review một contract giữa hai context

Phần tiêu đề “Checklist review một contract giữa hai context”
  1. Event hoặc API chỉ mang field downstream cần (như orderId, recipientId, occurredAt), không publish nguyên ORM entity hay model nội bộ.
  2. Contract có version và được tài liệu hóa (Published Language); đổi contract là thay đổi có chủ đích.
  3. Downstream có ACL dịch sang model riêng; ACL chỉ dịch, không chứa business rule hay orchestration.
  4. Field mang vai trò khác nhau có tên khác nhau (buyerId khác recipientId) và có test bảo vệ ngữ nghĩa.
  5. Consumer idempotent theo event ID; event được phát cùng transaction với dữ liệu qua outbox.
  6. Dữ liệu nhạy cảm (địa chỉ, PII) đi qua contract riêng đã tính versioning, không “tiện thể” nhét vào event.

Tự kiểm tra:

  1. Vì sao Shipping không nên import nguyên CheckoutOrder?
  2. Payment gateway bên ngoài thường là Conformist hay ACL, và điều kiện nào quyết định?
  3. Hai đội cùng sửa một context liên tục: nên xem lại ranh giới hay cơ cấu đội?