Chương 14. Aggregate: Chỗ Nào Cần Chặt, Chỗ Nào Nên Lỏng

Ở chương 12, ta nói về Domain: thế giới nghiệp vụ.

Ở chương 13, ta nói về Bounded Context: ranh giới nơi một từ có một nghĩa rõ ràng.

Chương này đi sâu hơn một tầng:

> Bên trong một context, dữ liệu nào phải được giữ đúng cùng nhau?

Đó là câu hỏi của Aggregate.

Nếu nói rất ngắn:

> Aggregate là một cụm dữ liệu và quy tắc phải nhất quán với nhau tại cùng một thời điểm.

Nói đời thường hơn:

> Aggregate là một "khối nghiệp vụ" mà khi thay đổi, ta phải thay đổi qua một cửa chính, theo đúng luật, để nó không rơi vào trạng thái sai.

Aggregate giúp ta trả lời những câu hỏi rất thực tế:

  • Khi nào cần transaction?
  • Những bảng nào nên sửa cùng nhau?
  • Dữ liệu nào không được ai sửa trực tiếp?
  • Chỗ nào cần đúng ngay lập tức?
  • Chỗ nào có thể cập nhật sau vài giây?
  • Một object nên to đến đâu?
  • Khi nào tách model ra để hệ thống đỡ nghẽn?

Aggregate không phải để làm code trông "enterprise".

Nó là công cụ để kiểm soát sự đúng đắn.

---

14.1. Ví dụ quán bánh: có thứ phải đúng ngay

Giả sử khách đặt một hộp bánh gồm 3 món:

  • 1 bánh chocolate.
  • 1 bánh tiramisu.
  • 1 bánh dâu.

Khi khách bấm đặt hàng, hệ thống tạo đơn.

Trong đơn đó, có vài thứ phải đúng cùng nhau:

  • Đơn phải có ít nhất một món.
  • Mỗi món phải có số lượng lớn hơn 0.
  • Tổng tiền phải khớp với các dòng hàng.
  • Trạng thái ban đầu phải hợp lệ.
  • Không thể vừa CANCELLED vừa PAID.
  • Nếu đơn đã xác nhận, không được tự ý sửa món theo cách phá luật.

Nếu hệ thống lưu được đơn nhưng mất dòng hàng, đó là sai.

Nếu dòng hàng có giá nhưng tổng tiền tính sai, đó là sai.

Nếu đơn không có khách nhưng vẫn được tạo, đó là sai.

Những thứ này cần được giữ chặt.

Ta có thể xem Order cùng các OrderItem là một aggregate:

Order Aggregate
├── Order
└── OrderItem[]

Khi tạo hoặc sửa đơn, ta không nên để code bên ngoài tùy tiện sửa từng dòng hàng, tổng tiền, trạng thái.

Nên đi qua hành vi của Order:

order.add_item(...)
order.remove_item(...)
order.confirm()
order.cancel(...)

Tức là thay vì ai cũng chọc vào dữ liệu, ta để aggregate bảo vệ luật của nó.

---

14.2. Aggregate là gì?

Aggregate là một nhóm object thuộc cùng một ranh giới nhất quán.

Trong nhóm đó có một object chính gọi là Aggregate Root.

Mọi thay đổi từ bên ngoài nên đi qua root.

Ví dụ:

Order Aggregate
Root: Order
Children: OrderItem

Bên ngoài không nên sửa OrderItem trực tiếp.

Bên ngoài nên yêu cầu Order làm việc:

Order.add_item(product, quantity)
Order.change_quantity(item_id, quantity)
Order.confirm()
Order.cancel(reason)

Vì sao?

Order biết luật của đơn hàng.

Ví dụ:

  • Không cho đơn rỗng.
  • Không cho số lượng âm.
  • Không cho sửa món sau khi đơn đã giao.
  • Khi thay đổi item thì tính lại tổng tiền.
  • Khi hủy đơn thì kiểm tra trạng thái hiện tại.

Nếu ai cũng sửa OrderItem trực tiếp, luật nằm rải rác khắp hệ thống.

Aggregate gom luật vào nơi sở hữu dữ liệu.

---

14.3. Root là cửa chính

Hãy tưởng tượng aggregate như một căn nhà.

Root là cửa chính.

Muốn thay đổi đồ trong nhà, phải đi qua cửa chính.

Không ai được leo cửa sổ vào sửa đồ.

Ví dụ:

Order
  - items
  - status
  - total_amount

Code bên ngoài không nên làm:

order.items[0].quantity = -3
order.total_amount = 100
order.status = "PAID"

Vì những dòng này có thể phá luật.

Code bên ngoài nên làm:

order.change_quantity(item_id, 2)
order.confirm_payment(payment_id)
order.cancel(reason)

Tên method nên thể hiện ý định nghiệp vụ, không chỉ là setter kỹ thuật.

set_status("CANCELLED") yếu hơn cancel(reason).

cancel(reason) có thể kiểm tra:

  • Đơn hiện tại có được hủy không?
  • Có cần hoàn tiền không?
  • Có cần phát event không?
  • Có cần lưu lý do không?

Root không chỉ giữ dữ liệu.

Root giữ luật.

---

14.4. Aggregate giải quyết vấn đề gì?

Aggregate giải quyết ba vấn đề chính.

Vấn đề 1: Luật nghiệp vụ bị rải rác

Nếu logic nằm khắp nơi:

API kiểm tra một ít
Service kiểm tra một ít
Worker kiểm tra một ít
Admin kiểm tra một ít
Database trigger kiểm tra một ít

Thì rất dễ có đường đi bỏ qua luật.

Aggregate giúp gom luật quan trọng vào một chỗ.

Vấn đề 2: Dữ liệu bị sửa sai

Nếu ai cũng có thể sửa field:

order.status = "PAID"

Thì không ai chắc trạng thái đó có hợp lệ không.

Aggregate bắt thay đổi phải đi qua hành vi:

order.mark_paid(payment_id)

Vấn đề 3: Không biết transaction nên bao quanh cái gì

Transaction không nên quá nhỏ đến mức dữ liệu dễ lệch.

Nhưng cũng không nên quá to đến mức khóa cả hệ thống.

Aggregate giúp trả lời:

> Những dữ liệu nào cần đúng cùng nhau thì nằm trong cùng một transaction.

Ví dụ tạo đơn:

Tạo Order
Tạo OrderItem
Tính total
Lưu trạng thái ban đầu

Những thứ này nên thành công hoặc thất bại cùng nhau.

---

14.5. Aggregate không phải là bảng

Một lỗi phổ biến:

> Mỗi bảng là một aggregate.

Không đúng.

Aggregate là ranh giới nghiệp vụ, không phải ranh giới database.

Một aggregate có thể lưu bằng nhiều bảng.

Ví dụ:

Order Aggregate
Database:
- orders
- order_items

Một bảng cũng có thể chỉ là read model, không phải aggregate.

Ví dụ:

order_summary_view

Bảng này chỉ để đọc nhanh trên admin, không phải nơi xử lý nghiệp vụ.

Cũng không nên nghĩ:

Foreign key = cùng aggregate

Không đúng.

Ordercustomer_id, nhưng Customer không nhất thiết nằm trong cùng aggregate với Order.

Order có thể tham chiếu customer bằng id.

Không cần nhét toàn bộ customer vào order.

---

14.6. Aggregate không phải là object graph càng to càng tốt

Một lỗi ngược lại là tạo aggregate quá to.

Ví dụ:

Customer Aggregate
├── Customer
├── Orders
├── Payments
├── Deliveries
├── Refunds
├── SupportCases
└── Coupons

Nghe có vẻ đầy đủ.

Nhưng nếu mỗi lần sửa thông tin khách phải load tất cả đơn, thanh toán, giao hàng, khiếu nại, thì hệ thống sẽ nặng.

Aggregate quá to gây nhiều vấn đề:

  • Transaction lớn.
  • Lock lâu.
  • Dễ nghẽn khi nhiều người sửa.
  • Code khó hiểu.
  • Một thay đổi nhỏ kéo theo nhiều dữ liệu không cần thiết.
  • Khó scale.

Aggregate tốt thường nhỏ vừa đủ để bảo vệ luật thật sự cần bảo vệ.

Không phải gom mọi thứ có liên quan vào một chỗ.

---

14.7. Câu hỏi quan trọng: có cần đúng ngay không?

Khi phân vân có nên đặt hai object vào cùng aggregate không, hãy hỏi:

> Hai thứ này có cần đúng cùng nhau ngay lập tức không?

Nếu có, có thể cùng aggregate.

Nếu không, nên cân nhắc tách ra.

Ví dụ:

Order và OrderItem

Một đơn hàng và các dòng hàng thường cần đúng cùng nhau.

Không nên có đơn hàng với tổng tiền không khớp dòng hàng.

Có thể cùng aggregate.

Order và Payment

Đơn hàng và thanh toán có liên quan chặt.

Nhưng có nhất thiết phải là cùng aggregate không?

Thường là không.

Payment có vòng đời riêng:

  • Created.
  • Authorized.
  • Captured.
  • Failed.
  • Refunded.

Order cũng có vòng đời riêng:

  • Draft.
  • Confirmed.
  • Paid.
  • Preparing.
  • Delivered.
  • Cancelled.

Payment có thể thất bại, retry, hoàn tiền, đối soát.

Nếu nhét Payment vào Order Aggregate, Order sẽ phình to.

Cách tốt hơn thường là:

Order Aggregate phát OrderConfirmed
Payment Aggregate xử lý thanh toán
Payment phát PaymentSucceeded hoặc PaymentFailed
Order cập nhật trạng thái từ event hoặc command phù hợp

Ở đây có sự nhất quán theo thời gian, không cần mọi thứ đúng trong cùng một transaction.

Order và Delivery

Delivery cũng có vòng đời riêng:

  • Waiting pickup.
  • Picked up.
  • Delivering.
  • Delivered.
  • Failed.
  • Returning.

Nó không cần nằm trong Order Aggregate.

Order chỉ cần biết kết quả quan trọng để hiển thị cho khách.

---

14.8. Chặt và lỏng nghĩa là gì?

Trong chương này, "chặt" nghĩa là:

  • Phải kiểm soát bằng luật.
  • Phải thay đổi qua một cửa.
  • Thường cần transaction.
  • Không cho sửa trực tiếp lung tung.
  • Sai là ảnh hưởng nghiệp vụ nghiêm trọng.

"Lỏng" nghĩa là:

  • Có thể cập nhật sau.
  • Có thể đồng bộ bằng event.
  • Có thể chấp nhận trễ vài giây.
  • Không cần nằm cùng transaction.
  • Sai tạm thời có thể sửa được.

Ví dụ:

Cần chặt:
- Order và OrderItem.
- Payment và PaymentAttempt.
- Wallet và LedgerEntry.
- Booking và SeatHold.
- QuizAttempt và Answer.

Có thể lỏng:
- Order và EmailNotification.
- Order và AnalyticsEvent.
- Payment và báo cáo doanh thu realtime.
- User profile và recommendation.
- Submission và notification kết quả.

Không phải cái gì liên quan nhau cũng cần chặt.

Trong hệ thống phân tán, nếu cố làm mọi thứ chặt, hệ thống sẽ chậm, dễ nghẽn, khó mở rộng.

---

14.9. Ví dụ: giỏ hàng có phải aggregate không?

Giỏ hàng là ví dụ thú vị.

Trong e-commerce, Cart có thể là một aggregate:

Cart
├── CartItem[]

Luật:

  • Một cart thuộc một khách hoặc một session.
  • Số lượng item không âm.
  • Có thể thêm/xóa/sửa item.
  • Có thể tính tổng tạm.

Nhưng cart thường không cần quá chặt như order.

Vì sao?

Cart là trạng thái tạm.

Nếu giá thay đổi, cart có thể tính lại khi checkout.

Nếu tồn kho thay đổi, cart không nhất thiết giữ chỗ ngay.

Nếu khách thêm item rồi bỏ đi, không sao.

Vì vậy:

  • Cart có thể chấp nhận mềm hơn.
  • Order cần chặt hơn.
  • Payment cần chặt hơn nữa.

Đây là tư duy quan trọng:

> Mức độ chặt phụ thuộc vào rủi ro nghiệp vụ, không phụ thuộc vào việc object nghe có vẻ quan trọng hay không.

---

14.10. Ví dụ: đặt vé và giữ ghế

Hệ thống đặt vé là nơi Aggregate rất dễ hiểu.

Giả sử có một ghế A1 cho buổi hòa nhạc.

Luật quan trọng:

> Không được bán cùng một ghế cho hai người.

Ở đây, chỗ cần chặt là quyền sở hữu tạm thời hoặc cuối cùng của ghế trong một sự kiện.

Ta có thể có aggregate:

SeatInventory Aggregate
Root: SeatInventory hoặc EventSeat

Nó bảo vệ luật:

  • Ghế trống thì được giữ.
  • Ghế đang giữ thì người khác không được giữ.
  • Giữ chỗ có thời hạn.
  • Hết hạn thì ghế được thả.
  • Đã bán thì không được bán lại.

Nếu hai người cùng bấm mua ghế A1, hệ thống phải đảm bảo chỉ một người thành công.

Đây là consistency cần chặt.

Nhưng email xác nhận vé có thể gửi sau.

Analytics về lượt mua vé có thể cập nhật sau.

Recommendation "sự kiện tương tự" có thể cập nhật sau.

Không nên đưa tất cả vào transaction giữ ghế.

---

14.11. Ví dụ: ví tiền và ledger

Ví tiền là nơi cần cực kỳ chặt.

Nếu user có số dư 1.000.000 đồng, hệ thống không được để họ rút 1.200.000 đồng.

Không được mất tiền.

Không được cộng tiền hai lần.

Không được có giao dịch không có dấu vết.

Thiết kế thường nên xoay quanh ledger:

Wallet Aggregate
├── Wallet
└── LedgerEntry[]

Hoặc tùy hệ thống, LedgerEntry có thể là aggregate riêng, nhưng nguyên tắc vẫn là:

  • Mỗi thay đổi số dư phải có bút toán.
  • Không sửa số dư trực tiếp mà không có lý do.
  • Giao dịch phải idempotent.
  • Không xử lý trùng request làm cộng/trừ tiền hai lần.

Ví dụ hành vi:

wallet.deposit(amount, transaction_id)
wallet.withdraw(amount, transaction_id)
wallet.reserve(amount, order_id)
wallet.release_reservation(reservation_id)

Không nên:

wallet.balance -= amount

ở khắp nơi.

Với tiền, aggregate và transaction rất quan trọng.

Những thứ như gửi email "bạn vừa nạp tiền" thì không cần nằm trong cùng transaction với cập nhật ledger.

---

14.12. Ví dụ: AI Judge

Quay lại bài toán chấm bài bằng AI.

Ta có thể có các aggregate khác nhau.

Submission Aggregate

Quan tâm đến việc học viên nộp bài:

Submission
├── content
├── assignment_id
├── student_id
├── submitted_at
└── version

Luật:

  • Không nộp nếu assignment đã đóng.
  • Có thể nộp lại hay không tùy quy định.
  • Mỗi lần nộp có version.
  • Nội dung không được rỗng.

GradingJob Aggregate

Quan tâm đến việc chấm:

GradingJob
├── submission_id
├── rubric_id
├── status
├── attempt_count
├── model_name
├── score
└── feedback

Luật:

  • Job mới tạo có trạng thái pending.
  • Đang chạy thì không chạy lại song song tùy chính sách.
  • Thất bại thì retry tối đa N lần.
  • Thành công thì ghi score/feedback.
  • Không được ghi kết quả nếu job đã bị hủy.

Notification

Thông báo kết quả có thể là aggregate riêng hoặc job riêng.

Nó không cần nằm trong Submission.

Nếu chấm xong mà email gửi chậm 10 giây, hệ thống vẫn ổn.

Nhưng nếu GradingJob vừa SUCCEEDED vừa không có score trong trường hợp bắt buộc phải có score, đó là sai.

Vậy chỗ cần chặt là trong GradingJob.

Chỗ có thể lỏng là notification, analytics, cập nhật dashboard.

---

14.13. Aggregate và transaction

Một quy tắc thực dụng:

> Một transaction thường nên bảo vệ một aggregate.

Không phải lúc nào cũng tuyệt đối, nhưng là hướng tốt.

Ví dụ tạo order:

begin transaction
  create order
  create order items
  calculate total
  save order event
commit

Sau commit, hệ thống có thể phát event:

OrderPlaced

Payment, kitchen, notification có thể xử lý sau.

Không nên cố làm tất cả trong một transaction:

begin transaction
  create order
  charge payment gateway
  create kitchen ticket
  assign shipper
  send email
  update analytics
commit

Thiết kế này nguy hiểm vì:

  • Gọi bên ngoài trong transaction lâu.
  • Dễ lock database.
  • Nếu email lỗi thì order có rollback không?
  • Nếu shipper service chậm thì khách đợi lâu.
  • Nếu payment gateway timeout thì trạng thái rất khó xử lý.

Transaction nên bảo vệ sự đúng đắn cục bộ.

Luồng dài nên dùng event, queue, saga hoặc workflow.

---

14.14. Aggregate và eventual consistency

Khi tách aggregate, ta thường chấp nhận eventual consistency.

Nghĩa là:

> Dữ liệu giữa các phần có thể chưa khớp ngay lập tức, nhưng sẽ khớp sau một khoảng thời gian ngắn.

Ví dụ:

Khách đặt hàng xong:

Ordering tạo Order = CONFIRMED
Payment đang xử lý
Kitchen chưa nhận ticket
Notification chưa gửi

Trong vài giây đầu, các phần chưa hoàn toàn đồng bộ.

Nhưng hệ thống không sai nếu trạng thái đó được thiết kế rõ.

Sai là khi:

  • Không biết phần nào là nguồn sự thật.
  • Event bị mất.
  • Retry tạo trùng dữ liệu.
  • UI hiển thị như thể mọi thứ đã xong trong khi chưa xong.
  • Không có cách sửa khi đồng bộ thất bại.

Eventual consistency không có nghĩa là "thích thì sai".

Nó nghĩa là:

> Ta biết phần nào được phép trễ, trễ bao lâu, và xử lý thế nào nếu trễ quá lâu.

---

14.15. Không phải mọi thứ đều cần strong consistency

Strong consistency nghĩa là dữ liệu phải đúng ngay, nhìn từ mọi nơi.

Điều này cần thiết ở một số chỗ:

  • Số dư ví.
  • Bán vé cùng một ghế.
  • Trừ tồn kho giới hạn.
  • Cấp quyền truy cập bảo mật.
  • Ghi nhận giao dịch tài chính.

Nhưng nhiều chỗ không cần strong consistency:

  • Lượt xem video.
  • Số like.
  • Gợi ý sản phẩm.
  • Email thông báo.
  • Dashboard thống kê realtime.
  • Feed hoạt động.
  • Search index.

Nếu bắt mọi thứ strong consistency, hệ thống sẽ đắt và chậm.

Senior thường không hỏi "làm sao cho mọi thứ đúng ngay?".

Họ hỏi:

> Chỗ nào thật sự cần đúng ngay, và chỗ nào chỉ cần đúng sau?

Đây là tinh thần của Aggregate.

---

14.16. Aggregate quá nhỏ thì sao?

Aggregate quá to gây nghẽn.

Nhưng aggregate quá nhỏ cũng gây rối.

Ví dụ nếu tách:

Order Aggregate
OrderItem Aggregate
Discount Aggregate
ShippingFee Aggregate
Tax Aggregate

Mỗi dòng hàng, mỗi khoản giảm giá, mỗi phí vận chuyển đều là aggregate riêng.

Khi checkout, hệ thống phải phối hợp quá nhiều aggregate để đảm bảo đơn đúng.

Rủi ro:

  • Logic phân tán.
  • Transaction khó.
  • Dễ có trạng thái lệch.
  • Code xử lý luồng chính dài.
  • Debug khó.

Nếu các phần luôn phải thay đổi cùng nhau, tách quá nhỏ sẽ làm hệ thống phức tạp không cần thiết.

Quy tắc:

> Những thứ phải đúng cùng nhau nên ở gần nhau.

Nhưng:

> Những thứ có vòng đời riêng và không cần đúng ngay nên tách ra.

---

14.17. Aggregate quá to thì sao?

Aggregate quá to thường xuất hiện khi ta nhầm "có liên quan" với "phải cùng một khối".

Ví dụ:

Order
├── Items
├── Payment
├── Refund
├── Delivery
├── Invoice
├── SupportCase
├── Notifications
└── Analytics

Mọi thứ đúng là có liên quan đến đơn hàng.

Nhưng không phải mọi thứ phải nằm trong Order Aggregate.

Hậu quả:

  • Mỗi lần load order quá nặng.
  • Nhiều luồng cùng muốn sửa order.
  • Dễ conflict.
  • Một field nhỏ thay đổi cũng phải hiểu cả model lớn.
  • Test phức tạp.
  • Khó chia team.
  • Khó scale.

Aggregate quá to thường là dấu hiệu của việc chưa hiểu vòng đời riêng của từng phần.

Payment có vòng đời payment.

Delivery có vòng đời delivery.

Support có vòng đời support case.

Invoice có vòng đời kế toán.

Order không nên nuốt tất cả.

---

14.18. Dùng id thay vì giữ object bên ngoài

Một quy tắc DDD thực dụng:

> Aggregate này nên tham chiếu aggregate khác bằng id, không giữ nguyên object bên trong.

Ví dụ Order có:

customer_id
shipping_address_snapshot

Không nhất thiết giữ toàn bộ object Customer.

Vì Customer có thể thay đổi email, tên, hạng thành viên.

Nhưng đơn hàng cần biết thông tin tại thời điểm đặt.

Ta có thể lưu snapshot cần thiết:

Order
- customer_id
- customer_name_at_order_time
- phone_at_order_time
- delivery_address_at_order_time

Tại sao không đọc trực tiếp từ Customer mỗi lần?

Vì nếu khách đổi địa chỉ sau khi đặt, đơn cũ không nên tự đổi địa chỉ giao.

Đây là ví dụ rất hay:

> Dữ liệu giống nhau trong đời thật, nhưng trong nghiệp vụ có thể cần lưu tại các thời điểm khác nhau.

Aggregate giúp ta quyết định dữ liệu nào thuộc về sự thật của chính nó.

---

14.19. Aggregate và invariant

Một từ quan trọng khi nói về Aggregate là invariant.

Nghe hơi học thuật, nhưng nghĩa đơn giản:

> Invariant là điều kiện luôn phải đúng.

Ví dụ với Order:

  • Tổng tiền không được âm.
  • Đơn phải có ít nhất một item khi confirm.
  • Item quantity phải lớn hơn 0.
  • Đơn đã delivered thì không được cancel như đơn mới.

Với Wallet:

  • Số dư không được âm nếu không cho overdraft.
  • Mỗi transaction id chỉ được xử lý một lần.
  • Mỗi thay đổi số dư phải có ledger entry.

Với Booking:

  • Một ghế không được bán cho hai người.
  • Hold hết hạn thì không được checkout.
  • Booking đã paid thì không được tự chuyển về draft.

Aggregate tồn tại để bảo vệ invariant.

Nếu không có invariant quan trọng, có thể ta không cần aggregate phức tạp.

Chỉ cần CRUD đơn giản.

---

14.20. CRUD và Aggregate khác nhau thế nào?

CRUD là:

  • Create.
  • Read.
  • Update.
  • Delete.

CRUD phù hợp với dữ liệu đơn giản:

  • Danh mục.
  • Tag.
  • Cấu hình.
  • Banner.
  • Trang tĩnh.
  • Ghi chú nội bộ.

Aggregate phù hợp khi có luật nghiệp vụ quan trọng.

Ví dụ:

CRUD đơn giản:

Admin sửa tên category
Admin bật/tắt banner
Admin cập nhật mô tả sản phẩm

Aggregate:

Khách checkout đơn hàng
Hệ thống giữ ghế
Ví tiền trừ số dư
AI grading job chuyển trạng thái
Booking bị hủy và hoàn tiền

Nếu mọi thứ đều biến thành aggregate phức tạp, code sẽ nặng.

Nếu mọi thứ đều CRUD, luật nghiệp vụ sẽ bị rải rác.

Biết phân biệt hai thứ này là rất quan trọng.

---

14.21. Aggregate và concurrency

Aggregate liên quan trực tiếp đến concurrency.

Nếu hai request cùng sửa một aggregate, ta phải cẩn thận.

Ví dụ:

Khách có 1 vé cuối cùng trong kho.

Hai người cùng bấm mua.

Nếu hệ thống không kiểm soát, cả hai có thể mua thành công.

Đây là lỗi concurrency.

Cách xử lý có thể là:

  • Database constraint.
  • Row lock.
  • Optimistic locking.
  • Atomic update.
  • Unique constraint.
  • Idempotency key.
  • Queue theo key.

Aggregate giúp ta xác định:

> Đơn vị nào cần được bảo vệ khỏi cập nhật đồng thời sai?

Ví dụ:

  • Ghế A1 trong event X.
  • Wallet của user Y.
  • Order số Z.
  • Coupon code còn 1 lượt dùng.
  • GradingJob đang chạy.

Không phải toàn hệ thống cần lock.

Chỉ cần lock đúng aggregate hoặc đúng tài nguyên đang tranh chấp.

---

14.22. Optimistic locking dễ hiểu

Optimistic locking nghĩa là:

> Ta cho phép nhiều request đọc dữ liệu, nhưng khi ghi thì kiểm tra xem dữ liệu có bị ai sửa trước đó chưa.

Ví dụ Order có version:

Order
- id: 123
- status: DRAFT
- version: 5

Request A đọc version 5.

Request B cũng đọc version 5.

A cập nhật thành công:

update orders
set status = 'CONFIRMED', version = 6
where id = 123 and version = 5

B cũng muốn cập nhật với version 5, nhưng lúc này database đã là version 6.

B thất bại.

Hệ thống biết có conflict và có thể retry hoặc báo lỗi.

Optimistic locking hợp khi:

  • Conflict không quá thường xuyên.
  • Muốn tránh lock lâu.
  • Có thể retry khi xung đột.

Ví dụ:

  • Sửa order.
  • Cập nhật profile.
  • Chuyển trạng thái job.

---

14.23. Pessimistic locking dễ hiểu

Pessimistic locking nghĩa là:

> Ta khóa dữ liệu trước khi sửa để người khác không sửa cùng lúc.

Ví dụ:

select * from wallets where id = 1 for update

Trong lúc transaction đang giữ lock, transaction khác phải chờ.

Cách này hợp khi:

  • Dữ liệu cực kỳ quan trọng.
  • Conflict thường xuyên.
  • Không muốn retry phức tạp.
  • Thao tác ngắn và rõ.

Ví dụ:

  • Trừ số dư ví.
  • Giữ ghế cuối cùng.
  • Cập nhật tồn kho cực nhạy.

Nhưng nếu lock quá lâu, hệ thống nghẽn.

Vì vậy không nên gọi API bên ngoài trong lúc giữ lock.

Không nên gửi email trong transaction đang lock.

Không nên chạy logic nặng khi đang khóa dữ liệu.

Lock phải ngắn, rõ, và đúng chỗ.

---

14.24. Aggregate và idempotency

Idempotency nghĩa là:

> Gọi lại cùng một yêu cầu nhiều lần vẫn chỉ tạo ra một kết quả như một lần.

Trong hệ thống thực tế, request có thể bị gửi lại:

  • User bấm nút hai lần.
  • Mobile mất mạng rồi retry.
  • Worker retry job.
  • Payment gateway gửi webhook nhiều lần.
  • Message queue deliver lại message.

Aggregate nên xử lý idempotency ở những hành vi quan trọng.

Ví dụ Payment:

payment.mark_succeeded(gateway_transaction_id)

Nếu cùng gateway_transaction_id đã xử lý rồi, không được cộng tiền hoặc xác nhận hai lần.

Ví dụ GradingJob:

grading_job.complete(result_id, score, feedback)

Nếu worker retry cùng một job, không được tạo hai kết quả mâu thuẫn.

Ví dụ Wallet:

wallet.deposit(amount, transaction_id)

Nếu transaction_id đã tồn tại, không cộng tiền lần nữa.

Idempotency thường là một phần của invariant.

Nó bảo vệ aggregate khỏi lỗi rất thật trong hệ thống phân tán.

---

14.25. Aggregate và event

Khi aggregate thay đổi thành công, nó có thể sinh ra domain event.

Ví dụ:

Order.confirm()
-> OrderConfirmed

Payment.mark_succeeded()
-> PaymentSucceeded

GradingJob.complete()
-> GradingCompleted

Event không nên phát ra khi thay đổi chưa được lưu thành công.

Một cách thực dụng:

begin transaction
  aggregate thay đổi
  save aggregate
  save event vào outbox
commit

worker đọc outbox và publish event

Vì sao cần cẩn thận?

Nếu lưu Order thành công nhưng publish event thất bại, downstream không biết có đơn mới.

Nếu publish event thành công nhưng lưu Order rollback, downstream tưởng có đơn không tồn tại.

Đây là lý do pattern outbox xuất hiện.

Ta sẽ bàn kỹ hơn ở các chương sau, nhưng ngay tại đây chỉ cần nhớ:

> Aggregate đổi trạng thái là nguồn tự nhiên để sinh domain event.

---

14.26. Aggregate và service layer

Aggregate không nên tự làm mọi thứ.

Ví dụ Order có thể biết cách:

  • Thêm item.
  • Xóa item.
  • Confirm.
  • Cancel.

Nhưng Order không nên tự gọi:

  • Payment gateway.
  • Email service.
  • Shipping provider.
  • AI API.

Những việc đó thuộc về service layer, application service, workflow hoặc worker.

Ví dụ:

CheckoutService
1. Load Cart
2. Tạo Order
3. Lưu Order
4. Phát OrderPlaced

Order giữ luật nội bộ.

CheckoutService điều phối luồng.

Đừng biến aggregate thành object biết cả thế giới.

Aggregate nên mạnh về luật của chính nó, không mạnh về gọi hạ tầng bên ngoài.

---

14.27. Aggregate và repository

Repository là nơi load và save aggregate.

Ví dụ:

order = order_repository.get(order_id)
order.cancel(reason)
order_repository.save(order)

Ý tưởng quan trọng:

> Repository nên làm việc với aggregate root, không để bên ngoài lắp ráp từng mảnh tùy tiện.

Nếu Order gồm OrderItem, repository có trách nhiệm load đủ dữ liệu cần thiết để Order bảo vệ luật.

Không nhất thiết lúc nào cũng load mọi thứ.

Nhưng khi thực hiện hành vi cần kiểm tra item, phải có dữ liệu đủ.

Đọc dữ liệu để hiển thị có thể dùng query/read model riêng.

Không phải mọi màn hình đều phải load aggregate đầy đủ.

Đây là điểm thực tế:

> Write model và read model có thể khác nhau.

Aggregate chủ yếu phục vụ ghi đúng.

Read model phục vụ đọc nhanh, tiện, đúng nhu cầu màn hình.

---

14.28. Đừng dùng aggregate cho mọi truy vấn đọc

