⚙️ Software✍️ Khoa📅 20/04/2026☕ 12 phút đọc

Domain-Driven Design: Core Concepts

Giới thiệu

Domain-Driven Design (DDD) không phải framework hay pattern. Nó là mindset: tư duy từ business problem trước, rồi reflect lại vào code structure.

Tại sao quan trọng? Vì khi hệ thống phát triển phức tạp, code càng dễ trở thành "big ball of mud" nếu không có mô hình miền (domain model) rõ ràng. DDD giúp bạn:

  • Giao tiếp với business bằng ngôn ngữ chung (Ubiquitous Language)
  • Tách biệt concern rõ ràng (không phải toán học trừu tượng, mà từ business)
  • Quyết định scope một cách có lý do

Problem: Tại sao code lại rối?

Hình dung bạn xây dựng một platform e-commerce. Ban đầu:

// "Tao code tổng quát thôi"
type Order struct {
    ID            string
    Items         []Item
    TotalPrice    float64
    Status        string  // "pending", "processing", "delivered"...
    Customer      Customer
    Address       Address
    PaymentMethod PaymentMethod
    Notes         string
}

type Customer struct {
    ID    string
    Name  string
    Email string
}

func ProcessOrder(order *Order) {
    // Kiểm tra inventory
    // Charge payment
    // Update database
    // Send email
    // Log analytics
    // Calculate commission
    // Sync to accounting system
    // Update recommendation engine
}

Vấn đề:

  1. Order là gì? Bộ data dump hay domain concept? Không rõ
  2. ProcessOrder làm gì? 10+ responsibilities khác nhau - ai test? Ai maintain?
  3. Business logic ở đâu? Nằm rải rác trong helper functions, utilities, transactions
  4. Khi business thay đổi rule (e.g., "chỉ charge payment sau 24h nếu là credit card") → cơm cháy vì logic phân tán

DDD giải quyết bằng cách: model hóa domain đúng, rồi code theo model đó.


Core Concept 1: Ubiquitous Language

Định nghĩa: Ngôn ngữ chung giữa engineers, product, business — cả 3 nói cùng 1 vocab không nhập nhằng.

Ví dụ sai:

  • Engineer: "Order có trạng thái is_processed = true"
  • Product: "Đơn hàng đã được confirm?"
  • Business: "Chúng ta cần track khi nào thanh toán được xác minh" → Ai biết là cùng 1 concept? 🤔

Ví dụ đúng:

  • Cả ba đều dùng: "Order ở trạng thái PaymentVerified"
  • Code: OrderStatus.PaymentVerified (constant)
  • Database: status column có giá trị payment_verified
  • Document: "PaymentVerified nghĩa là thanh toán đã pass 3D Secure"

Thực hành:

  1. Tổ chức meeting giữa engineer + product + business
  2. Liệt kê terms: "Order, Payment, Shipment, Refund, ..." và định nghĩa chính xác
  3. Viết vào docs hoặc Slack thread để reference
  4. Code tuân theo vocab này

Lợi ích:

  • Reduce miscommunication (vừa code vừa giải thích ý tưởng? Được)
  • Code review dễ: "Cái này không align với ubiquitous language"
  • Onboarding nhanh: engineer mới hiểu domain luôn

Core Concept 2: Bounded Context

Định nghĩa: Ranh giới rõ ràng của một domain model. Bên trong context, các rules áp dụng; bên ngoài, context khác có rules riêng.

Real-world analogy: Ngân hàng có 3 bộ phận — Retail, Corporate, Treasury. Từ "Customer" có ý nghĩa khác nhau trong mỗi context:

  • Retail: Customer là cá nhân có tài khoản tiết kiệm
  • Corporate: Customer là công ty có credit facility
  • Treasury: Customer là counterparty trong swap contract

Ví dụ trong e-commerce:

┌─────────────────────────────────────────┐
│         ORDERS Context                   │
│  - Order aggregate                       │
│  - Order state: Created → Confirmed...   │
│  - Business rule: "Order phải có >= 1    │
│    item"                                 │
└────────────┬──────────────────────────────┘
             │ triggers
             ↓
┌─────────────────────────────────────────┐
│      FULFILLMENT Context                 │
│  - Shipment aggregate                    │
│  - Shipment state: Pending → Shipped...  │
│  - Business rule: "Một Shipment không   │
│    có deadline" (Orders context có)      │
└─────────────────────────────────────────┘
             │ publishes event
             ↓
┌─────────────────────────────────────────┐
│      PAYMENT Context                     │
│  - Transaction aggregate                 │
│  - Rules: PCI compliance, idempotency    │
└─────────────────────────────────────────┘

Key insight: Order trong ORDERS Context ≠ Order trong FULFILLMENT Context (về semantics).

  • ORDERS Order: "Có gì hạn chế về thanh toán không?"
  • FULFILLMENT Order: "Tao pack gì?". Order ở đây là reference (order_id), không phải embedded model

Trong code Go:

// Package: order/order.go
package order

type Order struct {
    ID        string
    Items     []Item
    CreatedAt time.Time
    // ❌ WRONG: shipmentID := shipment.ID - coupling context
    // ✓ RIGHT: Chỉ store ID để reference
    ShipmentID string
}

type Item struct {
    ProductID string
    Quantity  int
}

// Package: fulfillment/shipment.go
package fulfillment

type Shipment struct {
    ID      string
    OrderID string // ← reference qua ID, không embed Order
    Items   []ShipmentItem
}

type ShipmentItem struct {
    ProductID string
    Quantity  int
}

Giao tiếp giữa contexts:

  • Event-driven: Order context publish OrderCreated event → Fulfillment context subscribe
  • API: Fulfillment gọi Order API để query order details khi cần
  • Database: Mỗi context có database riêng (microservices style) hoặc schema riêng (monolith style)

Common pitfall: Chia services theo technical layer (UserService, OrderService) thay vì business context → Kết quả: cyclic dependency, duplicate logic, confusion


Core Concept 3: Aggregate

Định nghĩa: Cluster of domain objects (entities + value objects) mà được treat như 1 unit từ business perspective. Aggregate có root entity (aggregate root).

Tại sao cần?

  1. Clear boundary: Khỏi phải suy nghĩ "ai modify cái này?"
  2. Consistency guarantee: Aggregate root chịu trách nhiệm consistency
  3. Transaction boundary: Transaction bao toàn aggregate, không cross aggregates
  4. Database query pattern: Luôn load full aggregate (hoặc không load)

Ví dụ: Order Aggregate

// ✓ GOOD: Aggregate được thiết kế rõ ràng
package order

type Order struct {  // Aggregate Root
    id        string
    items     []OrderItem       // Part của aggregate
    customer  Customer          // Part của aggregate
    status    OrderStatus
    createdAt time.Time
}

type OrderItem struct {
    productID string
    quantity  int
    price     Money  // Value Object
}

type Customer struct {
    id    string
    email string
    // Chỉ data cần thiết trong context này
}

type Money struct {
    amount   int    // cents
    currency string // "USD", "VND"
}

// ✓ Business rule được enforce bởi aggregate
func (o *Order) AddItem(productID string, qty int, price Money) error {
    if qty <= 0 {
        return errors.New("quantity must be > 0")
    }
    if o.status != OrderStatusCreated {
        return errors.New("cannot add item to confirmed order")
    }
    o.items = append(o.items, OrderItem{
        productID: productID,
        quantity:  qty,
        price:     price,
    })
    return nil
}

func (o *Order) Total() Money {
    total := 0
    for _, item := range o.items {
        total += item.price.amount * item.quantity
    }
    return Money{amount: total, currency: o.items[0].price.currency}
}

func (o *Order) Confirm() error {
    if len(o.items) == 0 {
        return errors.New("order must have at least 1 item")
    }
    o.status = OrderStatusConfirmed
    return nil
}

❌ Anti-pattern: Anemic domain model

// BAD: Aggregate là dumb data container
type Order struct {
    ID     string
    Items  []OrderItem
    Status string
}

// BAD: Business logic bên ngoài, được gọi từ service layer
type OrderService struct {
    db *Database
}

func (s *OrderService) AddItemToOrder(orderID, productID string, qty int) error {
    order, _ := s.db.GetOrder(orderID)
    
    if qty <= 0 {  // ← Rule nằm ở service, không phải aggregate
        return errors.New("qty invalid")
    }
    
    if order.Status != "created" {  // ← Lại check status ở đây
        return errors.New("cannot add")
    }
    
    order.Items = append(order.Items, OrderItem{...})
    return s.db.SaveOrder(order)
}

// Khi business rule thay đổi:
// 1. Modify OrderService
// 2. Nhưng OrderService được gọi từ HTTP handler, GRPC handler, scheduled job...
// → Rule bị duplicate ở 3 chỗ!

Aggregate design heuristic:

  • Root: Là entity "chính", business care nhất (Order, ShoppingCart, User)
  • Internal entities: Entity khác nằm trong aggregate (OrderItem)
  • Value objects: Immutable data (Money, Email, Address)
  • Size: Aggregates nên nhỏ. Nếu có 100 objects → bạn model sai

Core Concept 4: Entity vs Value Object

Entity Value Object
Identity Có unique ID, define by identity Không ID, define by attributes
Mutability Mutable Immutable
Lifecycle Có lifecycle (create → update → delete) Không lifecycle
Equality a == b nếu same ID a == b nếu same value
Example Order (ID: order123), Customer Money (100 USD), Email (john@example.com)

Ví dụ:

// ENTITY: Order
type Order struct {
    ID    string  // ← Identity
    Items []OrderItem
    Status string
}

order1 := Order{ID: "order123", Items: [...], Status: "pending"}
order2 := Order{ID: "order123", Items: [...], Status: "confirmed"}
// order1 == order2? NO (different status) → nhưng là cùng 1 Order
// Nên: order1.ID == order2.ID → là cùng domain object

// VALUE OBJECT: Money (Immutable)
type Money struct {
    Amount   int
    Currency string
}

money1 := Money{100, "USD"}
money2 := Money{100, "USD"}
// money1 == money2? YES (same value)

// VALUE OBJECT: Email
type Email struct {
    value string
}

func NewEmail(s string) (Email, error) {
    if !strings.Contains(s, "@") {
        return Email{}, errors.New("invalid email")
    }
    return Email{value: s}, nil
}

email1, _ := NewEmail("john@example.com")
email2, _ := NewEmail("john@example.com")
// email1 == email2? YES (same value)

// ✓ When to use value objects:
// - Trong order aggregate, address là value object (không care unique identity, chỉ care attributes)
type OrderAddress struct {
    Street  string
    City    string
    ZipCode string
}

// ✓ Immutability in Go
type Money struct {
    amount   int    // private
    currency string // private
}

func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, errors.New("currency mismatch")
    }
    return Money{
        amount:   m.amount + other.amount,
        currency: m.currency,
    }, nil
}
// m.Add(...) returns NEW Money, không mutate m

// Prevent copy-and-modify:
func (m *Money) Multiply(factor int) Money {  // ← pointer, không value
    // Prevents: money.Multiply(...) from returning struct by value
}

Lợi ích immutable value objects:

  • Thread-safe (no lock needed)
  • Easier to reason about
  • Can be used as map keys

Core Concept 5: Repository Pattern

Định nghĩa: Abstraction giữa domain model và persistence layer. Repository hide cách data được stored (DB, cache, file, ...).

// Domain layer: repository.go
package order

type Order struct {
    ID    string
    Items []OrderItem
}

type Repository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, id string) (*Order, error)
    FindAll(ctx context.Context) ([]*Order, error)
}

// Infrastructure layer: postgres_repository.go
package postgres

type OrderRepository struct {
    db *pgx.Conn
}

func (r *OrderRepository) Save(ctx context.Context, order *Order) error {
    // Translate Order → database rows
    // Handle transactions
    // Return error if PK conflict, etc.
}

func (r *OrderRepository) FindByID(ctx context.Context, id string) (*Order, error) {
    // Query database
    // Translate rows → Order
}

// Application layer: service.go
package app

type OrderService struct {
    repo order.Repository
}

func (s *OrderService) ConfirmOrder(ctx context.Context, orderID string) error {
    order, err := s.repo.FindByID(ctx, orderID)
    if err != nil {
        return err
    }
    
    if err := order.Confirm(); err != nil {
        return err
    }
    
    return s.repo.Save(ctx, order)
}

// ✓ Benefit: Easy to test
type MockOrderRepository struct {
    orders map[string]*Order
}

func (m *MockOrderRepository) Save(ctx context.Context, order *Order) error {
    m.orders[order.ID] = order
    return nil
}

