Back to blog

Domain-Driven Design (DDD): Strategic & Tactical Pattern

software-architectureddddesign-patternsbackendsystem-design
Domain-Driven Design (DDD): Strategic & Tactical Pattern

Đa số project phần mềm thất bại không phải vì code tệ, mà vì sự đứt kết nối giữa code và business mà nó phục vụ. Developer build feature, nhưng model trong code không khớp với model trong đầu domain expert. Theo thời gian, sự đứt kết nối này lớn dần — code trở nên khó thay đổi, khó hiểu, và khó align với business need đang phát triển.

Domain-Driven Design (DDD) là tập hợp principle và pattern giải quyết vấn đề này trực diện. Được giới thiệu bởi Eric Evans trong cuốn sách năm 2003 Domain-Driven Design: Tackling Complexity in the Heart of Software, DDD cung cấp cả strategic pattern (cách tổ chức hệ thống lớn thành bounded context) và tactical pattern (cách model business logic bên trong mỗi context).

DDD không phải framework hay library — nó là cách tư duy về software design đặt domain model ở trung tâm.

Trong bài viết này, chúng ta sẽ đề cập:

✅ DDD là gì (và không phải là gì)
✅ Strategic DDD: bounded context, ubiquitous language, context mapping
✅ Tactical DDD: entity, value object, aggregate, repository, domain event
✅ Aggregate design rule và consistency boundary
✅ Domain service vs application service
✅ Anti-corruption layer và integration pattern
✅ Event storming workshop
✅ DDD và microservices: bounded context = service boundary
✅ DDD với hexagonal và clean architecture
✅ Ví dụ thực tế: model domain e-commerce
✅ Khi nào DDD là quá mức


Problem: Code Không Nói Ngôn Ngữ Business

Trong đa số project, cuộc trò chuyện diễn ra như thế này:

Domain expert: "Khi khách hàng đặt đơn, chúng ta cần kiểm tra inventory, reserve item, tính giá với promotion đang active, rồi confirm đơn."

Developer: "Hiểu rồi. Em sẽ tạo endpoint POST /api/orders insert một row vào bảng orders và update bảng inventory."

Developer hiểu HTTP request và database table, nhưng bỏ lỡ các domain concept: inventory reservation, promotion rule, order confirmation workflow. Code kết quả hoạt động, nhưng nó không model business. Sáu tháng sau, khi business thêm bundle pricing hay pre-order, code trở thành mớ bòng bong if-else.

DDD thu hẹp khoảng cách này bằng cách làm code phản ánh domain.


Strategic DDD: Bức Tranh Lớn

Strategic DDD là về chia hệ thống lớn thành các phần nhỏ, được định nghĩa rõ ràng nơi mỗi phần có model riêng và ngôn ngữ riêng.

Ubiquitous Language

Concept quan trọng nhất trong DDD là ubiquitous language — từ vựng chung giữa developer và domain expert được dùng nhất quán trong conversation, documentation, và code.

❌ Không có ubiquitous language:
- Domain expert nói: "approved application"
- Developer viết: `request.setStatus("A")`
- Database có: `app_status_cd = 2`
 
✅ Có ubiquitous language:
- Domain expert nói: "approved application"
- Developer viết: `application.approve()`
- Database có: `status = 'APPROVED'`

Ubiquitous language không chỉ là documentation — nó được enforce trong code. Tên method, tên class, và tên biến dùng cùng thuật ngữ mà domain expert dùng. Khi đọc code, bạn có thể show cho domain expert và họ hiểu được ý đồ.

Bounded Context

Bounded context là ranh giới trong đó một domain model cụ thể được định nghĩa và áp dụng. Đây là pattern quan trọng nhất trong strategic DDD.

Xem xét một hệ thống e-commerce. Từ "Product" có nghĩa khác nhau trong các context khác nhau:

Trong Catalog context, Product có name, description, và image. Trong Pricing context, nó có base price, discount, và tax rule. Trong Inventory context, nó có SKU, quantity, và reorder level.

Đây là các model khác nhau — và điều đó OK. Mỗi bounded context sở hữu model của nó và không cần biết về các context khác. Cố tạo một class "Product" duy nhất hoạt động ở mọi nơi dẫn đến "God class" phình to và rối rắm.

Subdomain

Subdomain là một phân khúc của business domain. Có ba loại:

LoạiMô tảVí dụĐầu tư
CoreLợi thế cạnh tranh — điều làm business khác biệtPricing engine, recommendation algorithmCao — custom development, DDD
SupportingCần thiết nhưng không tạo khác biệtOrder management, customer profileTrung bình — model đơn giản hơn
GenericProblem đã được giải quyết, có sẵn off-the-shelfAuthentication, email sending, paymentThấp — mua hoặc dùng SaaS

DDD có giá trị nhất cho core subdomain. Đừng áp dụng heavy DDD pattern cho generic hay supporting subdomain nơi CRUD đơn giản là đủ.

Context Mapping

Context mapping định nghĩa cách các bounded context liên hệ và giao tiếp với nhau.

Các relationship pattern phổ biến:

PatternMô tảKhi nào dùng
PartnershipHai context phát triển cùng nhau, phối hợp kế hoạchHai team làm việc chặt trên feature liên quan
Customer-SupplierUpstream (supplier) cung cấp cái downstream (customer) cầnMột team depend vào data/API của team khác
ConformistDownstream tuân theo model của upstream nguyên trạngDùng external API bạn không thể thay đổi
Anti-Corruption Layer (ACL)Downstream dịch model upstream sang ngôn ngữ riêngBảo vệ model khỏi legacy hoặc external system
Published LanguageNgôn ngữ chung cho giao tiếp (event, API)Contract integration rõ ràng giữa các context
Shared KernelHai context chia sẻ một phần modelCode chung nhỏ giữa context liên quan chặt
Separate WaysKhông integration — context hoạt động độc lậpKhi chi phí integration vượt lợi ích

Tactical DDD: Building Block

Tactical DDD cung cấp pattern để implement domain model bên trong một bounded context.

Entity

Entity là object được định nghĩa bởi identity của nó, không phải attribute. Hai entity có cùng attribute nhưng khác ID là hai object khác nhau.

public class Customer {
    private final CustomerId id;  // Identity
    private String name;
    private Email email;
    private CustomerStatus status;
 
    public void activate() {
        if (this.status == CustomerStatus.SUSPENDED) {
            throw new IllegalStateException("Cannot activate suspended customer");
        }
        this.status = CustomerStatus.ACTIVE;
    }
 
    public void suspend(String reason) {
        this.status = CustomerStatus.SUSPENDED;
        // Record domain event
    }
 
    // Equality dựa trên identity
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Customer)) return false;
        return id.equals(((Customer) o).id);
    }
 
    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

Đặc điểm chính:

  • Có unique identity tồn tại xuyên thời gian
  • Equality dựa trên identity, không phải attribute
  • Chứa business behavior (không chỉ data)
  • Mutable — state thay đổi qua business method

Value Object

Value object được định nghĩa bởi attribute của nó, không phải identity. Hai value object có cùng attribute là cùng một thứ.

// Value object — định nghĩa bởi attribute
public record Money(BigDecimal amount, Currency currency) {
 
    public Money {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount must be non-negative");
        }
        Objects.requireNonNull(currency, "Currency is required");
    }
 
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
 
    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
    }
}
 
// Sử dụng
Money price = new Money(new BigDecimal("29.99"), Currency.USD);
Money total = price.multiply(3);  // $89.97
// Value object khác — Email
public record Email(String value) {
 
    public Email {
        if (value == null || !value.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$")) {
            throw new IllegalArgumentException("Invalid email: " + value);
        }
    }
}
 
// Value object khác — Address
public record Address(String street, String city, String state, String zipCode, String country) {
 
    public Address {
        Objects.requireNonNull(street, "Street is required");
        Objects.requireNonNull(city, "City is required");
        Objects.requireNonNull(country, "Country is required");
    }
}

Đặc điểm chính:

  • Không có identity — equality dựa trên attribute
  • Immutable — operation trả về instance mới
  • Self-validating — enforce invariant trong constructor
  • Thay thế primitive type bằng domain concept (Money thay vì BigDecimal, Email thay vì String)

Aggregate

Aggregate là cluster của entity và value object được xử lý như một đơn vị duy nhất cho data change. Nó có một aggregate root — entity duy nhất mà code bên ngoài có thể tương tác.

// Order là Aggregate Root
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
    private final List<OrderItem> items;  // Entity bên trong aggregate
    private ShippingAddress shippingAddress;  // Value object
    private Money totalAmount;  // Value object
 
    // Factory method — enforce invariant khi tạo
    public static Order place(CustomerId customerId, List<OrderItem> items,
                               ShippingAddress address) {
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one item");
        }
        Order order = new Order();
        order.id = OrderId.generate();
        order.customerId = customerId;
        order.status = OrderStatus.PLACED;
        order.items = new ArrayList<>(items);
        order.shippingAddress = address;
        order.totalAmount = calculateTotal(items);
        return order;
    }
 
    // Business method — đóng gói domain logic
    public void addItem(ProductId productId, String productName,
                        Money price, int quantity) {
        if (status != OrderStatus.PLACED) {
            throw new IllegalStateException("Cannot modify confirmed order");
        }
        OrderItem item = new OrderItem(productId, productName, price, quantity);
        items.add(item);
        recalculateTotal();
    }
 
    public void removeItem(ProductId productId) {
        if (status != OrderStatus.PLACED) {
            throw new IllegalStateException("Cannot modify confirmed order");
        }
        items.removeIf(item -> item.getProductId().equals(productId));
        if (items.isEmpty()) {
            throw new IllegalStateException("Order must have at least one item");
        }
        recalculateTotal();
    }
 
    public void confirm() {
        if (status != OrderStatus.PLACED) {
            throw new IllegalStateException("Can only confirm PLACED orders");
        }
        status = OrderStatus.CONFIRMED;
    }
 
    public void ship(String trackingNumber) {
        if (status != OrderStatus.CONFIRMED) {
            throw new IllegalStateException("Can only ship CONFIRMED orders");
        }
        status = OrderStatus.SHIPPED;
    }
 
    // Internal consistency — luôn valid
    private void recalculateTotal() {
        this.totalAmount = items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.ZERO_USD, Money::add);
    }
 
    // Read-only access cho internal entity
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }
}

Aggregate Design Rule

Những rule này giữ aggregate dễ quản lý và performant:

Rule 1: Bảo vệ business invariant bên trong aggregate

Aggregate root chịu trách nhiệm đảm bảo tất cả business rule được thỏa mãn. Không code bên ngoài nào có thể đưa aggregate vào trạng thái không hợp lệ.

Rule 2: Design aggregate nhỏ

// ❌ Aggregate lớn — load mọi thứ
public class Customer {
    private List<Order> orders;        // Có thể hàng ngàn!
    private List<Address> addresses;
    private List<PaymentMethod> payments;
    private LoyaltyAccount loyalty;
}
 
// ✅ Aggregate nhỏ — reference bằng ID
public class Customer {
    private CustomerId id;
    private String name;
    private Email email;
    // Order, address, payment là aggregate riêng
}
 
public class Order {
    private OrderId id;
    private CustomerId customerId;  // Reference bằng ID, không phải object
    private List<OrderItem> items;
}

Rule 3: Reference aggregate khác bằng ID

Không bao giờ giữ direct object reference đến aggregate khác. Dùng ID để duy trì loose coupling và enable distribution.

Rule 4: Update một aggregate per transaction

Mỗi transaction chỉ modify một aggregate. Cross-aggregate consistency được xử lý qua domain event và eventual consistency.

// ❌ Update nhiều aggregate trong một transaction
@Transactional
public void placeOrder(PlaceOrderCommand cmd) {
    Order order = Order.place(...);
    orderRepo.save(order);
 
    Inventory inventory = inventoryRepo.findByProductId(cmd.productId());
    inventory.reserve(cmd.quantity());  // Aggregate khác!
    inventoryRepo.save(inventory);
}
 
// ✅ Một aggregate per transaction + domain event
@Transactional
public void placeOrder(PlaceOrderCommand cmd) {
    Order order = Order.place(...);
    orderRepo.save(order);
    eventPublisher.publish(new OrderPlacedEvent(order.getId(), order.getItems()));
}
 
// Handler riêng update inventory
@EventHandler
public void onOrderPlaced(OrderPlacedEvent event) {
    for (OrderItemData item : event.getItems()) {
        Inventory inventory = inventoryRepo.findByProductId(item.productId());
        inventory.reserve(item.quantity());
        inventoryRepo.save(inventory);
    }
}

Repository

Repository cung cấp ảo giác về một in-memory collection các aggregate. Nó abstract away persistence concern.

// Repository interface — định nghĩa trong domain layer
public interface OrderRepository {
    OrderId nextId();
    Order findById(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
    void save(Order order);
    void delete(OrderId id);
}

Rule chính:

  • Một repository per aggregate root (không phải per entity)
  • Interface sống trong domain layer
  • Implementation sống trong infrastructure/adapter layer
  • Return domain object, không bao giờ database entity

Domain Event

Domain event là điều gì đó đã xảy ra trong domain mà các phần khác của hệ thống có thể quan tâm. Nó được đặt tên ở thì quá khứ vì nó đại diện cho điều đã xảy ra.

// Domain event — immutable, thì quá khứ
public record OrderPlacedEvent(
    OrderId orderId,
    CustomerId customerId,
    Money totalAmount,
    List<OrderItemData> items,
    Instant occurredAt
) implements DomainEvent {
 
    public OrderPlacedEvent(OrderId orderId, CustomerId customerId,
                             Money totalAmount, List<OrderItemData> items) {
        this(orderId, customerId, totalAmount, items, Instant.now());
    }
}
 
public record OrderShippedEvent(
    OrderId orderId,
    String trackingNumber,
    Instant occurredAt
) implements DomainEvent {}
 
public record OrderCancelledEvent(
    OrderId orderId,
    String reason,
    Instant occurredAt
) implements DomainEvent {}

Domain event enable:

  • Decoupling giữa các bounded context
  • Eventual consistency giữa các aggregate
  • Audit trail và event sourcing
  • Trigger side effect mà không làm bẩn core logic

Domain Service

Domain service chứa business logic không thuộc tự nhiên về bất kỳ entity hay value object đơn lẻ nào. Nó operate trên nhiều aggregate hoặc cần thông tin bên ngoài.

// Domain service — business logic span nhiều aggregate
public class PricingService {
 
    public Money calculateOrderTotal(List<OrderItem> items,
                                      DiscountPolicy discountPolicy,
                                      TaxPolicy taxPolicy) {
        Money subtotal = items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.ZERO_USD, Money::add);
 
        Money afterDiscount = discountPolicy.apply(subtotal);
        Money withTax = taxPolicy.apply(afterDiscount);
        return withTax;
    }
}
 
// Domain service — chuyển tiền giữa account
public class TransferService {
 
    public void transfer(Account from, Account to, Money amount) {
        if (!from.hasBalance(amount)) {
            throw new InsufficientBalanceException(from.getId(), amount);
        }
        from.debit(amount);
        to.credit(amount);
    }
}

Domain service vs Application service:

Khía cạnhDomain ServiceApplication Service
ChứaBusiness logicOrchestration/coordination
DependencyChỉ domain objectRepository, port, external service
Ví dụTính pricing, validate transferPlace order use case, gửi notification
LayerDomain layerApplication/use case layer

Anti-Corruption Layer (ACL)

Khi integrate với external system hoặc legacy code, Anti-Corruption Layer dịch giữa external model và domain model của bạn, ngăn external model làm hỏng bounded context của bạn.

// Anti-corruption layer — dịch legacy model sang domain model
public class LegacyCustomerAdapter implements CustomerInfoProvider {
 
    private final LegacyCustomerApiClient legacyClient;
 
    @Override
    public CustomerProfile getProfile(CustomerId customerId) {
        // Gọi legacy API với terminology riêng
        LegacyCustRecord record = legacyClient.getCustByAcctNbr(
            customerId.value()
        );
 
        // Dịch sang domain model của bạn
        return new CustomerProfile(
            new CustomerId(record.getAcctNbr()),
            record.getFirstNm() + " " + record.getLastNm(),
            new Email(record.getEmailAddr()),
            mapLegacyTier(record.getCustTierCd())  // "G" → GOLD
        );
    }
 
    private CustomerTier mapLegacyTier(String legacyCode) {
        return switch (legacyCode) {
            case "G" -> CustomerTier.GOLD;
            case "S" -> CustomerTier.SILVER;
            case "B" -> CustomerTier.BRONZE;
            default -> CustomerTier.STANDARD;
        };
    }
}

ACL giữ domain model sạch ngay cả khi external system dùng naming khó hiểu, concept khác, hoặc convention lỗi thời.


Event Storming

Event Storming là kỹ thuật workshop cộng tác để khám phá domain model. Được tạo bởi Alberto Brandolini, nó tập hợp developer và domain expert cùng nhau để map business process bằng sticky note.

Cách Hoạt Động

Bước 1: Domain Event (Cam)

Mọi người viết domain event lên sticky note cam và đặt trên timeline.

[Order Placed] → [Payment Received] → [Inventory Reserved] → [Order Shipped] → [Order Delivered]

Bước 2: Command (Xanh)

Cái gì trigger mỗi event? Command là action mà user hoặc system thực hiện.

{Place Order} → [Order Placed]
{Process Payment} → [Payment Received]
{Reserve Inventory} → [Inventory Reserved]
{Ship Order} → [Order Shipped]

Bước 3: Aggregate (Vàng)

Aggregate nào xử lý mỗi cặp command-event?

Bước 4: Bounded Context

Nhóm aggregate làm việc chặt chẽ với nhau thành bounded context.

Event Storming tạo ra sự hiểu biết chung về domain, xác định bounded context, và phát hiện domain event kết nối chúng.


Ví Dụ Thực Tế: Hệ Thống Order E-Commerce

Hãy gom tất cả lại với ví dụ e-commerce thực tế.

Bounded Context Đã Xác Định

Order Context Implementation

// === Value Object ===
 
public record OrderId(String value) {
    public static OrderId generate() {
        return new OrderId(UUID.randomUUID().toString());
    }
}
 
public record CustomerId(String value) {}
 
public record ProductId(String value) {}
 
public record Money(BigDecimal amount, String currency) {
    public static final Money ZERO_USD = new Money(BigDecimal.ZERO, "USD");
 
    public Money add(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
 
    public Money multiply(int qty) {
        return new Money(amount.multiply(BigDecimal.valueOf(qty)), currency);
    }
}
 
// === Entity ===
 
public class OrderItem {
    private final ProductId productId;
    private final String productName;
    private final Money unitPrice;
    private int quantity;
 
    public Money subtotal() {
        return unitPrice.multiply(quantity);
    }
 
    public void increaseQuantity(int additional) {
        if (additional <= 0) throw new IllegalArgumentException("Must be positive");
        this.quantity += additional;
    }
}
 
// === Aggregate Root ===
 
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
    private List<OrderItem> items;
    private ShippingAddress shippingAddress;
    private Money totalAmount;
    private final List<DomainEvent> domainEvents = new ArrayList<>();
 
    public static Order place(CustomerId customerId, List<OrderItem> items,
                               ShippingAddress address) {
        if (items.isEmpty()) {
            throw new EmptyOrderException();
        }
 
        Order order = new Order(OrderId.generate(), customerId,
            OrderStatus.PLACED, items, address);
        order.recalculateTotal();
 
        // Register domain event
        order.domainEvents.add(new OrderPlacedEvent(
            order.id, customerId, order.totalAmount, order.getItemData()
        ));
        return order;
    }
 
    public void confirm(PaymentId paymentId) {
        if (status != OrderStatus.PLACED) {
            throw new InvalidOrderStateException(id, status, "confirm");
        }
        this.status = OrderStatus.CONFIRMED;
        domainEvents.add(new OrderConfirmedEvent(id, paymentId));
    }
 
    public void ship(String trackingNumber) {
        if (status != OrderStatus.CONFIRMED) {
            throw new InvalidOrderStateException(id, status, "ship");
        }
        this.status = OrderStatus.SHIPPED;
        domainEvents.add(new OrderShippedEvent(id, trackingNumber));
    }
 
    public void cancel(String reason) {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
            throw new InvalidOrderStateException(id, status, "cancel");
        }
        this.status = OrderStatus.CANCELLED;
        domainEvents.add(new OrderCancelledEvent(id, reason));
    }
 
    // Collect và clear domain event
    public List<DomainEvent> pullDomainEvents() {
        List<DomainEvent> events = new ArrayList<>(domainEvents);
        domainEvents.clear();
        return events;
    }
}

Application Service (Use Case)

// Application service — orchestrate domain object và port
public class PlaceOrderUseCase {
 
    private final OrderRepository orderRepository;
    private final ProductCatalogPort productCatalog;
    private final EventPublisher eventPublisher;
 
    public PlaceOrderUseCase(OrderRepository orderRepository,
                              ProductCatalogPort productCatalog,
                              EventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.productCatalog = productCatalog;
        this.eventPublisher = eventPublisher;
    }
 
    public OrderId execute(PlaceOrderCommand command) {
        // Validate product tồn tại và lấy giá hiện tại
        List<OrderItem> items = command.items().stream()
            .map(item -> {
                ProductInfo product = productCatalog.getProduct(item.productId());
                return new OrderItem(
                    product.id(), product.name(), product.price(), item.quantity()
                );
            })
            .toList();
 
        ShippingAddress address = new ShippingAddress(
            command.street(), command.city(), command.state(),
            command.zipCode(), command.country()
        );
 
        // Tạo aggregate — business rule enforce bên trong
        Order order = Order.place(
            new CustomerId(command.customerId()), items, address
        );
 
        // Persist
        orderRepository.save(order);
 
        // Publish domain event
        order.pullDomainEvents().forEach(eventPublisher::publish);
 
        return order.getId();
    }
}

Cấu Trúc Folder

src/main/java/com/example/ordering/
├── domain/                              # Domain Layer
│   ├── model/
│   │   ├── Order.java                   # Aggregate root
│   │   ├── OrderItem.java               # Entity
│   │   ├── OrderStatus.java             # Enum
│   │   ├── OrderId.java                 # Value object
│   │   ├── CustomerId.java              # Value object
│   │   ├── Money.java                   # Value object
│   │   ├── ShippingAddress.java         # Value object
│   │   └── ProductId.java               # Value object
│   ├── event/
│   │   ├── DomainEvent.java             # Base interface
│   │   ├── OrderPlacedEvent.java
│   │   ├── OrderConfirmedEvent.java
│   │   ├── OrderShippedEvent.java
│   │   └── OrderCancelledEvent.java
│   ├── exception/
│   │   ├── EmptyOrderException.java
│   │   └── InvalidOrderStateException.java
│   ├── port/
│   │   ├── OrderRepository.java         # Driven port
│   │   ├── ProductCatalogPort.java      # Driven port
│   │   └── EventPublisher.java          # Driven port
│   └── service/
│       └── PricingService.java          # Domain service
│
├── application/                         # Application Layer
│   ├── PlaceOrderUseCase.java
│   ├── ConfirmOrderUseCase.java
│   ├── CancelOrderUseCase.java
│   └── command/
│       ├── PlaceOrderCommand.java
│       └── CancelOrderCommand.java
│
├── infrastructure/                      # Infrastructure Layer
│   ├── persistence/
│   │   ├── OrderJpaEntity.java
│   │   ├── OrderJpaRepository.java
│   │   └── OrderRepositoryImpl.java
│   ├── messaging/
│   │   └── KafkaEventPublisher.java
│   ├── rest/
│   │   ├── OrderController.java
│   │   └── dto/
│   │       ├── PlaceOrderRequest.java
│   │       └── OrderResponse.java
│   └── external/
│       └── CatalogServiceClient.java    # ACL cho catalog context