Một hiểu nhầm thường gặp:

> Muốn hiển thị danh sách đơn hàng thì phải load từng Order Aggregate.

Không cần.

Aggregate dùng để bảo vệ hành vi ghi.

Ví dụ khi cancel order, ta load Order aggregate để kiểm tra luật.

Nhưng khi hiển thị danh sách đơn trong admin, ta có thể dùng query:

OrderListItem
- order_id
- customer_name
- total_amount
- payment_status
- delivery_status
- created_at

Đây là read model.

Nó không cần có toàn bộ hành vi của Order.

Nếu cứ dùng aggregate cho mọi màn hình đọc, hệ thống sẽ nặng và code khó tối ưu.

Nguyên tắc:

  • Ghi: đi qua aggregate để đúng luật.
  • Đọc: dùng mô hình phù hợp để truy vấn hiệu quả.

---

14.29. Aggregate trong monolith và microservices

Trong monolith, aggregate thường nằm trong module/domain layer.

Ví dụ:

ordering/
  domain/
    order.py
    order_item.py
  application/
    checkout_service.py
  infrastructure/
    order_repository.py

Trong microservices, aggregate thường nằm bên trong service sở hữu context đó.

Ví dụ:

ordering-service
  Order Aggregate

payment-service
  Payment Aggregate

delivery-service
  DeliveryTask Aggregate

Điểm quan trọng:

> Không nên để service khác sửa aggregate của service này trực tiếp.

Nếu Payment muốn báo đơn đã thanh toán, Payment phát event PaymentSucceeded.

Ordering nghe event và quyết định cập nhật Order ra sao.

Hoặc Ordering gọi Payment để yêu cầu thanh toán, nhưng không tự sửa database Payment.

Aggregate và service boundary thường liên quan chặt, nhưng không đồng nghĩa.

Một service có thể chứa nhiều aggregate.

Một bounded context có thể có nhiều aggregate.

---

14.30. Một bounded context có nhiều aggregate

Ví dụ trong Ordering Context, có thể có:

Cart Aggregate
Order Aggregate
CouponRedemption Aggregate

Trong Payment Context:

Payment Aggregate
Refund Aggregate
Payout Aggregate

Trong Learning Context:

Enrollment Aggregate
LessonProgress Aggregate
QuizAttempt Aggregate

Trong AI Grading Context:

GradingJob Aggregate
Rubric Aggregate
EvaluationResult Aggregate

Đừng nghĩ:

Một context = một aggregate

Không đúng.

Context là khu vực nghĩa.

Aggregate là khối nhất quán bên trong khu vực đó.

---

14.31. Cách tìm aggregate trong dự án thật

Ta có thể tìm aggregate bằng các câu hỏi sau.

Câu hỏi 1: Đâu là hành vi nghiệp vụ quan trọng?

Ví dụ:

  • Checkout.
  • Cancel order.
  • Refund payment.
  • Reserve seat.
  • Submit assignment.
  • Complete grading.
  • Withdraw money.

Nếu chỉ là sửa mô tả hoặc đổi ảnh, có thể không cần aggregate phức tạp.

Câu hỏi 2: Dữ liệu nào phải đúng cùng nhau?

Ví dụ:

  • Order và OrderItem.
  • Wallet và ledger entry.
  • Booking và seat hold.
  • QuizAttempt và answers.

Câu hỏi 3: Invariant là gì?

Ví dụ:

  • Không bán quá số lượng tồn.
  • Không trừ tiền quá số dư.
  • Không hoàn tiền hai lần.
  • Không chấm một job đã hủy.

Câu hỏi 4: Ai là root?

Root là object đại diện cho khối đó.

Ví dụ:

  • Order là root của OrderItem.
  • Wallet là root của ledger entry nếu ledger chỉ phục vụ wallet đó.
  • QuizAttempt là root của Answer.
  • GradingJob là root của kết quả chấm trong một lần chấm.

Câu hỏi 5: Có đang quá to không?

Nếu root kéo theo quá nhiều thứ có vòng đời riêng, tách.

Câu hỏi 6: Có đang quá nhỏ không?

Nếu mỗi hành vi cần phối hợp quá nhiều aggregate chỉ để giữ một luật đơn giản, có thể đang tách quá nhỏ.

---

14.32. Ví dụ thiết kế Order Aggregate

Một Order aggregate thực dụng có thể gồm:

Order
- id
- customer_id
- items
- total_amount
- status
- created_at
- confirmed_at
- cancelled_at
- cancel_reason

OrderItem:

OrderItem
- product_id
- product_name_snapshot
- unit_price
- quantity
- line_total

Hành vi:

add_item(product, quantity)
remove_item(item_id)
change_quantity(item_id, quantity)
confirm()
cancel(reason)
mark_paid(payment_id)
mark_delivery_started()
mark_delivered()

Luật:

  • Không confirm đơn rỗng.
  • Không thêm item sau khi đã confirm, nếu nghiệp vụ không cho phép.
  • Không cancel đơn đã delivered.
  • Quantity phải dương.
  • Total phải bằng tổng line total.

Cần chú ý:

mark_paid không có nghĩa Order xử lý payment.

Payment xử lý ở Payment Context.

Order chỉ ghi nhận sự kiện thanh toán thành công theo ngôn ngữ của Ordering.

---

14.33. Ví dụ thiết kế GradingJob Aggregate

Trong AI Judge:

GradingJob
- id
- submission_id
- rubric_id
- status
- attempt_count
- max_attempts
- model_name
- started_at
- completed_at
- failed_reason
- score
- feedback

Hành vi:

start()
record_failure(reason)
retry()
complete(score, feedback)
cancel(reason)

Luật:

  • Job pending mới được start.
  • Job running không được start lại song song.
  • Retry không vượt quá max_attempts.
  • Job cancelled không được complete.
  • Job succeeded phải có score và feedback.
  • Một submission có thể có bao nhiêu job tùy chính sách.

Chỗ cần chặt:

  • Chuyển trạng thái job.
  • Số lần retry.
  • Ghi kết quả cuối.
  • Idempotency khi worker retry.

Chỗ có thể lỏng:

  • Gửi notification.
  • Cập nhật dashboard.
  • Ghi analytics chi phí.
  • Index kết quả vào search.

Nếu Celery worker bị retry, Aggregate phải giúp không tạo kết quả sai.

Đây là chỗ Aggregate liên quan trực tiếp đến vận hành thật.

---

14.34. Aggregate và trạng thái

Nhiều aggregate có state machine nhỏ bên trong.

Ví dụ Order:

DRAFT
-> CONFIRMED
-> PAID
-> PREPARING
-> DELIVERING
-> DELIVERED

Một số trạng thái phụ:

CANCELLED
REFUND_PENDING
REFUNDED

Không phải trạng thái nào cũng được chuyển sang trạng thái nào.

Ví dụ:

  • DRAFT có thể cancel.
  • DELIVERED không thể quay về DRAFT.
  • CANCELLED không thể mark delivered.
  • PAID có thể refund tùy điều kiện.

Nếu code chỉ dùng setter:

order.status = new_status

ta mất kiểm soát.

Aggregate nên có method thể hiện chuyển trạng thái hợp lệ:

order.confirm()
order.mark_paid()
order.start_preparing()
order.mark_delivered()
order.cancel(reason)

State machine không cần lúc nào cũng dùng thư viện.

Nhưng tư duy chuyển trạng thái hợp lệ rất quan trọng.

---

14.35. Aggregate và validation

Validation có nhiều tầng.

Tầng input

Kiểm tra dữ liệu người dùng gửi lên:

  • Field bắt buộc.
  • Định dạng email.
  • Số lượng là số.
  • Chuỗi không quá dài.

Tầng application

Kiểm tra quyền và luồng:

  • User có được sửa đơn này không?
  • Assignment có thuộc lớp này không?
  • Request có idempotency key không?

Tầng aggregate

Kiểm tra luật nghiệp vụ cốt lõi:

  • Không cancel order đã delivered.
  • Không withdraw quá số dư.
  • Không complete grading job đã cancelled.
  • Không checkout cart rỗng.

Đừng nhầm validation input với invariant nghiệp vụ.

Input validation chỉ đảm bảo dữ liệu có hình dạng hợp lệ.

Aggregate đảm bảo hành vi hợp lệ theo nghiệp vụ.

---

14.36. Aggregate và database constraint

Aggregate không thay thế database constraint.

Hai thứ bổ sung cho nhau.

Aggregate bảo vệ luật ở tầng domain.

Database constraint bảo vệ dữ liệu ở tầng lưu trữ.

Ví dụ:

  • quantity > 0
  • email unique
  • order_id not null
  • unique (event_id, seat_id) cho vé đã bán
  • unique transaction_id để chống xử lý trùng

Nếu chỉ dựa vào aggregate mà không có constraint, bug hoặc code path khác có thể ghi sai.

Nếu chỉ dựa vào constraint mà không có aggregate, lỗi sẽ bị phát hiện quá muộn và thông điệp nghiệp vụ không rõ.

Thiết kế tốt thường dùng cả hai:

  • Aggregate để diễn đạt luật.
  • Database để làm lớp bảo vệ cuối.

---

14.37. Aggregate và performance

Aggregate tốt không chỉ đúng mà còn phải vận hành được.

Một vài nguyên tắc:

  • Đừng load quá nhiều dữ liệu không cần.
  • Đừng giữ transaction lâu.
  • Đừng gọi API bên ngoài khi đang lock.
  • Đừng để aggregate quá to gây conflict.
  • Đừng dùng aggregate đầy đủ cho màn hình chỉ đọc.
  • Dùng read model cho truy vấn danh sách, dashboard, search.

Ví dụ:

Admin xem 1.000 đơn gần nhất.

Không nên load 1.000 Order Aggregate đầy đủ với toàn bộ item, payment, delivery.

Nên dùng query/read model gọn.

Nhưng khi admin bấm hủy một đơn, lúc đó load đúng Order Aggregate để kiểm tra luật hủy.

Đọc và ghi có nhu cầu khác nhau.

Thiết kế tốt tôn trọng sự khác nhau đó.

---

14.38. Aggregate và distributed systems

Trong hệ thống phân tán, aggregate càng quan trọng vì không có một transaction khổng lồ bao toàn bộ service.

Ví dụ:

Ordering Service
Payment Service
Delivery Service
Notification Service

Không nên cố tạo một transaction xuyên qua tất cả service.

Thay vào đó:

  • Mỗi service giữ đúng aggregate của mình.
  • Mỗi aggregate đảm bảo invariant cục bộ.
  • Các service giao tiếp bằng event/command.
  • Luồng dài dùng saga/workflow.
  • Chấp nhận eventual consistency ở phần phù hợp.

Ví dụ:

Order.confirm()
-> OrderConfirmed
-> Payment.create()
-> PaymentSucceeded
-> Order.mark_paid()
-> KitchenTicket.create()

Mỗi bước có transaction riêng.

Nếu một bước fail, hệ thống có cơ chế retry hoặc compensation.

Đây thực tế hơn nhiều so với cố ép mọi service phải đúng cùng lúc bằng một transaction phân tán.

---

14.39. Saga và aggregate khác nhau thế nào?

Aggregate bảo vệ luật trong một khối dữ liệu.

Saga điều phối một luồng dài qua nhiều aggregate hoặc nhiều service.

Ví dụ checkout:

1. Tạo Order
2. Trừ tồn kho
3. Thanh toán
4. Tạo phiếu bếp
5. Gửi thông báo

Không nên nhét hết vào một aggregate.

Thay vào đó:

  • Order Aggregate giữ luật của order.
  • Inventory Aggregate giữ luật tồn kho.
  • Payment Aggregate giữ luật thanh toán.
  • KitchenTicket Aggregate giữ luật bếp.
  • Saga điều phối các bước.

Nếu thanh toán thất bại:

  • Saga yêu cầu release stock.
  • Order chuyển sang payment_failed hoặc cancelled.

Saga không thay thế aggregate.

Aggregate bảo vệ từng phần.

Saga nối các phần lại thành luồng nghiệp vụ lớn.

---

14.40. Những dấu hiệu aggregate đang sai

Dấu hiệu aggregate quá to

  • Mỗi lần sửa phải load rất nhiều dữ liệu.
  • Nhiều luồng khác nhau cùng sửa một root.
  • Conflict lock thường xuyên.
  • Model có quá nhiều field không liên quan trực tiếp.
  • Một class có quá nhiều method cho nhiều nghiệp vụ khác nhau.
  • Test một hành vi nhỏ phải setup rất nhiều thứ.

