Đây là Phần 0 của series DDD (Domain-Driven Design) — viết trước 2 bài DDD Là Gì? (Và Vì Sao Honeydue Cần Nó) và DDD Building Blocks, vốn đi thẳng vào code thật của Honeydue. Nếu bạn chưa biết DDD là gì, đọc bài này trước — nó trả lời một vài câu hỏi nền tảng nhất mà ai mới tiếp cận DDD cũng có thể sẽ thắc mắc.
📐 Xem Bản Vẽ DDD — không cần biết code cũng hiểu
1. DDD là gì? (Tư duy thiết kế, hay một tiêu chuẩn?)
DDD không phải một chuẩn (standard) — không có tổ chức nào ban hành “spec DDD” để bạn build xong rồi validate đúng/sai. Nó cũng không phải framework hay thư viện — không có gì để import hay go get.
DDD là một tư duy thiết kế phần mềm (a design approach), do Eric Evans đúc kết trong cuốn sách Domain-Driven Design: Tackling Complexity in the Heart of Software (2003) — dân lập trình hay gọi tắt là “Blue Book”. Nó là tập hợp nguyên tắc + pattern trả lời câu hỏi: tổ chức code thế nào khi nghiệp vụ (business) đủ phức tạp để tự nó trở thành thứ khó nhất trong dự án — khó hơn cả bài toán kỹ thuật?
So sánh cho dễ hình dung: DDD giống SOLID hay Clean Architecture — một tư duy bạn áp dụng vào cách chia module, đặt tên, tổ chức tầng — chứ không phải một công cụ bạn cài vào rồi có ngay.
Hình 1: Bản đồ tư duy tổng quan — 4 mảnh ghép sẽ được giải thích lần lượt trong bài này.
2. DDD ra đời để giải quyết vấn đề gì?
Trước DDD, cách làm phổ biến là thiết kế bắt đầu từ database (vẽ ERD trước, map field ra object) hoặc từ framework (theo khuôn MVC, business logic nhét vào đâu tiện thì nhét). Với hệ thống nhỏ, cách này ổn. Với hệ thống phức tạp, nó sinh ra một anti-pattern rất phổ biến gọi là Anemic Domain Model — object chỉ là cái túi đựng field (get/set), còn logic thật sự nằm hết ở các class Service bên ngoài, tự ý thao túng field của object khác. Hệ quả: mất luôn lợi ích của lập trình hướng đối tượng, nhưng vẫn phải gánh toàn bộ độ phức tạp mà lẽ ra OOP giúp giảm bớt.
DDD ra đời để đảo ngược việc đó: đưa logic về đúng chỗ của nó — bên trong chính domain object, dưới dạng behavior thật sự chứ không phải field trần trụi.
Hình 2: Anemic Model — Service ở ngoài tự ý đọc/ghi field. Rich Domain Model — bên ngoài chỉ được gọi method, entity tự bảo vệ chính nó.
Viết lại thành pseudocode cho dễ so sánh:
|
|
3. Tư tưởng cốt lõi của DDD
4 ý chính, xếp theo mức độ nền tảng:
- Domain là trung tâm — mọi quyết định thiết kế xoay quanh nghiệp vụ trước, công nghệ tính sau. Ngược hẳn tư duy “database-first” hay “framework-first” ở mục 2.
- Ubiquitous Language — dev và người hiểu nghiệp vụ dùng chung 1 từ vựng, từ đó xuất hiện y hệt trong code. Business nói “ngưỡng cảnh báo ngân sách” thì code phải có hẳn 1 khái niệm tên gần như vậy, không phải biến
flag2haylimitPct. - Chia để trị (Bounded Context) — domain lớn không model được trong 1 khối duy nhất, nên chia thành nhiều context nhỏ, mỗi context có model và ngôn ngữ riêng, chỉ giao tiếp qua ranh giới rõ ràng.
- Bảo vệ tính nhất quán nghiệp vụ (invariant) — mọi thay đổi state phải đi qua đúng “cửa” hợp lệ, để dữ liệu không bao giờ rơi vào trạng thái vi phạm business rule, dù chỉ trong khoảnh khắc.
4. Lợi ích khi áp dụng DDD
| Lợi ích | Vì sao |
|---|---|
| Business logic tập trung | Sửa 1 rule chỉ sửa đúng 1 chỗ, không lùng khắp handler/SQL |
| Test nhanh, không cần DB/HTTP | Domain logic không phụ thuộc infra → unit test chạy trong mili-giây |
| Dễ đổi công nghệ | Đổi database, đổi framework — domain layer không cần sửa |
| Giảm hiểu lầm dev ↔ business | Ubiquitous Language làm cầu nối chung |
| Scale được đội ngũ | Mỗi bounded context 1 team làm riêng, ít giẫm chân nhau |
Mặt trái cần biết: DDD tốn công hơn hẳn so với CRUD thuần — tạo Value Object, Aggregate, Repository interface cho 1 app “thêm/sửa/xoá” đơn giản là over-engineering. DDD đáng giá khi nghiệp vụ thật sự phức tạp: nhiều rule, nhiều ràng buộc, nhiều actor. Một todo-list app không cần DDD.
5. Các thành phần trong DDD và quan hệ giữa chúng
Nhìn lại nhánh ③④ ở Hình 1 — Strategic (bức tranh lớn) và Tactical (chi tiết trong code). Phần Tactical là nơi hầu hết người mới bị rối vì có nhiều khái niệm cùng lúc, nên tôi minh hoạ bằng vòng đời thay vì liệt kê khô khan:
Hình 3: Vòng đời 1 domain object — Factory tạo, Aggregate vận hành, Repository lưu trữ, Domain Event thông báo ra ngoài mà không cần gọi trực tiếp.
Vài điểm bổ sung, lấy từ loạt bài DDD của softwaredesign.vn (liệt kê đầy đủ ở mục Đọc thêm) — vì mỗi khối trong Hình 3 còn có những quyết định thiết kế nhỏ hơn đáng biết:
- Identity: nên sinh ID trước khi lưu, ngay trong Factory (không chờ database tự tăng) — nhờ vậy Domain Event có thể tham chiếu đến entity ngay lập tức, và ID có thể được model như 1 Value Object riêng (
UserId) thay vì string trần. - Factory: không phải object nào cũng cần Factory riêng — quyết định dựa trên độ phức tạp của việc khởi tạo là chính, tính đóng gói là phụ. Object đơn giản thì constructor bình thường là đủ.
- Aggregate: khi 2 entity có vòng đời khác nhau, nên tách thành 2 aggregate riêng, chỉ tham chiếu nhau qua ID — không giữ tham chiếu object trực tiếp. Đây là lý do vì sao 2 aggregate độc lập chỉ nên đồng bộ với nhau qua Domain Event (eventual consistency), thay vì gọi thẳng vào nhau.
- Domain Event: chỉ nên được tạo ra bên trong Aggregate Root, Factory, hoặc Domain Service — không bao giờ ở Application layer hay Value Object. Và một cạm bẫy thường gặp: khi load lại một aggregate đã tồn tại từ database, đừng dùng lại constructor tạo-mới — nó sẽ vô tình phát lại sự kiện “vừa được tạo” cho một thứ đã tồn tại từ lâu.
6. Một ứng dụng “đáp ứng được DDD” khi nào?
Không có bài kiểm tra pass/fail duy nhất — DDD là một dải phổ, áp dụng nhiều hay ít tuỳ độ phức tạp nghiệp vụ. Vài tín hiệu để tự kiểm tra:
- ✅ Đọc code domain layer, người không biết ngôn ngữ lập trình vẫn hiểu được nghiệp vụ đang làm gì.
- ✅ Domain layer test được mà không cần chạy database, không cần start web server.
- ✅ Đổi hẳn database hoặc đổi hẳn framework HTTP — business rule (domain layer) không cần sửa.
- ✅ Mọi thay đổi state đi qua đúng 1 “cửa” hợp lệ — không có chỗ nào gán thẳng field bỏ qua validate.
- ✅ Ranh giới bounded context rõ ràng — context này không tự ý đọc/ghi model nội bộ của context khác.
Thiếu 1-2 tín hiệu không có nghĩa là “sai” — chỉ là đang áp dụng ở mức vừa phải, hợp lý cho quy mô hiện tại. Không có app nào đạt DDD “100%” tuyệt đối; quan trọng là áp dụng đúng liều lượng cho đúng chỗ phức tạp.
Đi tiếp
Từ đây, 2 bài tiếp theo trong series áp dụng toàn bộ những khái niệm trên vào code Go thật của Honeydue:
- Phần 1 — DDD Là Gì? (Và Vì Sao Honeydue Cần Nó): layered architecture, bounded context map thật của Honeydue.
- Phần 2 — DDD Building Blocks: Value Object, Entity, Aggregate, Repository, Domain Service, Domain Event — từng dòng code thật, kèm sơ đồ giải phẫu aggregate và luồng request end-to-end.
Đọc thêm
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software (cuốn “Blue Book” gốc)
- Vaughn Vernon — Implementing Domain-Driven Design
- martinfowler.com/bliki/DomainDrivenDesign
Loạt bài DDD tiếng Việt rất chi tiết trên softwaredesign.vn — đáng đọc để đào sâu từng khối: