Chương 13. Bounded Context Bằng Ngôn Ngữ Dễ Hiểu
Ở chương trước, ta nói về domain: thế giới nghiệp vụ mà hệ thống phục vụ.
Nhưng khi hệ thống lớn dần, ta sẽ gặp một vấn đề rất khó chịu:
> Cùng một từ, nhưng ở mỗi khu vực của hệ thống lại có nghĩa khác nhau.
Ví dụ:
- "Đơn hàng" trong mắt khách hàng là thứ họ vừa đặt.
- "Đơn hàng" trong mắt bếp là danh sách món cần làm.
- "Đơn hàng" trong mắt kế toán là doanh thu, thuế, hoàn tiền.
- "Đơn hàng" trong mắt giao hàng là địa chỉ, số điện thoại, trạng thái giao.
- "Đơn hàng" trong mắt chăm sóc khách hàng là lịch sử khiếu nại, đổi trả, ghi chú.
Nếu ta cố nhét tất cả nghĩa đó vào một model Order duy nhất, model này sẽ phình to, rối, khó sửa, và cuối cùng không còn ai hiểu rõ nó đại diện cho cái gì.
Đây là lúc ta cần Bounded Context.
Nói đơn giản:
> Bounded Context là một khu vực trong hệ thống nơi các từ ngữ, mô hình và quy tắc có một ý nghĩa rõ ràng, nhất quán.
Nó không phải một khái niệm để làm hệ thống trông cao siêu hơn.
Nó là cách để tránh việc cả công ty tranh luận mãi về một chữ như "user", "order", "product", "payment", "status", nhưng mỗi người lại đang hiểu theo một kiểu khác nhau.
---
13.1. Vấn đề thật: một model không thể gánh mọi ý nghĩa
Khi mới làm hệ thống, ta thường thích có một model dùng chung.
Ví dụ trong website bán bánh:
Order
- id
- customer_id
- items
- total_price
- status
- payment_status
- delivery_status
- kitchen_status
- refund_status
- invoice_id
- coupon_id
- support_note
- cancel_reason
- tax_amount
- delivery_address
- ...
Ban đầu nhìn cũng hợp lý.
Đơn hàng thì đúng là có khách, có bánh, có tiền, có giao hàng.
Nhưng càng làm, model này càng trở thành một cái túi đựng mọi thứ.
Bếp cần thêm:
- Thời gian bắt đầu làm bánh.
- Thợ nào phụ trách.
- Bánh nào cần trang trí riêng.
- Ghi chú dị ứng.
- Thời hạn phải hoàn thành.
Kế toán cần thêm:
- Số hóa đơn.
- Thuế.
- Phí nền tảng.
- Phí thanh toán.
- Trạng thái đối soát.
- Khoản hoàn tiền.
Giao hàng cần thêm:
- Shipper.
- Lộ trình.
- Số lần gọi khách.
- Lý do giao thất bại.
- Bằng chứng giao hàng.
Chăm sóc khách hàng cần thêm:
- Lý do khiếu nại.
- Lịch sử chat.
- Mức độ ưu tiên.
- Ai đang xử lý.
- Hứa hẹn bồi thường gì.
Nếu tất cả đều dùng một Order, mỗi thay đổi nhỏ ở một bộ phận có thể ảnh hưởng đến toàn hệ thống.
Lúc này vấn đề không chỉ là code dài.
Vấn đề là mô hình đã mất nghĩa.
---
13.2. Ví dụ quán bánh: cùng là "bánh" nhưng không cùng nghĩa
Ta lấy ví dụ từ quán bánh.
Cùng một chữ "bánh", nhưng mỗi nơi quan tâm đến một khía cạnh khác nhau.
Với khách hàng, "bánh" là sản phẩm để mua:
- Tên bánh.
- Hình ảnh.
- Giá.
- Mô tả.
- Đánh giá.
- Có còn bán không.
Với bếp, "bánh" là công thức và công việc sản xuất:
- Nguyên liệu.
- Quy trình làm.
- Thời gian nướng.
- Nhiệt độ.
- Người phụ trách.
- Dụng cụ cần dùng.
Với kho, "bánh" hoặc nguyên liệu liên quan đến tồn kho:
- Còn bao nhiêu nguyên liệu.
- Hạn sử dụng.
- Nhà cung cấp.
- Lô nhập hàng.
- Mức tồn tối thiểu.
Với kế toán, "bánh" là mặt hàng có giá vốn và doanh thu:
- Giá bán.
- Giá vốn.
- Thuế.
- Biên lợi nhuận.
- Mã hạch toán.
Nếu ta hỏi: "Bánh có những thuộc tính gì?", câu trả lời phụ thuộc vào việc ta đang đứng ở đâu.
Không có một câu trả lời duy nhất cho toàn hệ thống.
Bounded Context giúp ta nói:
Trong Catalog Context:
Bánh là sản phẩm để khách xem và mua.
Trong Kitchen Context:
Bánh là món cần sản xuất theo công thức.
Trong Inventory Context:
Bánh liên quan đến nguyên liệu và tồn kho.
Trong Accounting Context:
Bánh là mặt hàng tạo doanh thu và chi phí.
Cùng một vật trong đời thật, nhưng trong phần mềm có thể cần nhiều mô hình khác nhau.
Điều này không sai.
Ngược lại, đây thường là dấu hiệu của thiết kế trưởng thành hơn.
---
13.3. Bounded Context là gì?
Ta tách cụm từ này ra:
- Bounded: có ranh giới.
- Context: ngữ cảnh.
Bounded Context là ranh giới trong đó một mô hình có nghĩa rõ ràng.
Ví dụ:
Trong ngữ cảnh bán hàng:
Orderlà đơn khách đặt.Customerlà người mua.Productlà thứ được bán.Pricelà số tiền khách phải trả.
Trong ngữ cảnh giao hàng:
Ordercó thể chỉ là một việc cần giao.Customercó thể là người nhận hàng.Addressquan trọng hơn sản phẩm.Statusxoay quanh lấy hàng, đang giao, giao thành công, giao thất bại.
Trong ngữ cảnh kế toán:
Orderkhông quan trọng bằngInvoice,Transaction,Refund.Customercó thể là đối tượng xuất hóa đơn.Pricephải tách thành doanh thu, thuế, phí, chiết khấu.
Nếu không có bounded context, ta dễ tưởng các khái niệm này là một.
Nhưng thực tế, chúng phục vụ những câu hỏi khác nhau.
---
13.4. Cách hiểu rất ngắn
Nếu chương 12 nói:
> Domain là thế giới nghiệp vụ.
Thì chương 13 nói:
> Bounded Context là từng khu vực trong thế giới đó, nơi mỗi từ có nghĩa riêng và luật chơi riêng.
Một thành phố có nhiều khu:
- Khu bán hàng.
- Khu bếp.
- Khu kho.
- Khu giao hàng.
- Khu kế toán.
- Khu chăm sóc khách hàng.
Mỗi khu có ngôn ngữ riêng.
Ở bếp, "xong" nghĩa là bánh đã làm xong.
Ở giao hàng, "xong" nghĩa là đã giao tới khách.
Ở kế toán, "xong" nghĩa là đã thu tiền và đối soát.
Nếu hệ thống chỉ có một trạng thái DONE, sớm muộn gì cũng rối.
---
13.5. Vì sao bounded context quan trọng?
Bounded Context quan trọng vì nó giảm nhầm lẫn.
Trong hệ thống nhỏ, nhầm lẫn còn chịu được.
Một vài người cùng hiểu ngầm với nhau.
Nhưng khi hệ thống lớn:
- Nhiều tính năng hơn.
- Nhiều developer hơn.
- Nhiều team hơn.
- Nhiều trạng thái hơn.
- Nhiều quy tắc ngoại lệ hơn.
- Nhiều service hơn.
- Nhiều dữ liệu hơn.
Hiểu ngầm bắt đầu trở thành rủi ro.
Một người sửa Order.status để phục vụ giao hàng, nhưng lại làm hỏng màn hình kế toán.
Một người đổi nghĩa của User.active, nhưng không biết bên marketing dùng nó để gửi email.
Một người thêm Product.available, nhưng không rõ đó là còn hàng trong kho, còn bán trên website, hay còn đủ nguyên liệu để làm.
Bounded Context giúp ta nói rõ:
- Thuật ngữ này có nghĩa gì trong khu vực này?
- Model này thuộc về khu vực nào?
- Ai được sửa dữ liệu này?
- Bên ngoài muốn dùng thì phải đi qua API/event nào?
- Khi đi sang context khác, dữ liệu cần được dịch nghĩa ra sao?
---
13.6. Bounded Context không nhất thiết là microservice
Đây là điểm rất quan trọng.
Bounded Context là ranh giới về mô hình và ngôn ngữ nghiệp vụ.
Microservice là ranh giới về triển khai và vận hành.
Hai thứ này có liên quan, nhưng không giống nhau.
Một bounded context có thể nằm trong:
- Một module của monolith.
- Một package trong modular monolith.
- Một service riêng.
- Một nhóm service.
Ví dụ:
Monolith bán bánh
├── catalog/
├── ordering/
├── kitchen/
├── payment/
├── delivery/
└── support/
Ở đây, mỗi thư mục có thể là một bounded context ở mức code, dù cả hệ thống vẫn deploy chung.
Sau này nếu payment hoặc delivery cần tách thành service riêng, ta đã có ranh giới tốt hơn để tách.
Nhưng nếu chưa có lý do vận hành, đừng vội biến mỗi bounded context thành một microservice.
Tách model trước.
Tách module trước.
Tách service sau.
---
13.7. Cùng một từ, nhiều nghĩa: dấu hiệu cần bounded context
Khi nghe team tranh luận một từ nhưng không thống nhất được, đó thường là dấu hiệu có nhiều context.
Ví dụ 1: "User"
Trong hệ thống giáo dục:
- Với Auth, user là tài khoản đăng nhập.
- Với lớp học, user có thể là học viên hoặc giáo viên.
- Với thanh toán, user là người trả tiền.
- Với hỗ trợ, user là người cần chăm sóc.
- Với phân tích dữ liệu, user là một actor tạo event.
Nếu dùng một model User cho tất cả, nó sẽ nhanh chóng thành model khổng lồ.
Ví dụ 2: "Course"
- Với catalog, course là khóa học được hiển thị để bán.
- Với learning, course là nội dung học, bài học, tiến độ.
- Với instructor, course là sản phẩm cần biên tập.
- Với certificate, course là điều kiện để cấp chứng chỉ.
- Với finance, course là SKU tạo doanh thu.
Ví dụ 3: "Active"
- Tài khoản active nghĩa là đăng nhập được.
- Gói học active nghĩa là còn thời hạn.
- Sản phẩm active nghĩa là đang được bán.
- Chiến dịch active nghĩa là đang chạy.
- Shipper active nghĩa là đang nhận đơn.
Một chữ active nghe vô hại, nhưng nếu không rõ context, nó có thể gây lỗi rất lớn.
---
13.8. Bounded Context giúp ta đặt tên tốt hơn
Một hệ thống rối thường có tên gọi quá chung:
UserOrderProductItemStatusDataManagerService
Không phải các tên này luôn sai.
Nhưng nếu cả hệ thống dùng chung một tên cho nhiều nghĩa khác nhau, code sẽ khó hiểu.
Bounded Context cho phép đặt tên cụ thể hơn.
Thay vì chỉ có:
Order
Ta có thể có:
SalesOrder
KitchenTicket
DeliveryTask
Invoice
RefundRequest
SupportCase
Thay vì chỉ có:
User
Ta có thể có:
Account
Customer
Student
Teacher
Operator
Recipient
Payer
Thay vì chỉ có:
Product
Ta có thể có:
CatalogProduct
StockItem
Recipe
Sku
BillableItem
Tên tốt không phải để làm màu.
Tên tốt giúp người đọc biết mình đang ở ngữ cảnh nào.
---
13.9. Ví dụ: đơn hàng đi qua nhiều context
Hãy nhìn một đơn hàng bánh đi từ lúc khách đặt đến lúc hoàn tất.
Khách đặt bánh
-> Sales/Ordering Context tạo SalesOrder
-> Payment Context xử lý Payment
-> Kitchen Context tạo KitchenTicket
-> Delivery Context tạo DeliveryTask
-> Accounting Context tạo Invoice/RevenueRecord
-> Support Context có thể tạo SupportCase nếu có khiếu nại
Ở mỗi bước, hệ thống không nhất thiết truyền nguyên một object Order khổng lồ.
Nó có thể truyền những thông tin cần thiết.
Ví dụ Ordering phát event:
OrderPlaced
- order_id
- customer_id
- items
- delivery_time
- total_amount
Kitchen nhận event này và tạo:
KitchenTicket
- ticket_id
- order_id
- cakes_to_make
- due_time
- special_notes
- status
Delivery có thể nhận thông tin khác:
DeliveryTask
- task_id
- order_id
- recipient_name
- phone
- address
- pickup_time
- delivery_note
Accounting có thể nhận:
RevenueRecord
- order_id
- gross_amount
- discount_amount
- tax_amount
- payment_method
- paid_at
Cùng xuất phát từ một đơn hàng, nhưng mỗi context tạo mô hình riêng phục vụ việc của nó.
Đây là điều bình thường.
---
13.10. Dịch nghĩa giữa các context
Khi dữ liệu đi từ context này sang context khác, ta không nên giả định rằng mọi bên hiểu giống nhau.
Cần có bước "dịch nghĩa".
Ví dụ:
Ordering nói:
Order status = CONFIRMED
Nhưng Kitchen không nhất thiết dùng trạng thái đó.
Kitchen có thể dịch thành:
KitchenTicket status = WAITING_TO_PREPARE
Payment nói:
Payment status = PAID
Accounting có thể dịch thành:
RevenueRecord status = RECOGNIZED
Delivery nói:
DeliveryTask status = FAILED
Support có thể dịch thành:
SupportCase reason = DELIVERY_FAILED
priority = HIGH
Dịch nghĩa không phải là thừa.
Nó bảo vệ từng context khỏi việc bị ép dùng ngôn ngữ của context khác.
Trong DDD, có một khái niệm tên là Anti-Corruption Layer.
Tên nghe hơi nặng, nhưng ý rất đơn giản:
> Khi nhận dữ liệu từ bên ngoài, đừng để mô hình bên ngoài làm bẩn mô hình bên trong. Hãy chuyển nó sang ngôn ngữ của mình.
Ví dụ:
Payment gateway bên thứ ba trả về:
result_code = 00
message = approved
txn_state = captured
Trong hệ thống của ta, không nên để khắp code phải hiểu result_code = 00.
Ta nên dịch thành:
PaymentSucceeded
hoặc:
Payment.status = SUCCEEDED
Như vậy phần còn lại của hệ thống nói ngôn ngữ của mình, không bị kéo theo chi tiết của nhà cung cấp bên ngoài.
---
13.11. Bounded Context và database
Một câu hỏi rất thực tế:
> Nếu có nhiều bounded context, có phải mỗi context phải có database riêng không?
Câu trả lời ngắn:
> Không bắt buộc.
Trong modular monolith, nhiều context có thể dùng chung một database vật lý.
Nhưng tốt nhất vẫn nên có quyền sở hữu dữ liệu rõ ràng.
Ví dụ:
ordering_orders thuộc Ordering Context
payment_transactions thuộc Payment Context
kitchen_tickets thuộc Kitchen Context
delivery_tasks thuộc Delivery Context
support_cases thuộc Support Context
Dù nằm chung database, code của context khác không nên tự tiện sửa bảng không thuộc về mình.
Sai lầm phổ biến là:
Payment code tự update ordering_orders.status
Delivery code tự update payment_transactions
Support code tự sửa mọi thứ trực tiếp
Nhìn thì tiện.
Nhưng lâu dài, không còn context nào thật sự sở hữu dữ liệu của mình.
Cách tốt hơn:
- Context sở hữu dữ liệu thì cung cấp API, command, hoặc event.
- Context khác muốn thay đổi thì gửi yêu cầu qua ranh giới đó.
- Nếu cần đọc dữ liệu để hiển thị, có thể tạo read model hoặc view phù hợp.
Khi đã lên microservices, nguyên tắc này càng quan trọng:
> Mỗi service nên sở hữu dữ liệu của nó. Service khác không nên chọc thẳng vào database của nó.
---
13.12. Bounded Context và API
API không chỉ là endpoint.
API là cách một context nói chuyện với context khác.
Nếu API thiết kế kém, nó sẽ làm lộ mô hình nội bộ ra ngoài.
Ví dụ API xấu:
PATCH /orders/123
{
"payment_status": "PAID",
"kitchen_status": "WAITING",
"delivery_status": "CREATED",
"refund_status": null
}
API này biến Order thành nơi mọi context cùng sửa.
API tốt hơn thường thể hiện ý định:
POST /payments/123/confirm
POST /kitchen-tickets
POST /delivery-tasks
POST /refund-requests
Hoặc dùng event:
PaymentSucceeded
KitchenTicketCreated
DeliveryTaskAssigned
RefundRequested
Điểm quan trọng:
> API nên nói bằng ngôn ngữ của context sở hữu hành vi đó.
Nếu Payment Context xử lý thanh toán, hãy để Payment nói về Payment, Transaction, Capture, Refund.
Đừng bắt Payment phải sửa trực tiếp Order.payment_status như thể nó chỉ là một cột phụ của Order.
---
13.13. Bounded Context và event
Event rất hợp với bounded context vì event cho phép một context thông báo sự thật đã xảy ra mà không cần biết ai sẽ dùng.
Ví dụ:
OrderPlaced
PaymentSucceeded
PaymentFailed
KitchenTicketCompleted
DeliveryFailed
RefundIssued
Mỗi event nên là một câu có nghĩa trong domain.
Không nên đặt event kiểu quá kỹ thuật:
OrderTableUpdated
StatusChanged
DataSynced
Những event này không nói rõ nghiệp vụ gì đã xảy ra.
StatusChanged đặc biệt nguy hiểm vì status trong context nào?
Status đổi từ gì sang gì?
Vì sao đổi?
Ai cần quan tâm?
Event tốt nên trả lời được:
- Chuyện gì đã xảy ra?
- Xảy ra với đối tượng nào?
- Xảy ra khi nào?
- Vì sao các context khác có thể quan tâm?
Ví dụ:
DeliveryFailed
- delivery_task_id
- order_id
- failed_reason
- failed_at
Support Context nghe event này và tạo case.
Ordering Context nghe event này và cập nhật trạng thái hiển thị cho khách.
Accounting Context có thể không cần quan tâm.
Đây chính là lợi ích: mỗi context tự quyết định mình có cần phản ứng hay không.
---
13.14. Khi nào nên tách bounded context?
Ta nên nghĩ đến bounded context khi thấy các dấu hiệu này:
1. Cùng một từ có nhiều nghĩa khác nhau.
Ví dụ Order trong bán hàng, bếp, giao hàng, kế toán không còn cùng nghĩa.
2. Một model có quá nhiều field phục vụ nhiều nhóm khác nhau.
Ví dụ Order có cả field giao hàng, thanh toán, khiếu nại, hóa đơn, sản xuất.
3. Các quy tắc thay đổi độc lập.
Ví dụ quy tắc hủy đơn thay đổi khác với quy tắc hoàn tiền.
4. Các nhóm làm việc khác nhau sở hữu các phần khác nhau.
Ví dụ team Payment không nên bị kéo vào mọi thay đổi của Catalog.
5. Vòng đời dữ liệu khác nhau.
Ví dụ Payment có trạng thái riêng, không nên bị ép chung vào trạng thái Order.
6. Mức độ rủi ro khác nhau.
Ví dụ dữ liệu kế toán và thanh toán cần chặt hơn dữ liệu marketing.
7. Tần suất thay đổi khác nhau.
Ví dụ UI catalog đổi liên tục, còn ledger tài chính phải cực kỳ ổn định.
Nếu nhiều dấu hiệu cùng xuất hiện, khả năng cao ta cần ranh giới context rõ hơn.
---
13.15. Khi nào chưa cần tách?
Không phải thấy khái niệm mới là tách ngay.
Nếu hệ thống còn nhỏ, ít người, nghiệp vụ đơn giản, tách quá sớm có thể làm mọi thứ nặng hơn.
Chưa cần tách mạnh khi:
- Chỉ có một team nhỏ làm toàn bộ hệ thống.
- Quy tắc nghiệp vụ còn đang thay đổi rất nhanh.
- Các khái niệm chưa đủ rõ.
- Chưa có vấn đề thật về rối model.
- Chưa có nhu cầu deploy/scale độc lập.
- Chi phí giao tiếp giữa các phần lớn hơn lợi ích.
Ví dụ quán bánh mới mở chỉ có:
- Danh sách bánh.
- Đặt hàng.
- Thanh toán khi nhận.
- Giao hàng thủ công.
Lúc này có thể chỉ cần module rõ ràng trong monolith.
Đừng ép thành quá nhiều context phức tạp.
Cách thực dụng là:
Ban đầu:
Một module ordering đơn giản.
Khi nghiệp vụ rõ hơn:
Tách ordering, payment, delivery về mặt code.
Khi quy mô lớn hơn:
Có thể tách service nếu cần.
Bounded Context không yêu cầu ta phức tạp hóa hệ thống ngay từ ngày đầu.
Nó yêu cầu ta nhận ra ranh giới khi sự phức tạp bắt đầu xuất hiện.
---
13.16. Bounded Context khác module như thế nào?
Module là ranh giới trong code.
Bounded Context là ranh giới về nghĩa và mô hình.
Hai thứ thường đi cùng nhau, nhưng không hoàn toàn giống nhau.
Ví dụ:
modules/
payment/
delivery/
catalog/
Đây là module.
Nếu mỗi module có:
- Ngôn ngữ riêng.
- Model riêng.
- Quy tắc riêng.
- Dữ liệu sở hữu rõ.
- API/event rõ.
Thì module đó đang phản ánh bounded context tốt.
Nhưng nếu chỉ chia thư mục cho đẹp:
modules/
order/
payment/
delivery/
Mà bên nào cũng import model của nhau, sửa database của nhau, dùng chung Order.status, thì đó chỉ là chia file, chưa phải chia context.
Một câu hỏi tốt:
> Nếu đọc code trong module này, ta có thấy nó đang nói một ngôn ngữ riêng rõ ràng không?
Nếu câu trả lời là không, module đó có thể chỉ là phân loại kỹ thuật, chưa phải ranh giới nghiệp vụ.
---
13.17. Bounded Context khác database schema như thế nào?
Database schema là cách lưu dữ liệu.
Bounded Context là cách hiểu nghiệp vụ.
Đôi khi một context có nhiều bảng.
Đôi khi nhiều context tạm thời nằm chung một database.
Đừng nhầm:
Một bảng = một context
Không đúng.
Ví dụ orders có thể là bảng của Ordering Context.
Nhưng Payment Context không nên vì có order_id mà trở thành một phần của Ordering.
Payment có thể có bảng riêng:
payments
payment_attempts
refunds
Các bảng này tham chiếu order_id như một id bên ngoài, nhưng model chính vẫn là Payment.
Tương tự, Delivery có thể lưu order_id, nhưng Delivery không cần biết toàn bộ cấu trúc Order.
Nó chỉ cần biết:
- Cần giao cái gì?
- Giao cho ai?
- Giao ở đâu?
- Giao khi nào?
Đó là sự khác nhau giữa tham chiếu dữ liệu và sở hữu mô hình.
---
13.18. Bounded Context và quyền sở hữu
Một bounded context tốt nên có người hoặc nhóm chịu trách nhiệm rõ.
Không nhất thiết là một team riêng trong công ty nhỏ.
Nhưng ít nhất trong đầu ta phải biết:
- Ai hiểu luật của context này?
- Ai được quyết định model này thay đổi ra sao?
- Nếu có bug, phần nào chịu trách nhiệm?
- Nếu context khác cần dữ liệu, đi qua đường nào?
Ví dụ:
Payment Context sở hữu:
- Payment
- PaymentAttempt
- Refund
- Transaction
Ordering Context không tự sửa Payment.
Ordering chỉ yêu cầu thanh toán hoặc nhận event thanh toán thành công/thất bại.
Khi ownership rõ, hệ thống dễ tiến hóa hơn.
Khi ownership mờ, mọi thứ thành "của chung".
Mà trong phần mềm, "của chung" thường biến thành "không ai thật sự chịu trách nhiệm".
---
13.19. Ví dụ thực tế: hệ thống học online
Hãy lấy một hệ thống học online.
Nếu nhìn hời hợt, ta có các bảng:
- users
- courses
- lessons
- payments
- certificates
Nhưng nếu nhìn theo bounded context, ta có thể thấy nhiều khu vực khác nhau.
Identity Context
Quan tâm đến:
- Tài khoản.
- Đăng nhập.
- Mật khẩu.
- Vai trò.
- Quyền truy cập.
Ở đây, User là account.
Catalog Context
Quan tâm đến:
- Khóa học được bán.
- Tiêu đề.
- Mô tả.
- Giá.
- Giảng viên hiển thị.
- Tag.
- Landing page khóa học.
Ở đây, Course là sản phẩm để khách xem.
Learning Context
Quan tâm đến:
- Nội dung học.
- Bài học.
- Tiến độ học.
- Bài đã xem.
- Bài tập.
- Hoàn thành khóa học.
Ở đây, Course là trải nghiệm học.
Payment Context
Quan tâm đến:
- Giao dịch.
- Thanh toán thành công/thất bại.
- Hoàn tiền.
- Mã giảm giá.
- Đối soát.
Ở đây, User có thể là payer.
Certificate Context
Quan tâm đến:
- Điều kiện cấp chứng chỉ.
- Ai được cấp.
- Ngày cấp.
- Mã chứng chỉ.
- Thu hồi chứng chỉ.
Ở đây, CourseCompleted từ Learning có thể kích hoạt kiểm tra chứng chỉ.
Nếu gộp tất cả vào một model Course, model đó sẽ vừa phải bán hàng, vừa phải dạy học, vừa phải cấp chứng chỉ, vừa phải tính tiền.
Đó là một mùi thiết kế.
---
13.20. Ví dụ thực tế: hệ thống đặt vé
Trong hệ thống đặt vé, cùng một chữ "seat" cũng có nhiều nghĩa.
Venue Context
Seat là ghế vật lý trong rạp hoặc sân vận động:
- Khu.
- Hàng.
- Số ghế.
- Sơ đồ chỗ ngồi.
- Có bị hỏng không.
Inventory Context
Seat là thứ có thể bán cho một sự kiện cụ thể:
- Còn trống không.
- Đang được giữ chỗ không.
- Đã bán chưa.
- Hết hạn giữ chỗ lúc nào.
Pricing Context
Seat là đơn vị định giá:
- Hạng vé.
- Giá cơ bản.
- Phụ phí.
- Khuyến mãi.
Ticketing Context
Seat là thứ được gắn với vé:
- Ticket id.
- Người sở hữu.
- Mã QR.
- Trạng thái check-in.
Nếu không tách context, ta dễ tạo một bảng seats chứa mọi thứ:
- Tọa độ trên sơ đồ.
- Giá.
- Trạng thái bán.
- Người mua.
- Mã QR.
- Lịch sử check-in.
Bảng này sẽ rất khó vận hành khi có nhiều sự kiện, nhiều loại giá, nhiều trạng thái giữ chỗ và nhiều luồng check-in.
---
13.21. Context Map: bản đồ quan hệ giữa các context
Sau khi chia context, ta cần biết chúng liên hệ với nhau thế nào.
Đó là ý tưởng của Context Map.
Tên nghe học thuật, nhưng thực tế chỉ là bản đồ:
Catalog
|
| khách chọn sản phẩm
v
Ordering
|
| yêu cầu thanh toán
v
Payment
|
| thanh toán thành công
v
Kitchen
|
| làm xong
v
Delivery
|
| giao thất bại
v
Support
Context Map giúp trả lời:
- Context nào gọi context nào?
- Context nào phát event cho context nào?
- Context nào là nguồn sự thật của dữ liệu nào?
- Context nào phụ thuộc context nào?
- Khi thay đổi một context, ai có thể bị ảnh hưởng?
Không cần vẽ quá đẹp.
Quan trọng là nhìn vào thấy được luồng phụ thuộc.
---
13.22. Upstream và downstream bằng ngôn ngữ dễ hiểu
Trong quan hệ giữa hai context, thường có bên upstream và downstream.
Hiểu đơn giản:
- Upstream là bên tạo ra dữ liệu/quy tắc mà bên khác phải dùng.
- Downstream là bên phụ thuộc vào dữ liệu/quy tắc đó.
Ví dụ:
Payment -> Accounting
Payment phát event PaymentSucceeded.
Accounting nghe event đó để ghi nhận doanh thu.
Trong quan hệ này:
- Payment là upstream.
- Accounting là downstream.
Nếu Payment đổi cấu trúc event, Accounting có thể bị ảnh hưởng.
Vì vậy, upstream cần cẩn thận khi thay đổi contract.
Ví dụ khác:
Catalog -> Ordering
Ordering cần thông tin sản phẩm từ Catalog.
Catalog là nguồn sự thật về sản phẩm đang bán.
Ordering là downstream đối với dữ liệu catalog.
Điểm quan trọng:
> Context càng có nhiều downstream, thay đổi của nó càng cần được kiểm soát kỹ.
---
13.23. Shared Kernel: dùng chung một phần rất nhỏ
Đôi khi hai context thật sự cần dùng chung một phần model.
Ví dụ:
- Money.
- Currency.
- EmailAddress.
- PhoneNumber.
- CountryCode.
Ta có thể tạo phần dùng chung nhỏ gọi là Shared Kernel.
Nhưng phải rất cẩn thận.
Shared Kernel càng lớn, context càng bị dính nhau.
Sai lầm thường gặp:
shared/
models/
User
Order
Product
Payment
Delivery
Thư mục shared trở thành nơi chứa toàn bộ domain.
Khi đó bounded context gần như mất tác dụng.
Shared tốt nên nhỏ, ổn định, ít thay đổi.
Ví dụ:
shared/
value_objects/
Money
Currency
EmailAddress
Nếu một thứ có nhiều quy tắc nghiệp vụ, đừng vội đưa vào shared.
Hãy hỏi:
> Thứ này thật sự có cùng nghĩa ở mọi context không?
Nếu không chắc, đừng share.
---
13.24. Cách tìm bounded context trong dự án thật
Không cần bắt đầu bằng lý thuyết nặng.
Ta có thể bắt đầu bằng vài câu hỏi.
Câu hỏi 1: Những nhóm nghiệp vụ chính là gì?
Ví dụ bán bánh:
- Catalog.
- Ordering.
- Payment.
- Kitchen.
- Delivery.
- Support.
- Accounting.
Câu hỏi 2: Mỗi nhóm dùng những từ nào?
Catalog nói:
- Product.
- Category.
- Price.
- Availability.
Kitchen nói:
- Recipe.
- Ingredient.
- Batch.
- Ticket.
Payment nói:
- Transaction.
- Capture.
- Refund.
- PaymentAttempt.
Câu hỏi 3: Có từ nào bị dùng nhiều nghĩa không?
Ví dụ:
- Order.
- Customer.
- Product.
- Status.
- Available.
- Done.
- Cancelled.
Câu hỏi 4: Ai sở hữu luật nào?
Ví dụ:
- Ai quyết định đơn được hủy hay không?
- Ai quyết định thanh toán thành công?
- Ai quyết định đã giao hàng?
- Ai quyết định được hoàn tiền?
- Ai quyết định ghi nhận doanh thu?
Câu hỏi 5: Dữ liệu nào là nguồn sự thật?
Ví dụ:
- Catalog là nguồn sự thật về sản phẩm đang bán.
- Payment là nguồn sự thật về giao dịch.
- Delivery là nguồn sự thật về quá trình giao.
- Accounting là nguồn sự thật về ghi nhận tài chính.
Câu hỏi 6: Phần nào thay đổi cùng nhau?
Những thứ thường thay đổi cùng nhau có thể thuộc cùng context.
Những thứ thay đổi vì lý do khác nhau có thể nên tách context.
---
13.25. Một quy trình thực dụng để chia context
Khi đứng trước một hệ thống thật, có thể làm theo quy trình này.
Bước 1: Viết các luồng nghiệp vụ chính
Ví dụ:
Khách đặt bánh
Khách thanh toán
Bếp làm bánh
Shipper giao bánh
Khách khiếu nại
Kế toán đối soát
Bước 2: Gạch chân danh từ và động từ quan trọng
Danh từ:
- Khách.
- Bánh.
- Đơn hàng.
- Thanh toán.
- Phiếu bếp.
- Giao hàng.
- Khiếu nại.
- Hóa đơn.
Động từ:
- Đặt.
- Xác nhận.
- Thanh toán.
- Làm.
- Giao.
- Hủy.
- Hoàn tiền.
- Đối soát.
Bước 3: Nhóm theo mục đích
Ví dụ:
Catalog: bánh, danh mục, giá hiển thị
Ordering: đơn hàng, giỏ hàng, xác nhận
Payment: thanh toán, giao dịch, hoàn tiền
Kitchen: phiếu bếp, công thức, trạng thái làm bánh
Delivery: nhiệm vụ giao, shipper, trạng thái giao
Accounting: hóa đơn, doanh thu, đối soát
Support: khiếu nại, ghi chú, xử lý khách
Bước 4: Kiểm tra từ bị trùng nghĩa
Nếu Order xuất hiện ở nhiều nhóm, hỏi:
- Ở nhóm này, Order dùng để làm gì?
- Nó có trạng thái riêng không?
- Nó có luật riêng không?
- Nó có cần cùng model với nhóm kia không?
Bước 5: Quyết định ranh giới mềm trước
Đừng vội tách service.
Có thể bắt đầu bằng:
- Package riêng.
- Module riêng.
- Service layer riêng.
- Model riêng.
- Database table ownership rõ.
- Event/API rõ.
Bước 6: Chỉ tách vật lý khi có lý do
Tách thành service riêng khi có lý do như:
- Cần scale riêng.
- Cần deploy riêng.
- Cần bảo mật dữ liệu riêng.
- Team riêng sở hữu.
- Workload rất khác.
- Failure cần cô lập.
---
13.26. Những lỗi phổ biến
Lỗi 1: Tưởng bounded context là microservice
Không đúng.
Microservice là một cách triển khai.
Bounded Context là một cách hiểu và chia mô hình nghiệp vụ.
Có thể có bounded context trong monolith.
Lỗi 2: Dùng một model cho cả hệ thống
Một model User, Order, Product dùng khắp nơi thường rất tiện lúc đầu.
Nhưng về lâu dài, nó dễ thành điểm nghẽn về hiểu biết.
Lỗi 3: Chia context theo kỹ thuật thay vì nghiệp vụ
Ví dụ:
UserService
DatabaseService
EmailService
FileService
Đây có thể là chia theo kỹ thuật, không phải domain.
Bounded Context nên xoay quanh nghiệp vụ:
Ordering
Payment
Delivery
Learning
Booking
Inventory
Lỗi 4: Tạo shared quá lớn
Cái gì cũng đưa vào common, shared, core.
Kết quả là mọi context vẫn phụ thuộc nhau.
Lỗi 5: Không dịch nghĩa khi giao tiếp
Context A gửi model nội bộ cho Context B dùng trực tiếp.
B phụ thuộc vào chi tiết của A.
Sau này A đổi model, B vỡ.
Lỗi 6: Chia quá nhỏ
Mỗi bảng thành một context.
Mỗi entity thành một service.
Hệ thống trở nên nhiều ranh giới nhưng ít ý nghĩa.
Tách nhỏ không tự động làm hệ thống tốt hơn.
Ranh giới tốt phải dựa trên nghiệp vụ và sự thay đổi thật.
---
13.27. So sánh nhanh: model dùng chung và model theo context
| Cách làm | Ưu điểm | Nhược điểm | Khi phù hợp | |---|---|---|---| | Một model dùng chung | Nhanh lúc đầu, ít mapping | Dễ phình to, dễ lẫn nghĩa, thay đổi lan rộng | Hệ thống nhỏ, nghiệp vụ đơn giản | | Model riêng theo context | Rõ nghĩa, dễ sở hữu, dễ tiến hóa | Cần mapping, cần thiết kế contract | Hệ thống vừa/lớn, nhiều nghiệp vụ | | Service riêng theo context | Scale/deploy/ownership tốt hơn | Tăng chi phí vận hành, network, monitoring | Khi có lý do vận hành thật |
Không có cách nào đúng tuyệt đối.
Câu hỏi là:
> Mức độ phức tạp hiện tại có xứng đáng với ranh giới này không?
---
13.28. Ví dụ sai và cách sửa
Thiết kế dễ rối
Order
- id
- customer
- items
- payment_status
- delivery_status
- kitchen_status
- invoice_status
- support_status
- refund_status
Mọi module cùng đọc/sửa Order.
Luồng xử lý:
Payment sửa Order.payment_status
Kitchen sửa Order.kitchen_status
Delivery sửa Order.delivery_status
Accounting sửa Order.invoice_status
Support sửa Order.support_status
Vấn đề:
- Không ai thật sự sở hữu Order.
- Thay đổi trạng thái dễ ảnh hưởng chéo.
- Model phình to.
- Test khó.
- Logic nằm rải rác.
- Không rõ event nào quan trọng.
Thiết kế rõ hơn
Ordering Context:
SalesOrder
Payment Context:
Payment
PaymentAttempt
Refund
Kitchen Context:
KitchenTicket
Delivery Context:
DeliveryTask
Accounting Context:
Invoice
RevenueRecord
Support Context:
SupportCase
Các context giao tiếp bằng event/API:
OrderPlaced
PaymentSucceeded
KitchenTicketCompleted
DeliveryFailed
RefundIssued
Mỗi context giữ model của mình.
Khi cần hiển thị "trạng thái tổng" cho khách, Ordering hoặc một read model có thể tổng hợp:
Đã đặt hàng
Đã thanh toán
Đang làm bánh
Đang giao
Đã giao
Trạng thái hiển thị cho khách không nhất thiết là trạng thái nội bộ duy nhất của toàn hệ thống.
Đây là một điểm rất quan trọng.
---
13.29. Bounded Context và màn hình quản trị
Nhiều hệ thống có một trang admin muốn xem mọi thứ.
Điều này dễ khiến ta phá boundary.
Ví dụ admin cần xem:
- Thông tin đơn.
- Thanh toán.
- Giao hàng.
- Hoàn tiền.
- Khiếu nại.
Ta có thể bị cám dỗ tạo một query join tất cả bảng rồi sửa trực tiếp mọi thứ.
Nhưng admin UI không nhất thiết phải là chủ sở hữu của mọi dữ liệu.
Cách tốt hơn:
- Admin UI đọc dữ liệu tổng hợp từ read model.
- Khi muốn thao tác, nó gọi đúng context sở hữu hành động đó.
Ví dụ:
Admin bấm "hoàn tiền"
-> gọi Refund API của Payment Context
Admin bấm "giao lại"
-> gọi Retry Delivery API của Delivery Context
Admin bấm "hủy đơn"
-> gọi Cancel Order API của Ordering Context
Admin là bề mặt thao tác.
Nó không nên trở thành nơi chứa toàn bộ logic nghiệp vụ.
---
13.30. Bounded Context và báo cáo
Báo cáo cũng là nơi dễ làm hỏng boundary.
Team phân tích dữ liệu thường muốn lấy mọi bảng.
Điều này không sai nếu mục tiêu là analytics, nhưng cần phân biệt:
- Hệ thống vận hành.
- Hệ thống báo cáo.
Trong hệ thống vận hành, context nên sở hữu dữ liệu của mình.
Trong hệ thống báo cáo, ta có thể copy dữ liệu sang data warehouse, tạo bảng tổng hợp, join nhiều nguồn.
Ví dụ:
Operational systems:
Ordering
Payment
Delivery
Support
Analytics:
order_fact
payment_fact
delivery_fact
customer_support_fact
Báo cáo có thể cần nhìn toàn cảnh.
Nhưng đừng để nhu cầu báo cáo làm mọi context trong hệ thống vận hành phụ thuộc trực tiếp vào nhau.
---
13.31. Bounded Context không làm biến mất sự phức tạp
Bounded Context không phải phép màu.
Nó không làm nghiệp vụ đơn giản đi.
Nó chỉ đặt sự phức tạp vào đúng chỗ.
Nếu thanh toán phức tạp, Payment Context vẫn phức tạp.
Nếu giao hàng nhiều ngoại lệ, Delivery Context vẫn phức tạp.
Nhưng ít nhất:
- Phức tạp của Payment không tràn sang Catalog.
- Phức tạp của Delivery không làm bẩn Accounting.
- Phức tạp của Support không biến
Orderthành model khổng lồ.
Thiết kế tốt không phải là không có phức tạp.
Thiết kế tốt là biết phức tạp nào thuộc về đâu.
---
13.32. Bảng nhận diện nhanh
| Hiện tượng | Có thể đang thiếu gì? | |---|---| | Order có quá nhiều trạng thái | Thiếu context riêng cho payment/delivery/kitchen | | User dùng cho mọi vai trò | Thiếu phân biệt account/customer/operator | | Một thay đổi nhỏ làm nhiều module vỡ | Boundary chưa rõ | | Nhiều team tranh luận cùng một từ | Ngôn ngữ context chưa rõ | | shared ngày càng phình to | Shared Kernel bị lạm dụng | | Service khác sửa database của nhau | Ownership dữ liệu yếu | | Event tên StatusChanged xuất hiện nhiều | Event chưa nói đúng nghiệp vụ | | Admin chứa quá nhiều logic | UI đang vượt quyền context |
---
13.33. Cách áp dụng trong code mà không làm quá
Nếu đang có một monolith, có thể bắt đầu nhẹ nhàng:
src/
catalog/
models/
services/
api/
ordering/
models/
services/
api/
events/
payment/
models/
services/
api/
events/
delivery/
models/
services/
api/
events/
Nguyên tắc:
- Context nào sở hữu model thì context đó định nghĩa model.
- Context khác không import bừa model nội bộ nếu không cần.
- Giao tiếp qua service method, API nội bộ, command hoặc event.
- Shared chỉ chứa thứ thật sự chung và ổn định.
- Tên model phản ánh ngữ cảnh.
Ví dụ thay vì:
from order.models import Order
ở khắp nơi, hãy hỏi:
> Context này thật sự cần Order, hay chỉ cần biết order_id và một sự kiện nghiệp vụ?
Nhiều khi Delivery không cần toàn bộ Order.
Delivery chỉ cần:
- order_id
- người nhận
- địa chỉ
- thời gian giao
- ghi chú giao hàng
Biết ít hơn đôi khi là thiết kế tốt hơn.
---
13.34. Một bài kiểm tra rất thực tế
Lấy một model lớn nhất trong hệ thống của bạn.
Ví dụ:
UserOrderProductCourseSubmissionBooking
Sau đó hỏi:
1. Model này đang phục vụ bao nhiêu nghiệp vụ khác nhau? 2. Có bao nhiêu nhóm người quan tâm đến nó? 3. Có bao nhiêu trạng thái trong model này? 4. Các trạng thái đó có thuộc cùng một vòng đời không? 5. Có field nào chỉ dùng cho một tính năng rất xa phần còn lại không? 6. Có module nào chỉ cần một phần rất nhỏ của model không? 7. Có thay đổi nào ở model này thường làm nhiều nơi vỡ không? 8. Có từ nào trong model này đang bị hiểu theo nhiều nghĩa không?
Nếu nhiều câu trả lời là "có", model đó có thể đang gánh nhiều context.
Đừng sửa bằng cách tách service ngay.
Hãy bắt đầu bằng tách nghĩa.
---
13.35. Áp dụng vào bài toán AI Judge
Trong cuộc trò chuyện trước, ta có nhắc đến hệ thống chấm bài bằng AI.
Bài toán này cũng có nhiều bounded context.
Ví dụ:
Submission Context
Quan tâm đến:
- Học viên nộp bài.
- Bài nộp thuộc assignment nào.
- Phiên bản bài nộp.
- Trạng thái đã nộp/chưa nộp.
Grading Context
Quan tâm đến:
- Job chấm bài.
- Rubric.
- Điểm.
- Nhận xét.
- Lần retry.
- Model AI được dùng.
- Trạng thái đang chấm/thành công/thất bại.
Learning Context
Quan tâm đến:
- Tiến độ học.
- Bài nào đã hoàn thành.
- Học viên được mở bài tiếp theo chưa.
Billing/Quota Context
Nếu có tính phí hoặc giới hạn:
- Mỗi user được chấm bao nhiêu lần.
- Chi phí mỗi lần gọi AI.
- Gói hiện tại còn quota không.
Notification Context
Quan tâm đến:
- Khi nào báo học viên có kết quả.
- Gửi email hay in-app.
- Gửi lại nếu thất bại.
Nếu gộp hết vào Submission, model này sẽ có:
- Nội dung bài nộp.
- Trạng thái chấm.
- Điểm.
- Prompt.
- Response AI.
- Retry count.
- Cost.
- Notification status.
- Quota status.
- Learning progress.
Rất dễ rối.
Thiết kế rõ hơn:
SubmissionCreated
-> Grading Context tạo GradingJob
-> AI worker xử lý
-> GradingCompleted
-> Learning cập nhật tiến độ
-> Notification gửi thông báo
Ở đây, Submission không cần biết toàn bộ chi tiết của AI job.
GradingJob mới là nơi quản lý việc chấm.
Đây chính là sức mạnh của bounded context: mỗi phần giữ đúng trách nhiệm của nó.
---
13.36. Checklist khi thiết kế bounded context
Khi thiết kế hoặc review một hệ thống, hãy hỏi:
- Context này giải quyết nghiệp vụ gì?
- Những thuật ngữ chính trong context là gì?
- Những thuật ngữ đó có bị trùng nghĩa với context khác không?
- Model nào thuộc quyền sở hữu của context này?
- Dữ liệu nào là nguồn sự thật của context này?
- Context này nhận dữ liệu từ đâu?
- Context này phát event hoặc cung cấp API nào?
- Context khác có được sửa dữ liệu của context này không?
- Có phần shared nào đang quá lớn không?
- Có model nào đang gánh quá nhiều nghĩa không?
- Có cần tách service thật không, hay chỉ cần tách module?
- Khi context này thay đổi, ai bị ảnh hưởng?
Nếu trả lời được các câu này, ranh giới hệ thống sẽ rõ hơn rất nhiều.
---
13.37. Kết luận của chương
Bounded Context là một trong những khái niệm quan trọng nhất khi thiết kế hệ thống theo nghiệp vụ.
Nó giúp ta hiểu rằng:
- Một từ có thể có nhiều nghĩa.
- Một model dùng chung cho mọi nơi thường không bền.
- Mỗi khu vực nghiệp vụ nên có ngôn ngữ và quy tắc riêng.
- Context không nhất thiết là microservice.
- Tách nghĩa trước, tách module sau, tách service khi thật sự cần.
- Dữ liệu đi qua ranh giới context nên được dịch nghĩa.
- Shared càng lớn, boundary càng yếu.
Thông điệp quan trọng nhất:
> Đừng cố tạo một mô hình duy nhất cho toàn bộ doanh nghiệp. Hãy để mỗi khu vực nghiệp vụ có mô hình đúng với ngôn ngữ và trách nhiệm của nó.
Khi hiểu Bounded Context, ta sẽ bớt bị ám ảnh bởi câu hỏi "nên có bao nhiêu service?".
Câu hỏi tốt hơn là:
> Hệ thống của ta có những khu vực nghĩa nào, ai sở hữu chúng, và chúng nên giao tiếp với nhau ra sao?
Trả lời tốt câu hỏi này, ta sẽ thiết kế monolith tốt hơn, modular monolith tốt hơn, và nếu sau này cần microservices, ta cũng tách đúng hơn.