DDD Và Microservices

DDD và microservices là đối tác tự nhiên. Bounded context map trực tiếp sang microservice boundary.

Key alignment:

  • Mỗi bounded context = một microservice
  • Mỗi microservice sở hữu database riêng (database per service)
  • Giao tiếp giữa context qua domain event (async)
  • Anti-corruption layer cho mỗi integration point
  • Ubiquitous language scope trong mỗi service

Cảnh báo: Đừng dùng microservice boundary để khám phá bounded context. Khám phá bounded context trước (qua event storming và domain analysis), rồi mới quyết định context nào trở thành microservice.


DDD Với Hexagonal Và Clean Architecture

DDD, hexagonal architecture, và clean architecture bổ sung cho nhau:

LayerDDD PatternHexagonal ConceptClean Architecture
Inner coreEntity, Value Object, Domain EventCore business logicEntities layer
Business ruleAggregate, Domain ServiceCore business logicUse Cases layer
Interface boundaryRepository interfacePortInterface Adapter
External systemRepository implementation, ACLAdapterFrameworks & Drivers

Sự kết hợp hoạt động như sau:

  • DDD cho bạn biết model cái gì (aggregate, event, value object)
  • Hexagonal/Clean cho bạn biết đặt ở đâu (layer, port, adapter)
  • Dependency rule cho bạn biết kết nối cách nào (tất cả dependency chỉ vào trong)

Khi Nào DDD Là Quá Mức

DDD thêm complexity đáng kể. Đây là khi nào nên dùng — và khi nào nên bỏ qua.

Dùng DDD khi:

  • Domain phức tạp với nhiều business rule
  • Domain expert sẵn sàng và muốn cộng tác
  • Project sống lâu dài và sẽ phát triển
  • Nhiều bounded context với model khác nhau
  • Business logic là competitive advantage

Bỏ qua DDD khi:

  • CRUD đơn giản với business logic tối thiểu
  • Không tiếp cận được domain expert
  • Project ngắn hạn hoặc prototype
  • Domain đã được hiểu rõ và ổn định
  • Team nhỏ nơi ceremony thêm tạo overhead

Cách tiếp cận thực dụng:

Bạn không cần all-in DDD. Dùng pattern nào thêm giá trị:

PatternKhi nào dùngBỏ qua khi
Ubiquitous languageLuôn luôn — không tốn gì, luôn có íchKhông bao giờ bỏ qua
Bounded contextNhiều team hoặc domain phức tạpApplication nhỏ đơn lẻ
AggregateBusiness rule và invariant phức tạpCRUD operation đơn giản
Value objectKhi có domain conceptKhông đáng cho type tầm thường
Domain eventCần cross-aggregate communicationMột aggregate, không side effect
Event StormingDomain phức tạp hoặc chưa quenDomain đơn giản, đã hiểu rõ
Repository patternNhiều persistence option hoặc cần testabilityDùng ORM trực tiếp là đủ

Những Lỗi Phổ Biến

1. Anemic Domain Model

// ❌ Anemic — entity chỉ là túi data
public class Order {
    private String status;
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
}
 
