Đâ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ó)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.

DDD Domain-Driven Design ① Vấn đề DDD giải quyết • Business logic rải rác nhiều nơi • Anemic Domain Model — chỉ get/set • Dev & Business lệch ngôn ngữ ② Tư tưởng cốt lõi • Domain là trung tâm thiết kế • Ubiquitous Language — ngôn ngữ chung • Bounded Context — chia để trị ③ Strategic — bức tranh lớn • Ubiquitous Language • Bounded Context · Context Map • Subdomain: Core / Supporting / Generic ④ Tactical — chi tiết trong code • Entity · Value Object · Aggregate • Repository · Domain Service • Domain Event · Factory

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.

❌ Anemic Model Order (Entity) id, amount, status chỉ get/set — không có logic OrderService if (order.getStatus()==X) order.setStatus(Y) → logic nằm NGOÀI entity ✅ Rich Domain Model Order (Aggregate Root) - id, amount, status (private) + ship() + cancel() → tự bảo vệ invariant của chính nó Application Service order.ship()

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:

1
2
3
4
5
6
7
// Anemic — logic nằm ngoài, entity chỉ là data
if order.getStatus() == "paid" {
    order.setStatus("shipped")
}

// Rich Domain Model — logic nằm trong entity
order.Ship()   // tự kiểm tra status hợp lệ, tự đổi state, tự phát event

3. Tư tưởng cốt lõi của DDD

4 ý chính, xếp theo mức độ nền tảng:

  1. 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.
  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 flag2 hay limitPct.
  3. 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.
  4. 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:

Factory tạo Aggregate hợp lệ NewXAggregate(...) tạo ra Aggregate (Entity + Value Object) vận hành qua method bảo vệ invariant chỉ 1 cửa vào: Root lưu / lấy lại Repository Save / GetByID che giấu SQL, DB... phát Event Domain Event XCreatedEvent state vừa đổi loose coupling Bounded Context khác (vd: Notification) subscribe qua message queue

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:

Đọ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: