Domain-Driven Design chia làm hai phần: strategic (bounded context, ubiquitous language — chuyện tổ chức) và tactical (các khối xây dựng mã: value object, entity, aggregate). Phần tactical thường được minh họa bằng Java/C# hướng đối tượng, nhưng Go — với value semantics của struct và không có class kế thừa — thể hiện chúng theo cách riêng, đôi khi gọn hơn. Bài này dựng ba khối cốt lõi chạy được, đo thật hành vi của chúng, và chỉ ra chỗ Go làm khác ngôn ngữ OOP truyền thống.
Value Object: bất biến, so theo giá trị
Value object là thứ không có định danh — nó chính là giá trị của nó. Tiền 100 VND bằng mọi 100 VND khác, bất kể tạo lúc nào. Nó phải bất biến: mọi phép toán trả về bản mới thay vì sửa tại chỗ. Trong Go, struct với các trường comparable là ứng viên hoàn hảo:
type Tien struct{ xu int; tienTe string } // lưu xu tránh sai số float
func (t Tien) Cong(o Tien) (Tien, error) {
if t.tienTe != o.tienTe { return Tien{}, errors.New("khác loại tiền tệ") }
return Tien{xu: t.xu + o.xu, tienTe: t.tienTe}, nil // trả BẢN MỚI
}
Điểm Go làm khác OOP truyền thống: không cần viết equals()/hashCode(). Go so sánh struct bằng == theo giá trị sẵn, và một struct comparable dùng làm map key được ngay. Đây là value semantics — thứ Java phải mô phỏng bằng tay thì Go cho miễn phí.

Hình 1: Value object Tien (bất biến, Cong trả bản mới); Entity DongHang (định danh qua ID, chứa value object); Aggregate DonHang (trường dong riêng, root ThemDong ép bất biến). Go dùng struct value semantics thay cho equals()/hashCode().
Entity: định danh qua ID
Entity là thứ có định danh — hai entity cùng ID là cùng một thực thể dù thuộc tính khác nhau (một khách hàng đổi tên vẫn là khách hàng đó). Entity so sánh theo ID, không theo giá trị:
type DongHang struct {
ID int // định danh
SanPham string
SoLuong int
DonGia Tien // entity CHỨA value object
}
Đo thật (Go 1.23) làm rõ khác biệt then chốt giữa hai khối:
[Entity] cùng ID 5? true -> CÙNG thực thể (dù thuộc tính khác)
[Value] 10 == 20? false -> KHÁC value (so theo giá trị)
[Value] 10 == 10? true -> CÙNG value (bất kể tạo riêng)
Hai DongHang cùng ID=5 là cùng thực thể dù SanPham và SoLuong khác nhau — bạn so e1.ID == e2.ID. Ngược lại, hai Tien bằng nhau khi và chỉ khi giá trị bằng nhau. Đây là ranh giới quan trọng nhất trong tactical DDD: hỏi "cái này có định danh không?" để quyết định entity hay value object.
Aggregate: root ép bất biến
Aggregate là một cụm entity/value object được xem như một đơn vị nhất quán, với một aggregate root là cửa duy nhất để sửa. Root chịu trách nhiệm ép bất biến (invariant) của cả cụm — luật luôn phải đúng:
type DonHang struct {
ID int
dong []DongHang // TRƯỜNG RIÊNG (chữ thường) — chỉ sửa qua root
tran Tien // bất biến: tổng <= trần
}
func (d *DonHang) ThemDong(dh DongHang) error {
// ... tính tổng sau khi thêm ...
if sau.xu > d.tran.xu { return ErrVuotHanMuc } // ROOT ép luật
d.dong = append(d.dong, dh)
return nil
}

Hình 2: Value object so theo giá trị (true), tự bảo vệ (từ chối cộng khác tiền tệ). Entity so theo ID; value object so theo giá trị. Aggregate root từ chối dòng 800 (300+800=1100 > trần 1000), tổng giữ ở 300. Value object comparable làm map key: 2 khóa (10VND gộp).
Đo thật: thêm dòng 300 OK (tổng 300), thêm dòng 800 bị từ chối (đơn vượt hạn mức, vì 300+800=1100 > trần 1000), tổng giữ vững ở 300. Vì trường dong là chữ thường (private trong package), không code ngoài nào sửa được slice trực tiếp — mọi thay đổi phải qua ThemDong, nơi bất biến được kiểm. Đây là cách Go đóng gói aggregate: dùng khả năng hiển thị cấp package thay cho private của class. Bonus đo thật: map[Tien]int có 2 khóa khác nhau khi thêm 10VND (hai lần, gộp), 10VND lại và 10USD — comparable struct làm map key miễn phí.
Đánh đổi cần cân nhắc
Đừng nhét mọi thứ vào một aggregate khổng lồ. Cám dỗ phổ biến là làm một aggregate KhachHang chứa mọi đơn hàng, địa chỉ, lịch sử. Aggregate lớn gây khóa tranh chấp (mọi thay đổi khóa cả cụm) và tải bộ nhớ nặng. Nguyên tắc: aggregate nên nhỏ nhất có thể mà vẫn giữ được bất biến. Thứ không cần nhất quán tức thì nên là aggregate riêng, tham chiếu nhau bằng ID chứ không nhúng.
Value semantics của Go giúp value object nhưng cẩn thận con trỏ và slice. Struct chỉ comparable (và làm map key được) khi mọi trường comparable — thêm một trường []byte hay map là mất tính đó, == không biên dịch. Với value object có trường không comparable, phải tự viết phương thức BangNhau. Và nhớ: value object nên bất biến, nên tránh trường con trỏ/slice mà người ngoài có thể sửa qua tham chiếu chung.
DDD tactical là công cụ, không phải luật. Không phải mọi struct đều cần là entity hay value object có phương thức. Với CRUD đơn giản, một struct dữ liệu phẳng đọc rõ hơn nhiều lớp trừu tượng. DDD trả cổ tức khi domain phức tạp về nghiệp vụ — nhiều bất biến, nhiều luật, nhiều thuật ngữ chuyên ngành. Áp nó cho domain đơn giản chỉ thêm nghi thức.
Ba ý mang về
- Value object bất biến và so theo giá trị: trong Go, struct với các trường comparable so bằng
==theo giá trị sẵn (đo thật10 == 10là true dù tạo riêng) và làm map key được — không cầnequals()/hashCode()như OOP truyền thống; mọi phép toán trả bản mới. - Entity có định danh, so theo ID: hai entity cùng ID là cùng thực thể dù thuộc tính khác (đo thật
e1.ID == e2.IDtrue) — câu hỏi "có định danh không?" quyết định entity hay value object. - Aggregate root là cửa duy nhất ép bất biến: đo thật, root từ chối thao tác phá bất biến (dòng 800 vượt trần 1000), và trường private cấp package chặn sửa trực tiếp từ ngoài — nhưng giữ aggregate nhỏ nhất có thể, và DDD chỉ đáng khi domain thật sự phức tạp.
Phần sau ta quay về kỹ thuật web thực dụng với một mẫu Go dùng khắp nơi: Phần sau mổ xẻ middleware chain có thể kết hợp — cách xâu chuỗi các lớp xử lý HTTP bằng hàm bậc cao, và làm nó gọn mà vẫn linh hoạt.