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/ordersinsert một row vào bảngordersvà update bảnginventory."
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ại | Mô tả | Ví dụ | Đầu tư |
|---|---|---|---|
| Core | Lợi thế cạnh tranh — điều làm business khác biệt | Pricing engine, recommendation algorithm | Cao — custom development, DDD |
| Supporting | Cần thiết nhưng không tạo khác biệt | Order management, customer profile | Trung bình — model đơn giản hơn |
| Generic | Problem đã được giải quyết, có sẵn off-the-shelf | Authentication, email sending, payment | Thấ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:
| Pattern | Mô tả | Khi nào dùng |
|---|---|---|
| Partnership | Hai context phát triển cùng nhau, phối hợp kế hoạch | Hai team làm việc chặt trên feature liên quan |
| Customer-Supplier | Upstream (supplier) cung cấp cái downstream (customer) cần | Một team depend vào data/API của team khác |
| Conformist | Downstream tuân theo model của upstream nguyên trạng | Dùng external API bạn không thể thay đổi |
| Anti-Corruption Layer (ACL) | Downstream dịch model upstream sang ngôn ngữ riêng | Bảo vệ model khỏi legacy hoặc external system |
| Published Language | Ngôn ngữ chung cho giao tiếp (event, API) | Contract integration rõ ràng giữa các context |
| Shared Kernel | Hai context chia sẻ một phần model | Code chung nhỏ giữa context liên quan chặt |
| Separate Ways | Không integration — context hoạt động độc lập | Khi 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 (
Moneythay vìBigDecimal,Emailthay 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ạnh | Domain Service | Application Service |
|---|---|---|
| Chứa | Business logic | Orchestration/coordination |
| Dependency | Chỉ domain object | Repository, port, external service |
| Ví dụ | Tính pricing, validate transfer | Place order use case, gửi notification |
| Layer | Domain layer | Application/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 contextDDD 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:
| Layer | DDD Pattern | Hexagonal Concept | Clean Architecture |
|---|---|---|---|
| Inner core | Entity, Value Object, Domain Event | Core business logic | Entities layer |
| Business rule | Aggregate, Domain Service | Core business logic | Use Cases layer |
| Interface boundary | Repository interface | Port | Interface Adapter |
| External system | Repository implementation, ACL | Adapter | Frameworks & 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ị:
| Pattern | Khi nào dùng | Bỏ qua khi |
|---|---|---|
| Ubiquitous language | Luôn luôn — không tốn gì, luôn có ích | Không bao giờ bỏ qua |
| Bounded context | Nhiều team hoặc domain phức tạp | Application nhỏ đơn lẻ |
| Aggregate | Business rule và invariant phức tạp | CRUD operation đơn giản |
| Value object | Khi có domain concept | Không đáng cho type tầm thường |
| Domain event | Cần cross-aggregate communication | Một aggregate, không side effect |
| Event Storming | Domain phức tạp hoặc chưa quen | Domain đơn giản, đã hiểu rõ |
| Repository pattern | Nhiều persistence option hoặc cần testability | Dù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:
- ✅ ARCH-1: Software Architecture Patterns Roadmap
- ✅ ARCH-2: Monolithic Architecture
- ✅ ARCH-3: Layered (N-Tier) Architecture
- ✅ ARCH-4: MVC, MVP & MVVM Patterns
- ✅ ARCH-5: Microservices Architecture
- ✅ ARCH-6: Event-Driven Architecture
- ✅ ARCH-7: CQRS & Event Sourcing
- ✅ ARCH-8: Hexagonal Architecture
- ✅ ARCH-9: Clean Architecture
- ✅ ARCH-10: Domain-Driven Design (bài này)
- 🔜 ARCH-11: Serverless & Function-as-a-Service
- 🔜 ARCH-12: Choosing the Right Architecture
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.