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
Vấn đề
Phần tiêu đề “Vấn đề”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.
Ngôn ngữ quyết định ranh giới
Phần tiêu đề “Ngôn ngữ quyết định ranh giới”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ị |
ACL khác Published Language ở đâu
Phần tiêu đề “ACL khác Published Language ở đâu”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
ShippingJobtừ 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 đó.
Cách tìm ranh giới
Phần tiêu đề “Cách tìm ranh giới”- 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.
- 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ứ.
- Gom các chức năng gắn chặt; tách phần core khỏi supporting và generic.
- 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?
- 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
OrderPaidkhi đơn đã thanh toán theo quy tắc của Checkout.OrderPaidEventchứaorderId,recipientId,occurredAt, không chứa toàn bộCheckoutOrderhay object ORM. - Shipping tạo
ShippingJobvới trạng tháipending. Nó không biếttotalMinorhay discount. buyerIdvàrecipientIdlà 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.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Chạy demo
Phần tiêu đề “Chạy demo”npx --prefix examples ts-node examples/src/bounded-contexts/context-demo.tsOutput 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.
Khi nào tách context / khi nào không
Phần tiêu đề “Khi nào tách context / khi nào không”- 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.
Trade-offs
Phần tiêu đề “Trade-offs”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.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Checklist review một contract giữa hai context
Phần tiêu đề “Checklist review một contract giữa hai context”- 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ộ. - Contract có version và được tài liệu hóa (Published Language); đổi contract là thay đổi có chủ đích.
- Downstream có ACL dịch sang model riêng; ACL chỉ dịch, không chứa business rule hay orchestration.
- Field mang vai trò khác nhau có tên khác nhau (
buyerIdkhácrecipientId) và có test bảo vệ ngữ nghĩa. - Consumer idempotent theo event ID; event được phát cùng transaction với dữ liệu qua outbox.
- 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óm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra:
- Vì sao Shipping không nên import nguyên
CheckoutOrder? - Payment gateway bên ngoài thường là Conformist hay ACL, và điều kiện nào quyết định?
- 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?