// Service làm mọi thứ
public class OrderService {
    public void confirmOrder(Order order) {
        if (!order.getStatus().equals("PLACED")) {
            throw new RuntimeException("Invalid state");
        }
        order.setStatus("CONFIRMED");
    }
}
// ✅ Rich domain model — entity sở hữu behavior
public class Order {
    private OrderStatus status;
 
    public void confirm() {
        if (status != OrderStatus.PLACED) {
            throw new InvalidOrderStateException(id, status, "confirm");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

2. Aggregate Khổng Lồ

// ❌ Quá lớn — load mọi thứ, lock quá nhiều
public class Customer {
    private List<Order> orders;         // Có thể 10,000+
    private List<Review> reviews;       // Coupling không cần thiết
    private ShoppingCart cart;
    private WishList wishList;
    private LoyaltyAccount loyalty;
}
// ✅ Aggregate nhỏ, focused, kết nối bằng ID
public class Customer {
    private CustomerId id;
    private String name;
    private Email email;
}
 
public class Order {
    private OrderId id;
    private CustomerId customerId;  // Reference bằng ID
}
 
public class ShoppingCart {
    private CartId id;
    private CustomerId customerId;  // Reference bằng ID
}

3. Chia Sẻ Aggregate Qua Bounded Context

// ❌ "Product" chung dùng trong catalog, pricing, và inventory
public class Product {
    private String name;
    private String description;     // Catalog
    private BigDecimal price;       // Pricing
    private List<Discount> discounts; // Pricing
    private int stockQuantity;      // Inventory
    private String warehouseLocation; // Inventory
    // God class — ai cũng thay đổi
}
// ✅ Mỗi context có model riêng
// Catalog context
public class CatalogProduct {
    private ProductId id;
    private String name;
    private String description;
    private List<String> images;
}
 
// Pricing context
public class PricedItem {
    private ProductId productId;
    private Money basePrice;
    private List<DiscountRule> applicableDiscounts;
}
 
// Inventory context
public class StockItem {
    private ProductId productId;
    private int quantity;
    private String warehouseId;
    private int reorderLevel;
}

4. Dùng DDD Ở Mọi Nơi

Đừng áp dụng aggregate, repository, và domain event cho trang setting đơn giản hay admin CRUD. Dành DDD pattern cho core domain nơi business complexity xứng đáng với đầu tư.


Tổng Kết

Domain-Driven Design vừa là triết lý vừa là toolkit để build phần mềm phản ánh business domain.

Strategic pattern (bức tranh lớn):

  • Ubiquitous language — từ vựng chung giữa developer và domain expert, enforce trong code
  • Bounded context — ranh giới rõ ràng nơi mỗi model có ý nghĩa cụ thể
  • Context mapping — mối quan hệ giữa các context (partnership, customer-supplier, ACL, v.v.)
  • Subdomain — core (đầu tư mạnh), supporting (model đơn giản hơn), generic (mua/dùng SaaS)

Tactical pattern (building block):

  • Entity — object có identity và behavior
  • Value object — object immutable định nghĩa bởi attribute (Money, Email, Address)
  • Aggregate — consistency boundary với root entity
  • Repository — persistence abstraction, một per aggregate root
  • Domain event — record thì quá khứ về những gì đã xảy ra
  • Domain service — business logic không thuộc về entity đơn lẻ

Rule chính:

  • Design aggregate nhỏ, reference aggregate khác bằng ID
  • Một aggregate per transaction, eventual consistency giữa các aggregate
  • Rich domain model có behavior, không phải anemic data bag
  • Ubiquitous language ở mọi nơi — code nên đọc như business

DDD + architecture pattern:

  • Bounded context map tự nhiên sang microservice boundary
  • DDD domain layer fit hoàn hảo trong hexagonal/clean architecture
  • Domain event enable loose coupling giữa các context

"The heart of software is its ability to solve domain-related problems for its users." — Eric Evans


Tiếp Theo Trong Software Architecture Series

Đây là bài 10 trong 12 bài của series Software Architecture Patterns:

Bài liên quan:

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.