Common DDD Pitfalls - Những Lỗi Thường Gặp¶
1. Anemic Domain Model - "Cơ Thể Không Có Máu"¶
Vấn Đề: Domain Objects Chỉ Là Data Container¶
Ví dụ sai:
// Anemic Order - chỉ có data, không có behavior
class Order {
id: string;
customerId: string;
items: OrderItem[];
status: string;
totalAmount: number;
// Chỉ có getter/setter, không có logic
}
// Logic bị đẩy xuống Service
class OrderService {
createOrder(customerId: string, items: OrderItem[]) {
// Tính toán ở đây thay vì trong Order
let total = 0;
for (let item of items) {
total += item.price * item.quantity;
}
// Validation ở đây thay vì trong Order
if (total > 100000000) {
throw new Error("Order too large");
}
// Tạo order
const order = new Order();
order.customerId = customerId;
order.items = items;
order.totalAmount = total;
order.status = "PENDING";
return this.orderRepository.save(order);
}
}
Giải Pháp: Rich Domain Model¶
Ví dụ đúng:
// Rich Order - có behavior và business logic
class Order {
private constructor(
private readonly id: OrderId,
private readonly customerId: CustomerId,
private items: OrderItem[],
private status: OrderStatus,
private totalAmount: Money
) {}
// Factory method với validation
static create(customerId: CustomerId, items: OrderItem[]): Order {
if (items.length === 0) {
throw new Error("Order must have at least one item");
}
const total = Money.sum(items.map(item => item.totalPrice()));
if (total.isGreaterThan(Money.fromVND(100000000))) {
throw new Error("Order amount exceeds limit");
}
return new Order(
OrderId.generate(),
customerId,
items,
OrderStatus.PENDING,
total
);
}
// Business methods
confirm(): void {
if (!this.status.canTransitionTo(OrderStatus.CONFIRMED)) {
throw new Error("Cannot confirm order in current status");
}
this.status = OrderStatus.CONFIRMED;
}
addItem(item: OrderItem): void {
const newTotal = this.totalAmount.add(item.totalPrice());
if (newTotal.isGreaterThan(Money.fromVND(100000000))) {
throw new Error("Adding item would exceed order limit");
}
this.items.push(item);
this.totalAmount = newTotal;
}
}
// Service chỉ orchestrate
class OrderService {
createOrder(customerId: CustomerId, items: OrderItem[]): Order {
const order = Order.create(customerId, items); // Logic trong domain
return this.orderRepository.save(order);
}
}
2. God Objects/Aggregates - "Vua Quá Khổ"¶
Vấn Đề: Aggregate Quá Lớn, Chứa Quá Nhiều Thứ¶
Ví dụ sai:
// Customer God Object - chứa mọi thứ
class Customer {
id: CustomerId;
name: string;
email: string;
// Profile info
dateOfBirth: Date;
gender: string;
phoneNumber: string;
// Address info
addresses: Address[];
defaultShippingAddress: Address;
defaultBillingAddress: Address;
// Order history
orders: Order[];
totalOrderCount: number;
totalSpent: Money;
// Loyalty info
loyaltyPoints: number;
loyaltyTier: string;
loyaltyExpiry: Date;
// Payment info
paymentMethods: PaymentMethod[];
defaultPaymentMethod: PaymentMethod;
// Preferences
emailNotifications: boolean;
smsNotifications: boolean;
marketingConsent: boolean;
// Support info
supportTickets: SupportTicket[];
lastSupportInteraction: Date;
// Analytics
lastLoginDate: Date;
sessionCount: number;
pageViews: number;
// Giảm giá
availableCoupons: Coupon[];
usedCoupons: Coupon[];
}
Vấn đề:
- Aggregate quá lớn, khó maintain
- Vi phạm Single Responsibility Principle
- Performance issue khi load/save
- Nhiều team cùng sửa → conflict
Giải Pháp: Chia Nhỏ Thành Nhiều Bounded Context¶
Ví dụ đúng:
// Customer Context - chỉ thông tin cơ bản
class Customer {
constructor(
private readonly id: CustomerId,
private name: CustomerName,
private email: Email,
private registrationDate: Date
) {}
updateProfile(name: CustomerName, email: Email): void {
this.name = name;
this.email = email;
}
}
// Order Context - riêng biệt
class OrderHistory {
constructor(
private readonly customerId: CustomerId,
private orders: Order[]
) {}
addOrder(order: Order): void {
this.orders.push(order);
}
getTotalSpent(): Money {
return Money.sum(this.orders.map(o => o.totalAmount));
}
}
// Loyalty Context - riêng biệt
class LoyaltyAccount {
constructor(
private readonly customerId: CustomerId,
private points: number,
private tier: LoyaltyTier
) {}
earnPoints(amount: number): void {
this.points += amount;
this.updateTier();
}
}
3. Quá Nhiều Bounded Contexts - "Chia Nhỏ Quá Mức"¶
Vấn Đề: Micro-Contexts¶
Ví dụ sai:
📁 user-name-context/
📁 user-email-context/
📁 user-phone-context/
📁 user-address-context/
📁 order-total-context/
📁 order-status-context/
📁 order-items-context/
Vấn đề: - Quá nhiều context nhỏ → overhead - Network calls nhiều - Inconsistency giữa contexts - Team không biết context nào chịu trách nhiệm gì
Giải Pháp: Bounded Context Có Ý Nghĩa¶
Ví dụ đúng:
📁 customer-management/ ← Quản lý thông tin khách hàng
📁 order-processing/ ← Xử lý đơn hàng
📁 inventory-management/ ← Quản lý kho
📁 payment-processing/ ← Thanh toán
📁 shipping-logistics/ ← Vận chuyển
Nguyên tắc chia Bounded Context: 1. Business Capability: Mỗi context giải quyết 1 vấn đề business 2. Team Ownership: 1 team chịu trách nhiệm 1-2 contexts 3. Data Autonomy: Mỗi context có database riêng 4. Communication: Contexts giao tiếp qua events/APIs
4. Repository Leakage - "Rò Rỉ Kỹ Thuật"¶
Vấn Đề: Domain Biết Về Database¶
Ví dụ sai:
// Domain service biết về database query
class OrderDomainService {
calculateCustomerDiscount(customerId: string): number {
// SAI: Domain service gọi trực tiếp database
const orders = this.database.query(`
SELECT * FROM orders
WHERE customer_id = ? AND status = 'COMPLETED'
ORDER BY created_date DESC LIMIT 10
`, [customerId]);
if (orders.length > 5) {
return 0.1; // 10% discount
}
return 0;
}
}
// Aggregate biết về persistence details
class Order {
save(): void {
// SAI: Domain object tự save
database.execute(`
INSERT INTO orders (id, customer_id, total)
VALUES (?, ?, ?)
`, [this.id, this.customerId, this.total]);
}
}
Giải Pháp: Repository Pattern Đúng Cách¶
Ví dụ đúng:
// Domain Interface - không biết về database
interface OrderRepository {
save(order: Order): void;
findById(id: OrderId): Order | null;
findCompletedOrdersByCustomer(customerId: CustomerId): Order[];
}
// Domain Service sử dụng interface
class OrderDomainService {
constructor(private orderRepo: OrderRepository) {}
calculateCustomerDiscount(customerId: CustomerId): number {
// Dùng domain method, không biết về SQL
const completedOrders = this.orderRepo.findCompletedOrdersByCustomer(customerId);
if (completedOrders.length > 5) {
return 0.1;
}
return 0;
}
}
// Infrastructure Implementation - biết về database
class SqlOrderRepository implements OrderRepository {
save(order: Order): void {
// Infrastructure lo database
this.database.query(`INSERT INTO orders...`);
}
findCompletedOrdersByCustomer(customerId: CustomerId): Order[] {
const rows = this.database.query(`SELECT * FROM orders WHERE...`);
return rows.map(row => this.toDomainObject(row));
}
}
5. Distributed Monolith - "Monolith Ряn Rải"¶
Vấn Đề: Microservices Với Tight Coupling¶
Ví dụ sai:
graph TD
A[Order Service] -->|Direct HTTP| B[Customer Service]
A -->|Direct HTTP| C[Inventory Service]
A -->|Direct HTTP| D[Payment Service]
A -->|Direct HTTP| E[Shipping Service]
B -->|Direct HTTP| C
C -->|Direct HTTP| D
D -->|Direct HTTP| E
E -->|Direct HTTP| A
Vấn đề: - Services phụ thuộc lẫn nhau - Không thể deploy riêng lẻ - Cascading failures - Performance bottleneck
Giải Pháp: Event-Driven Architecture¶
Ví dụ đúng:
graph TD
A[Order Service] -->|OrderPlacedEvent| F[Event Bus]
F -->|Event| B[Customer Service]
F -->|Event| C[Inventory Service]
F -->|Event| D[Payment Service]
F -->|Event| E[Shipping Service]
B -->|CustomerUpdatedEvent| F
C -->|StockReservedEvent| F
D -->|PaymentProcessedEvent| F
E -->|ShipmentCreatedEvent| F
6. Wrong Abstraction - "Trừu Tượng Sai"¶
Vấn Đề: Generic Repository/Service¶
Ví dụ sai:
// Generic repository quá abstract
interface GenericRepository<T> {
save(entity: T): void;
findById(id: string): T;
findAll(): T[];
delete(id: string): void;
query(sql: string): T[]; // Leak implementation detail
}
class OrderRepository extends GenericRepository<Order> {
// Inherited generic methods không express domain concepts
}
// Generic service
class GenericDomainService<T> {
create(entity: T): T { /* generic logic */ }
update(entity: T): T { /* generic logic */ }
delete(id: string): void { /* generic logic */ }
}
Giải Pháp: Domain-Specific Interfaces¶
Ví dụ đúng:
// Domain-specific repository
interface OrderRepository {
save(order: Order): void;
findById(orderId: OrderId): Order | null;
// Domain-specific queries
findPendingOrdersByCustomer(customerId: CustomerId): Order[];
findOrdersToShipToday(): Order[];
findHighValueOrders(minAmount: Money): Order[];
countOrdersInLastMonth(customerId: CustomerId): number;
}
// Domain-specific service
interface OrderDomainService {
// Express business operations
placeOrder(customerId: CustomerId, items: OrderItem[]): Order;
cancelOrder(orderId: OrderId, reason: string): void;
calculateShipping(order: Order, address: Address): Money;
applyDiscount(order: Order, coupon: Coupon): void;
}
7. Event Sourcing Overuse - "Lạm Dụng Event Sourcing"¶
Vấn Đề: Dùng Event Sourcing Cho Mọi Thứ¶
Ví dụ sai:
// Dùng Event Sourcing cho simple CRUD
class CustomerProfileEventStore {
events: Event[] = [
{ type: "NameChanged", data: { name: "John" } },
{ type: "NameChanged", data: { name: "Johnny" } },
{ type: "EmailChanged", data: { email: "john@gmail.com" } },
{ type: "EmailChanged", data: { email: "johnny@gmail.com" } },
// 1000 events chỉ để update profile...
];
}
Vấn đề: - Overhead lớn cho simple operations - Query phức tạp - Performance issues - Overkill cho most use cases
Giải Pháp: Chọn Đúng Tool Cho Đúng Job¶
Event Sourcing cho: - Financial transactions (cần audit trail) - Complex business processes - Temporal queries (state tại thời điểm X)
Traditional storage cho: - Simple CRUD operations - Reference data (countries, categories) - User profiles, settings
8. Poor Testing Strategy - "Test Sai Cách"¶
Vấn Đề: Test Implementation Thay Vì Behavior¶
Ví dụ sai:
// Test implementation details
test('Order should have items array', () => {
const order = new Order();
expect(order.items).toBeInstanceOf(Array); // Test implementation
expect(order.items.length).toBe(0); // Test implementation
});
// Test database operations
test('OrderRepository should call database.save', () => {
const mockDb = jest.mock(Database);
const repo = new OrderRepository(mockDb);
repo.save(order);
expect(mockDb.save).toHaveBeenCalled(); // Test implementation
});
Giải Pháp: Test Business Behavior¶
Ví dụ đúng:
// Test business behavior
test('Should not allow order with total exceeding limit', () => {
const expensiveItems = [
OrderItem.create(productId, 1000, Money.fromVND(100000000))
];
expect(() => {
Order.create(customerId, expensiveItems);
}).toThrow('Order amount exceeds limit');
});
// Test domain logic
test('Should calculate correct discount for loyal customers', () => {
const loyalCustomer = CustomerId.of('loyal-customer');
const mockRepo = createMockRepository([
// Setup 6 completed orders for this customer
]);
const service = new OrderDomainService(mockRepo);
const discount = service.calculateCustomerDiscount(loyalCustomer);
expect(discount).toBe(0.1); // Test business rule
});
9. Premature Performance Optimization¶
Vấn Đề: Tối Ưu Hóa Quá Sớm¶
Ví dụ sai:
// Cache everything
class OrderService {
private cache = new Map();
getOrder(id: string): Order {
// Premature caching
if (this.cache.has(id)) {
return this.cache.get(id);
}
const order = this.orderRepo.findById(id);
this.cache.set(id, order); // Cache everything
return order;
}
}
// Denormalize everything
class OrderSummary {
// Duplicate data for "performance"
customerName: string; // From Customer
customerEmail: string; // From Customer
productNames: string[]; // From Products
totalAmount: number; // Calculated
// ... more duplicated data
}
Giải Pháp: Domain-First, Optimize Later¶
Ví dụ đúng:
// Start with clean domain model
class Order {
// Focus on business logic first
static create(customerId: CustomerId, items: OrderItem[]): Order {
// Domain logic
}
calculateTotal(): Money {
// Business calculation
}
}
// Add performance optimizations when needed
class OrderQueryService {
// Add read models when performance issues arise
getOrderSummary(orderId: OrderId): OrderSummaryView {
// Optimized for reads
}
}
10. Checklist: Tránh Những Lỗi Này¶
Domain Layer¶
- [ ] Domain objects có behavior, không chỉ data
- [ ] Business rules ở trong domain, không ở service
- [ ] Aggregates có kích thước hợp lý
- [ ] Repository interfaces ở domain layer
Application Layer¶
- [ ] Application services chỉ orchestrate
- [ ] Không có business logic ở application layer
- [ ] Use cases rõ ràng và đơn giản
Infrastructure Layer¶
- [ ] Database logic chỉ ở infrastructure
- [ ] Domain không biết về database/web/external APIs
- [ ] Implementations có thể thay đổi mà không ảnh hưởng domain
Bounded Contexts¶
- [ ] Mỗi context có business capability rõ ràng
- [ ] Contexts giao tiếp qua well-defined interfaces
- [ ] Không có shared database giữa contexts
Events¶
- [ ] Events represent business facts
- [ ] Event handlers are idempotent
- [ ] Event schema versioning strategy
Testing¶
- [ ] Test business behavior, không test implementation
- [ ] Unit tests cho domain logic
- [ ] Integration tests cho workflows
Kết Luận¶
DDD là về modeling business domain, không phải về technology.
Hầu hết các lỗi đều xuất phát từ việc: 1. Tập trung vào technology thay vì business 2. Over-engineering hoặc under-engineering 3. Không hiểu rõ domain
Remember: Start simple, understand the domain deeply, then apply DDD patterns where they add value.