Domain model — Aggregates & Ownership
Hệ thống tổ chức theo Domain-Driven Design: dữ liệu gom thành aggregate (cụm entity xử lý như một khối nhất quán), và mỗi khái niệm có đúng một owner. Hiểu ranh giới này giúp tránh phá vỡ nhất quán khi sửa code.
Aggregate là gì?
Một aggregate có một root và một ranh giới: chỉ được sửa state bên trong qua root, và một giao dịch chỉ nên thay đổi một aggregate. Ví dụ trong AIMY:
| Aggregate (root) | Sở hữu | Ghi chú |
|---|---|---|
| Contact / Account Holder | Contact (TypeId = AccountHolder), Contact.ProfileImage | "Account Holder" không phải aggregate Account; Account không có avatar. |
| AccountPaymentCustomer | AccountId, BusinessUnitId, PaymentProvider, CustomerToken, StatusId | BusinessUnitId set lúc tạo, không đổi sau đó; truy cập phải lọc theo BU (cross-BU = vi phạm bảo mật). |
| TermBookingOrder (booking root) | LockTypeId, Version (ROWVERSION), IsDepositPayment, deposit snapshot | Một Order = một slot giao dịch deposit. |
| Billing | Billing.PaymentOptionId và normalization | Billing sở hữu PaymentOptionId độc quyền; tương tác cross-domain chỉ read/emit event. |
| Enrollment (Subscription) | state runtime ghi danh | Xem Subscription. |
📌 Đây là ví dụ minh họa từ vài feature (deposit, billing, dashboard), không phải ma trận đầy đủ.
Ma trận domain theo ownership
Các domain gom theo ownership mức cao như sau:
| Domain | Owner chính | Chỉ đọc / phụ thuộc |
|---|---|---|
| Finance | Billing, Invoice, CreditNote, FinanceTransaction projection | Booking/Subscription line data, BusinessUnit invoice settings, Payment paid state, Xero provider config. |
| Payment | AccountPaymentCustomer, DirectPaymentBatch, DirectPayment, provider checkout result | Billing/Invoice context, BookingOrder lock/deposit snapshot, BusinessUnit payment settings. |
| Master Data | Enterprise, BusinessUnit, Org/Site, Contact, Attendee, Program/Term/TPS/Product | Runtime domains đọc để xác định scope, account, attendee, session, pricing/availability context. |
Nguyên tắc áp dụng: Finance có thể đọc Booking/Subscription để tính chứng từ nhưng không sở hữu booking/enrollment runtime; Payment có thể đọc invoice/billing để thu tiền nhưng không sở hữu invoice lifecycle; Master Data được đọc rộng nhưng mỗi entity nền vẫn mutate qua owner riêng.
Nguyên tắc ownership (rất quan trọng)
- Mỗi khái niệm có đúng một owner aggregate; chỉ owner được mutate.
- Cross-aggregate chỉ được read hoặc emit event (append-only) — không sửa trực tiếp state của aggregate khác.
- Vi phạm điển hình (đã ghi nhận):
TermBookingOrder.LockTypeIdbị mutate bởi cả service layer lẫn controller →BOUNDARY_VIOLATION(rủi ro race).
Ví dụ áp dụng trong wiki: Subscription — UI Setup không trực tiếp sửa Enrollment/Invoice/Booking; mọi mutation runtime đi qua service confirmation hiện có.
Service → aggregate responsibility
Mỗi service khai rõ aggregate nào nó OWNS (được sửa) vs READS (chỉ đọc). Ví dụ (billing preview — read-only computation):
| Service | OWNS | READS |
|---|---|---|
BillingService.PreviewBillingDifferenceAsync | BillingPreview (transient) | Billing, Invoice, settings |
DashboardService.* | DashboardQueryContext (cache) | nhiều aggregate (read-only) |
DepositEligibilityService | EligibilityPolicy | ProgramCategory, SubsidyPolicy |
Read-only computation (Dashboard, Billing Preview) không mutate domain state → không có ownership conflict.
Contracts — hợp đồng giữa các phần
Một số hợp đồng API ổn định giữa thành phần, vd kết nối staff–BU. Quy tắc tiêu biểu: một staff không thể có quá một Employee link active cho cùng một BusinessUnit.
Liên quan
- Quy ước & nguyên tắc — nguyên tắc kỹ thuật xương sống.
- Database & lưu trữ — kho & khóa.