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.PreviewBookingActionDifferenceAsync | Billing Difference projection (transient) | Billing, Invoice, settings, booking/order context |
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.
Ownership của các capability mới
| Capability | Owner / nguồn sự thật | Đọc bởi |
|---|---|---|
| Unified settings | Settings hierarchy của Enterprise/BU/Org/ProgramCategory | Topi, Tuke và các domain cần cấu hình theo scope. |
| Subscription runtime | Subscription, Enrollment, billing schedule và invoice schedule | Customer Portal/Finance chỉ đọc hoặc gọi service owner. |
| Closure Day adjustment | Finance transaction/invoice projection | Finance audit và các màn hình billed/credit balance. |
| Notification trace | NotificationJob/OutgoingMessage theo SourceKey | Communication và support/observability. |
Ví dụ thực tế: Customer Portal không tự sửa invoice khi customer Extend; nó gọi consumer flow, còn Subscription service sở hữu việc tạo period mới và giữ nguyên invoice cũ.
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.