Dấu hiệu aggregate quá nhỏ

  • Một hành vi đơn giản phải sửa quá nhiều aggregate.
  • Luật nghiệp vụ bị rải rác.
  • Không biết transaction bao quanh đâu.
  • Dữ liệu dễ lệch trong cùng một nghiệp vụ nhỏ.
  • Có quá nhiều event nội bộ chỉ để giữ một luật cơ bản.

Dấu hiệu thiếu aggregate

  • Code sửa field trực tiếp khắp nơi.
  • Status bị set tùy tiện.
  • Cùng một luật được copy ở nhiều service.
  • Database có nhiều trạng thái không hợp lệ.
  • Bug chỉ xuất hiện khi request chạy đồng thời.
  • Worker retry gây trùng kết quả.

---

14.41. Một bảng chọn nhanh

| Tình huống | Nên nghĩ gì? | |---|---| | Dữ liệu phải đúng cùng nhau ngay | Cùng aggregate hoặc cùng transaction | | Dữ liệu có vòng đời riêng | Aggregate riêng | | Chỉ cần đọc để hiển thị | Read model/query riêng | | Cần xử lý qua nhiều service | Saga/workflow | | Có thể trễ vài giây | Eventual consistency | | Không được xử lý trùng | Idempotency | | Nhiều request cùng sửa | Lock/version/constraint | | Model phình quá to | Tách aggregate hoặc context | | Logic rải rác | Đưa invariant vào aggregate |

---

14.42. Cách thiết kế aggregate từng bước

Khi thiết kế một aggregate, có thể đi theo các bước sau.

Bước 1: Chọn hành vi quan trọng

Đừng bắt đầu từ bảng.

Bắt đầu từ hành vi:

  • Đặt hàng.
  • Hủy đơn.
  • Thanh toán.
  • Hoàn tiền.
  • Giữ ghế.
  • Nộp bài.
  • Chấm bài.

Bước 2: Liệt kê invariant

Hỏi:

  • Điều gì không được sai?
  • Trạng thái nào bị cấm?
  • Dữ liệu nào phải khớp?
  • Có xử lý trùng không?
  • Có tranh chấp đồng thời không?

Bước 3: Chọn root

Root là object mà bên ngoài nói chuyện.

Ví dụ:

  • Order.
  • Wallet.
  • Booking.
  • GradingJob.
  • QuizAttempt.

Bước 4: Quyết định dữ liệu bên trong

Chỉ đưa vào aggregate những thứ cần để bảo vệ invariant.

Đừng đưa mọi thứ "có liên quan".

Bước 5: Thiết kế method theo ngôn ngữ nghiệp vụ

Ưu tiên:

cancel(reason)
confirm()
mark_paid(payment_id)
reserve_seat(seat_id)
complete(score, feedback)

Hạn chế:

set_status(...)
update(...)
save(...)

Bước 6: Quyết định event sinh ra

Ví dụ:

  • OrderConfirmed.
  • PaymentSucceeded.
  • SeatReserved.
  • GradingCompleted.

Bước 7: Quyết định cách lưu và chống concurrency

Ví dụ:

  • Transaction.
  • Version.
  • Unique constraint.
  • Row lock.
  • Idempotency key.

---

14.43. Bài tập đọc hệ thống hiện tại

Lấy một hệ thống bạn đang làm.

Chọn một model lớn nhất.

Ví dụ:

  • Order.
  • User.
  • Submission.
  • Course.
  • Booking.
  • Payment.

Trả lời:

1. Model này có những hành vi nghiệp vụ nào? 2. Có invariant nào bắt buộc luôn đúng? 3. Ai đang được sửa model này? 4. Có field nào thuộc về context khác không? 5. Có field nào chỉ để hiển thị, không phải luật nghiệp vụ không? 6. Có thao tác nào cần transaction không? 7. Có request nào có thể chạy đồng thời gây lỗi không? 8. Có retry nào có thể tạo dữ liệu trùng không? 9. Model này đang quá to hay quá nhỏ? 10. Root thật sự nên là gì?

Nếu trả lời xong, bạn sẽ nhìn hệ thống rõ hơn rất nhiều.

---

14.44. Tổng kết ngắn bằng ví dụ

Giả sử có luồng:

Khách đặt bánh
-> Thanh toán
-> Bếp làm
-> Giao hàng
-> Gửi thông báo
-> Ghi báo cáo

Không nên nhét tất cả vào một aggregate.

Cách nhìn tốt hơn:

Order Aggregate:
  giữ đúng đơn và item

Payment Aggregate:
  giữ đúng giao dịch và trạng thái thanh toán

KitchenTicket Aggregate:
  giữ đúng việc bếp cần làm

DeliveryTask Aggregate:
  giữ đúng việc giao hàng

Notification Job:
  gửi thông báo, có thể retry

Analytics:
  cập nhật sau, có thể trễ

Chỗ cần chặt:

  • Order item và tổng tiền.
  • Payment không xử lý trùng.
  • Không bán quá tồn kho hoặc quá ghế.
  • Wallet không âm.

Chỗ nên lỏng:

  • Email.
  • Analytics.
  • Search index.
  • Dashboard.
  • Recommendation.

Đây là tinh thần của chương:

> Chặt ở nơi sai là nguy hiểm. Lỏng ở nơi chậm một chút vẫn ổn.

---

14.45. Kết luận của chương

Aggregate là công cụ giúp ta giữ đúng những phần quan trọng của domain.

Nó giúp ta quyết định:

  • Dữ liệu nào phải nhất quán cùng nhau.
  • Object nào là cửa chính để thay đổi.
  • Luật nghiệp vụ nên nằm ở đâu.
  • Transaction nên bao quanh phần nào.
  • Khi nào cần lock, version, constraint, idempotency.
  • Khi nào nên tách ra và chấp nhận eventual consistency.

Thông điệp quan trọng nhất:

> Aggregate không phải là gom tất cả dữ liệu liên quan vào một object lớn. Aggregate là ranh giới của những thứ phải đúng cùng nhau.

Khi thiết kế hệ thống, đừng hỏi trước:

> Có những bảng nào?

Hãy hỏi:

> Điều gì không được sai, và dữ liệu nào cần được bảo vệ cùng nhau để điều đó luôn đúng?

Trả lời được câu hỏi này, ta sẽ biết chỗ nào cần chặt, chỗ nào nên lỏng, và hệ thống sẽ vừa đúng hơn vừa dễ mở rộng hơn.