158 lines
5.9 KiB
Markdown
158 lines
5.9 KiB
Markdown
# DOM — Domain Model — <PROJECT>
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Version** | 1.0 |
|
|
| **Date** | YYYY-MM-DD |
|
|
| **Author** | <SA> (skill sa-2-architecture) |
|
|
| **Status** | 🟡 Draft |
|
|
| **Approved by** | Tech Lead: — · BE Lead: — |
|
|
| **Source** | SAD_… v1.0 · DAT_… v1.0 · BR_… của BA · BACKLOG_… của BA |
|
|
| **Scope** | |
|
|
| **Confidence** | 🟡 |
|
|
|
|
## Change Log
|
|
|
|
| Version | Date | Người sửa | Thay đổi | ADR |
|
|
|---|---|---|---|---|
|
|
| 1.0 | | | Bản đầu | — |
|
|
|
|
> **Thẩm quyền khi mâu thuẫn:** sơ đồ thắng về **quan hệ và luồng**; bảng thắng về **ràng buộc và
|
|
> con số**. `BR` của BA là **nguồn** của mọi invariant ở đây; `DOM` chỉ nói invariant đó sống ở
|
|
> class nào. Lệch với `BR` ⇒ `OQ`, không tự sửa rule.
|
|
|
|
> **Vì sao có tài liệu này:** `DAT` nói *ai sở hữu dữ liệu gì*, `PDM` nói *bảng vật lý trông thế
|
|
> nào*. Không có `DOM`, dev tự suy ra aggregate từ bảng — và mỗi người suy một kiểu, transaction
|
|
> boundary lệch nhau, invariant bị kiểm ở ba chỗ hoặc không chỗ nào.
|
|
|
|
---
|
|
|
|
## 1. Bounded context và aggregate
|
|
|
|
*Một dòng mỗi aggregate. Aggregate là **ranh giới transaction**: mọi invariant trong aggregate được
|
|
kiểm trong một lần ghi; giữa các aggregate chỉ có eventual consistency (`DAT` §4).*
|
|
|
|
| Bounded context | `CMP-nn` sở hữu | Aggregate root | Entity con | Value object | Invariant chính (`BR`) | Sự kiện phát ra |
|
|
|---|---|---|---|---|---|---|
|
|
| Cart & Order | `CMP-04` | `Order` | `OrderSeller`, `OrderItem` | `Money`, `Address` | `BR-014` tổng tiền = Σ dòng | `OrderPlaced` |
|
|
| | | | | | | |
|
|
|
|
🔴 **Một entity thuộc đúng một aggregate.** Entity xuất hiện ở hai aggregate ⇒ một trong hai chỉ
|
|
được giữ **id tham chiếu** (không giữ object), ghi rõ ở §3.
|
|
|
|
---
|
|
|
|
## 2. Class diagram theo bounded context
|
|
|
|
*Một sơ đồ mỗi bounded context, ≤ 12 class. Chỉ vẽ thuộc tính nghiệp vụ và phương thức đổi trạng
|
|
thái; getter/setter, DTO, repository không vẽ ở đây (thuộc `AGD`).*
|
|
|
|
### 2.1 <Bounded context>
|
|
|
|
<!-- archify: mermaid-only -->
|
|
```mermaid
|
|
classDiagram
|
|
class Order {
|
|
+OrderId id
|
|
+CustomerId customerId
|
|
+Money totalAmount
|
|
+OrderStatus status
|
|
+place(cart) Order
|
|
+cancel(reason)
|
|
}
|
|
class OrderSeller {
|
|
+SellerId sellerId
|
|
+Money subtotal
|
|
+confirm()
|
|
}
|
|
class OrderItem {
|
|
+ProductVariantId variantId
|
|
+int quantity
|
|
+Money unitPriceSnapshot
|
|
}
|
|
class Money {
|
|
<<value object>>
|
|
+decimal amount
|
|
+Currency currency
|
|
}
|
|
Order "1" *-- "1..*" OrderSeller : splits into
|
|
OrderSeller "1" *-- "1..*" OrderItem : contains
|
|
Order ..> Money
|
|
OrderItem ..> Money
|
|
```
|
|
|
|
**Bảng đi kèm** *(W13 — sơ đồ không nói được invariant kiểm ở đâu và ai được gọi)*:
|
|
|
|
| Class | Loại | Thuộc tính nghiệp vụ | Invariant (`BR`) | Kiểm ở phương thức | Ai được gọi (`ROLE`/`CMP`) | Bảng `PDM` |
|
|
|---|---|---|---|---|---|---|
|
|
| `Order` | aggregate root | `totalAmount`, `status` | `BR-014`, `BR-021` | `place()`, `cancel()` | `ROLE-01` qua `IF-002` | `TBL-012 order` |
|
|
| `Money` | value object | `amount`, `currency` | `BR-003` không âm | constructor | — | cột `*_amount` |
|
|
|
|
**Quan hệ:** `*--` composition (cùng transaction) · `o--` aggregation (khác vòng đời) · `..>` phụ
|
|
thuộc · `-->` tham chiếu bằng id sang aggregate khác.
|
|
|
|
### 2.2 <Bounded context tiếp theo>
|
|
|
|
*(cùng cấu trúc)*
|
|
|
|
---
|
|
|
|
## 3. Tham chiếu xuyên aggregate
|
|
|
|
*Chỗ hay sai nhất: giữ object của aggregate khác thay vì id, rồi load cả cây.*
|
|
|
|
| Từ aggregate | Tham chiếu tới | Bằng gì | Đọc dữ liệu kia bằng | Trễ chấp nhận (`DAT` §4) |
|
|
|---|---|---|---|---|
|
|
| `Order` | `Customer` | `CustomerId` | `IF-0nn` / read model | eventual ≤ … |
|
|
| `OrderItem` | `ProductVariant` | `ProductVariantId` + snapshot giá/tên | snapshot lúc đặt | không cần đồng bộ |
|
|
|
|
🔴 **Snapshot hay tham chiếu sống?** Giá, tên, địa chỉ lúc giao dịch phải là **snapshot** (thay đổi
|
|
sau đó không được đổi lịch sử). Ghi rõ từng trường, đây là nguồn của nhiều bug đối soát.
|
|
|
|
---
|
|
|
|
## 4. Vòng đời trạng thái → class
|
|
|
|
*Bảng chuyển trạng thái nằm ở `BR` §3; đây chỉ map xuống class và phương thức.*
|
|
|
|
| Entity | Enum trạng thái | Thuộc `BR` | Phương thức chuyển | Guard kiểm ở đâu | Ghi vết (`SEC` §audit) |
|
|
|---|---|---|---|---|---|
|
|
| `Order` | `OrderStatus` | `BR-021` | `confirm()`, `cancel()` | trong aggregate | `order_status_history` |
|
|
|
|
---
|
|
|
|
## 5. Domain event
|
|
|
|
| Sự kiện | Phát từ aggregate | Khi nào | Payload tối thiểu | Người nhận (`CMP`) | Contract (`CTR`) |
|
|
|---|---|---|---|---|---|
|
|
| `OrderPlaced` | `Order` | `place()` commit xong | `orderId`, `customerId`, `sellerIds[]`, `totalAmount` | `CMP-06`, `CMP-09` | `contracts/asyncapi/order-events.yaml#OrderPlaced` |
|
|
|
|
🔴 Payload event **không chứa PII** ngoài id trừ khi `SEC` §5 cho phép đích danh.
|
|
|
|
---
|
|
|
|
## 6. Đối chiếu
|
|
|
|
| Kiểm | Kết quả | Hành động |
|
|
|---|---|---|
|
|
| Mọi thực thể trong `DAT` §1 có class ở §2 | n/n | |
|
|
| Mọi `BR` loại "ràng buộc dữ liệu"/"tính toán" có class kiểm | n/n | |
|
|
| Mọi entity có trạng thái ở `BR` §3 có enum + phương thức ở §4 | n/n | |
|
|
| Mọi class root có bảng trong `PDM` §2 | n/n | *(điền khi `PDM` xong)* |
|
|
|
|
## 7. Giả định & Ngoài phạm vi
|
|
|
|
**Giả định:**
|
|
|
|
| ID | Giả định | Cách xác minh | Nếu sai |
|
|
|---|---|---|---|
|
|
|
|
**Ngoài phạm vi:**
|
|
|
|
- *(ví dụ: không mô hình hoá module báo cáo — chỉ đọc, dùng read model của `DAT` §4)*
|
|
|
|
## 8. Open Questions
|
|
|
|
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|
|
|---|---|---|---|---|---|
|