Xem lại phần 6: DDD [Part 6]: Những Domain Model Patterns giải quyết business logic phức tạp
Trong hành trình phát triển phần mềm, một trong những thách thức lớn nhất chính là hiểu đúng và đủ về những gì đã xảy ra trong hệ thống — không chỉ trạng thái hiện tại, mà còn cả quá trình dẫn đến trạng thái đó. Phần nội dung tiếp theo sẽ đưa ra một mẫu thiết kế cực kỳ thú vị giúp ta mô hình hóa “chiều thời gian” trong hệ thống: Event-Sourced Domain Model.
🧱 Các thành phần và thuật ngữ chính
Thuật ngữ | Giải thích |
---|---|
Aggregate | Một tập hợp các đối tượng được coi là một đơn vị thống nhất khi thay đổi dữ liệu. |
Domain Event | Sự kiện mô tả điều gì đó đã xảy ra trong domain. |
Event Store | Cơ sở dữ liệu dạng append-only lưu các domain event. |
Projection | Quá trình tạo ra một mô hình trạng thái từ chuỗi sự kiện. |
Snapshot | Một bản chụp trạng thái tại một thời điểm, giúp tăng hiệu năng khôi phục trạng thái. |
⏱️ Vấn đề: Dữ liệu chỉ cho biết hiện tại
Hãy tưởng tượng bạn có một bảng dữ liệu chứa thông tin khách hàng tiềm năng (lead
). Mỗi bản ghi chứa:
- Tên, số điện thoại
- Trạng thái hiện tại (NEW_LEAD, FOLLOWUP_SET, CONVERTED, v.v.)
- Ngày tạo và ngày cập nhật
Tuy nhiên, bạn không biết khách hàng đó đã trải qua bao nhiêu lần gọi, đã thay đổi thông tin bao nhiêu lần, và bao nhiêu follow-up đã được thiết lập trước khi chốt đơn.
➡️ Vấn đề nằm ở chỗ: bạn chỉ thấy trạng thái hiện tại, không thấy được hành trình.
🌀 Event Sourcing là gì?
Thay vì lưu trạng thái hiện tại, Event Sourcing lưu lại mọi sự kiện xảy ra với thực thể.
📌 Mỗi thay đổi (change) được mô tả bởi một sự kiện (event), ví dụ:
lead-initialized
,contacted
,order-submitted
,payment-confirmed
…
Ví dụ:
{ "event-type": "followup-set", "lead-id": 12, "timestamp": "2020-05-20T12:32:08.24Z" }
Một chuỗi các sự kiện như thế sẽ cho ta toàn bộ lịch sử của một khách hàng từ khi bắt đầu đến khi chốt đơn.
So sánh: State-based vs Event-sourced
Đặc điểm | State-based | Event-sourced |
---|---|---|
Dữ liệu lưu | Trạng thái cuối | Toàn bộ lịch sử sự kiện |
Khả năng “time travel” | ❌ Không | ✅ Có |
Truy vết nguyên nhân thay đổi | ❌ Khó | ✅ Rõ ràng |
Phức tạp triển khai | ✅ Đơn giản | ❌ Cao hơn |
🧱 Event-Sourced Domain Model là gì?
Mô hình này kết hợp event sourcing với các thành phần trong domain model như:
- Aggregate
- Domain Event
- Value Object

Tất cả các thay đổi của aggregate được biểu diễn và lưu trữ dưới dạng domain events. Điều này giúp:
- Khôi phục trạng thái tại mọi thời điểm (time traveling)
- Audit dễ dàng
- Phân tích hiệu quả quy trình
🧪 Ví dụ: Ticket Aggregate
✳️ Application Service
public void RequestEscalation(TicketId id, EscalationReason reason) { var events = _ticketsRepository.LoadEvents(id); // 1. Load events var ticket = new Ticket(events); // 2. Rehydrate var originalVersion = ticket.Version; var cmd = new RequestEscalation(reason); ticket.Execute(cmd); // 3. Execute _ticketsRepository.CommitChanges(ticket, originalVersion); // 4. Commit }
🧱 Cấu trúc bên trong Ticket
🧬 Rehydration bằng constructor:
public Ticket(IEnumerable<IDomainEvents> events) { _state = new TicketState(); // Khởi tạo state projector foreach (var e in events) { AppendEvent(e); // Tái dựng lại từng sự kiện } }
⚙️ AppendEvent()
– Gắn sự kiện và cập nhật state:
private void AppendEvent(IDomainEvent @event) { _domainEvents.Append(@event); // Ghi lại lịch sử sự kiện ((dynamic)_state).Apply((dynamic)@event); // Chiếu vào state }
🚀 Business Logic dưới dạng sự kiện
public void Execute(RequestEscalation cmd) { if (!_state.IsEscalated && _state.RemainingTimePercentage <= 0) { var escalatedEvent = new TicketEscalated(_id, cmd.Reason); AppendEvent(escalatedEvent); // Thay vì đổi trạng thái trực tiếp } }
🧠 Khác biệt chính:
- Không set trực tiếp flag
IsEscalated = true
. - Thay vào đó, tạo sự kiện
TicketEscalated
, và cập nhật trạng thái thông qua chiếu sự kiện.
🧩 State Projection Class: TicketState
public class TicketState { public bool IsEscalated { get; private set; } public void Apply(TicketInitialized @event) { Id = @event.Id; Version = 0; IsEscalated = false; } public void Apply(TicketEscalated @event) { IsEscalated = true; Version += 1; } }
🧭 Vì sao gọi là “Event-Sourced Domain Model”?
Không chỉ là Event Sourcing đơn thuần, mà là dùng Event Sourcing như một phần cấu trúc chính của Domain Model (Aggregate, Command, State, …).
📝 Việc ghi lại toàn bộ chuỗi sự kiện cho mỗi aggregate giúp tăng khả năng truy vết (traceability), kiểm tra (auditability) và mô phỏng hành vi (simulation) dễ dàng.
🔍 Một sự kiện – Nhiều góc nhìn
Từ cùng một chuỗi sự kiện, ta có thể tạo nhiều mô hình khác nhau:
Projection | Mục đích |
---|---|
LeadStateProjection | Dựng trạng thái hiện tại |
LeadSearchModelProjection | Tìm kiếm theo lịch sử số điện thoại, họ tên |
AnalysisModelProjection | Đếm số lần follow-up để phân tích hiệu quả |
📈 Lợi ích nổi bật
✅ Time traveling
Dễ dàng xem lại trạng thái trước đó, giúp debug, kiểm tra và audit.
✅ Deep insight
Tạo nhiều view từ event để phân tích hành vi người dùng, tối ưu luồng nghiệp vụ.
✅ Audit log mạnh mẽ
Phù hợp với các hệ thống yêu cầu lưu vết mọi hành động (ví dụ: tài chính, bảo hiểm).
✅ Concurrent-safe hơn
Có thể xác định chính xác event nào đã được ghi thêm, tránh ghi đè sai.
⚠️ Thách thức cần lưu ý
❌ Khó học hơn mô hình truyền thống
Đòi hỏi hiểu rõ domain event và tư duy theo hướng “diễn tiến theo thời gian”.
❌ Khó versioning event
Vì event là bất biến. Việc thay đổi schema cần kỹ thuật riêng (vd: Event Versioning).
❌ Tăng độ phức tạp kiến trúc
Phải kết hợp thêm snapshot, cache, projection, event store…
🧰 Khi nào nên dùng?
Hãy dùng Event-Sourced Domain Model khi:
- Cần audit hoặc trace đầy đủ hành vi
- Nghiệp vụ phức tạp, cần hiểu rõ từng bước
- Phân tích hành vi, tối ưu hệ thống là yêu cầu quan trọng
Đừng dùng nếu:
- Nghiệp vụ đơn giản
- Nhóm dev chưa quen với CQRS/Event Sourcing
- Không cần truy xuất lịch sử chi tiết
🧠 Các câu hỏi thường gặp & Giải pháp
1. Hiệu năng khi số lượng event lớn?
🧩 Vấn đề:
Việc khôi phục lại trạng thái của aggregate từ danh sách sự kiện sẽ tốn tài nguyên tính toán, nhất là khi danh sách này ngày càng dài theo thời gian.
🧪 Giải pháp:
- Thực hiện benchmark để đánh giá mức ảnh hưởng đến hiệu năng.
- Trong phần lớn hệ thống, hiệu năng chỉ bị ảnh hưởng đáng kể khi một aggregate có 10.000+ events.
- Trong thực tế, đa số aggregate chỉ có dưới 100 events trong vòng đời của nó.
⚡ Tối ưu hiệu năng với Snapshot Pattern:
📌 Các bước:
- Luôn theo dõi sự kiện mới và tạo projection tương ứng → lưu vào cache.
- Khi cần dùng:
- Lấy snapshot gần nhất từ cache.
- Áp dụng thêm các sự kiện mới sau snapshot.
- Khôi phục trạng thái nhanh chóng.

📛 Lưu ý:
Chỉ dùng snapshot nếu thực sự cần. Nếu không có nhiều hơn 10.000 events cho mỗi aggregate thì snapshot có thể là sự phức tạp không cần thiết.
2. Làm sao để scale?
👉 Sharding theo Aggregate ID
3. Xóa dữ liệu nhạy cảm (GDPR)?
🔐 Sử dụng mô hình Forgettable Payload Pattern:
- Dữ liệu nhạy cảm (như tên, email…) được mã hóa trong event.
- Khóa mã hóa được lưu trong key-value store riêng, khóa là aggregate ID.
💣 Khi cần xóa:
- Xóa khóa mã hóa khỏi key store.
- Dù event vẫn còn, dữ liệu nhạy cảm không thể giải mã được nữa.
🆚 Tại sao không dùng các cách thay thế khác?
- Log text: ❌ Không đảm bảo consistency
- Logs table trong DB: ❌ Dễ quên thêm log, schema dễ loạn
- Trigger tạo bảng lịch sử: ❌ Không có ngữ cảnh nghiệp vụ (“vì sao” thay đổi)
❓ Câu hỏi ôn tập nhanh
- Event và Value Object liên kết thế nào?
👉 Domain events sử dụng value objects để mô tả sự kiện. - Có thể tạo nhiều projection khác nhau từ cùng 1 chuỗi event không?
👉 ✅ Có. Dễ dàng bổ sung projection mới trong tương lai. - Cả event-sourced và state-based có sinh domain events?
👉 ✅ Có, nhưng chỉ event-sourced dùng event làm “nguồn dữ liệu chính”.
✍️ Tổng kết
Event-Sourced Domain Model giúp dữ liệu “kể lại câu chuyện của chính nó”. Đây không chỉ là một mẫu thiết kế kỹ thuật – nó là cách để hiểu sâu hơn về hành vi hệ thống, người dùng và những gì đang thực sự diễn ra.
Nếu anh em đang làm trong các lĩnh vực cần logic nghiệp vụ phức tạp, nhiều quy tắc thay đổi theo thời gian, hoặc cần theo dõi đầy đủ hành động của người dùng – đây chắc chắn là mẫu thiết kế đáng để học và thử nghiệm.
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.