func TestConfirmOrder(t *testing.T) {
    repo := &MockOrderRepository{orders: make(map[string]*Order)}
    service := &OrderService{repo: repo}
    
    // No database needed!
    err := service.ConfirmOrder(context.Background(), "order123")
}

Advanced: Query Repository vs Write Repository

// Write model: For commands
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    Delete(ctx context.Context, id string) error
}

// Read model: For queries (optimized for reads)
type OrderReadRepository interface {
    FindByID(ctx context.Context, id string) (*OrderDTO, error)
    FindByCustomer(ctx context.Context, customerID string) ([]*OrderDTO, error)
    FindRecentOrders(ctx context.Context, limit int) ([]*OrderDTO, error)
}

// ✓ Benefit: Write model stays simple (full Order aggregate), read model optimized

Core Concept 6: Domain Events

Định nghĩa: Event that occurred trong domain. Khi order được confirmed, publish OrderConfirmed event.

// Domain event
package order

type OrderConfirmed struct {
    OrderID      string
    CustomerID   string
    TotalAmount  Money
    OccurredAt   time.Time
}

// Domain model generates event
type Order struct {
    ID         string
    Items      []OrderItem
    status     OrderStatus
    events     []interface{}  // ← Buffer events
}

func (o *Order) Confirm() error {
    if len(o.items) == 0 {
        return errors.New("empty order")
    }
    o.status = OrderStatusConfirmed
    
    // ✓ Publish event (domain doesn't care who listens)
    o.events = append(o.events, OrderConfirmed{
        OrderID:     o.ID,
        CustomerID:  o.customerID,
        TotalAmount: o.Total(),
        OccurredAt:  time.Now(),
    })
    return nil
}

func (o *Order) PendingEvents() []interface{} {
    return o.events
}

func (o *Order) ClearEvents() {
    o.events = nil
}

// Application layer: handle events
package app

func (s *OrderService) ConfirmOrder(ctx context.Context, orderID string) error {
    order, _ := s.repo.FindByID(ctx, orderID)
    
    if err := order.Confirm(); err != nil {
        return err
    }
    
    if err := s.repo.Save(ctx, order); err != nil {
        return err
    }
    
    // ✓ Publish events
    for _, event := range order.PendingEvents() {
        s.eventBus.Publish(ctx, event)
    }
    order.ClearEvents()
    
    return nil
}

// Other contexts subscribe
package fulfillment

type OrderConfirmedHandler struct {
    shipmentService *ShipmentService
}

func (h *OrderConfirmedHandler) Handle(event order.OrderConfirmed) {
    // Create shipment
    h.shipmentService.CreateShipment(event.OrderID, event.CustomerID)
}

Benefits:

  • Loose coupling: Order context không depend on Fulfillment
  • Event sourcing ready: Can rebuild state từ events
  • Async processing: Handle events trong background job

Pitfall: Publishing events nhưng transaction fail

  • Solution: Transactional outbox pattern (advanced topic)

Tóm tắt

Concept Tác dụng Gotcha
Ubiquitous Language Ngôn ngữ chung, reduce miscommunication Cần setup từ đầu project
Bounded Context Clear scope, independent evolution Overuse → microservices chaos
Aggregate Business logic centralized, consistency Aggregate size explosion
Entity vs VO Model richness, testability Confusion khi nào dùng gì
Repository Abstraction, testability Can become "God" object
Domain Events Loose coupling, event sourcing Handling failure mode

Khi nào dùng DDD?

Good fit:

  • Dự án 6+ tháng, growing codebase
  • Domain complexity cao (multiple business rules, edge cases)
  • Team > 3 people
  • Long-term maintenance priority

Overkill:

  • CRUD app (blog, simple data form)
  • Prototype, 1-person project
  • Clear, linear business flow (no edge case)

Real-world red flag: "We did DDD nhưng code vẫn là big ball of mud" → Thường do:

  1. Chỉ follow syntax (create classes), bỏ qua spirit (model business)
  2. Aggregates quá lớn (vì "dễ code")
  3. Domain knowledge không được share (engineer không hiểu business)

Bước tiếp theo

  • Distributed Systems: Khi DDD meets microservices — context communication, saga pattern
  • System Design: Aggregate consistency vs eventual consistency trade-off
  • Event Sourcing: Store change history thay vì state