🌏 閱讀中文版本
Have you experienced this scenario: Your product manager says “Support a new discount rule,” and suddenly you’re modifying OrderService, DiscountService, PromotionService, PricingService, and CustomerService. By the time you’re done, you realize this “simple” requirement touched 5 different modules.
This isn’t an edge case. It’s a symptom of poor system design.
Domain-Driven Design (DDD) isn’t a new concept, but many engineers dismiss it as “complex” or “over-engineering.” The irony? DDD exists to do the opposite: transform complexity into simplicity through clear boundaries and well-defined responsibilities.
This article doesn’t dive into DDD’s history or academic definitions. We start with pain points and explore, through three different roles’ perspectives, how DDD solves fundamental system design problems.
Core Concepts: DDD’s Four Pillars
Before diving into the stories, let’s establish common language. DDD rests on four tightly interconnected concepts:
1. Aggregate – The Guardian of Business Rules
What is an aggregate?
An aggregate is a domain object that enforces all business rules within its scope. Simply put: an aggregate is a self-protecting entity that forbids operations violating business rules.
In an order system, Order is an aggregate. It manages OrderLineItem, OrderDiscount, and other child objects. Crucially, Order won’t let you directly modify a LineItem’s price. All business validations (Is discount valid? Is stock available? Does customer have sufficient credit?) are Order’s responsibility.
Why does it matter?
Without aggregates, business logic scatters: – Discount service validates discount validity – Customer service validates customer credit – Inventory service validates stock – Order service orchestrates all validations
Result: Changing any business rule requires modifying multiple places.
With aggregates, logic centralizes:
Order.applyDiscount(code) { // Order validates all rules internally // Change rules only in Order }
Aggregate Boundary and Protection
graph LR
User["🧑 External Caller"]
subgraph Agg ["📦 Order Aggregate"]
Root["Order Aggregate Root"]
Child1["LineItem Entity 1"]
Child2["LineItem Entity 2"]
Val["OrderDiscount Value Object"]
Rules["✅ Validation Rules<br/>• Stock check<br/>• Discount legality<br/>• Price validity<br/>• Customer credit"]
end
User -->|Can only call<br/>applyDiscount method| Agg
Root --> Child1
Root --> Child2
Root --> Val
Root --> Rules
BadWay["❌ Not allowed<br/>Direct modification<br/>of LineItem.price"]
User -.->|Forbidden| BadWay
style Agg fill:#f3e5f5
style Root fill:#9c27b0,color:#fff
style Rules fill:#ce93d8
style BadWay fill:#ffcdd2
2. Ubiquitous Language – Cross-Role Communication
What is ubiquitous language?
Developers, product managers, and business analysts use the same vocabulary. Not “our Order Service,” but “Order Aggregate.” Not “user table,” but “Customer.”
It sounds trivial but carries immense power. When everyone says “An order cannot be modified after payment” instead of “post_status = 2 blocks updates,” understanding becomes instant and accurate.
3. Bounded Context – Clear Boundaries
What is a bounded context?
Complex systems span multiple business domains. Shopping Cart and Order systems seem related, but they define “Customer” differently:
- Shopping Cart BC: Customer = ID + Shopping Preferences + Browse History
- Order BC: Customer = ID + Shipping Address + Payment Method + Credit Limit
If both systems share one Customer object, any change ripples through both:
- Cart adds “Shopping Preferences” field
- Order service forced to update Customer schema
- Payment service forced to update Customer schema
Result: Customer becomes a bloated mess, each service uses only 20% of its fields.
DDD’s solution: Each bounded context owns its Customer definition. They communicate through events, not shared databases.
Multiple Bounded Contexts with Independent Designs
graph LR
subgraph Cart["🛒 Shopping Cart BC<br/>Shopping Cart Context"]
C["Customer"]
C1["- CustomerID<br/>- Browse History<br/>- Shopping Preferences"]
C --> C1
end
subgraph Order["📦 Order BC<br/>Order Context"]
O["Customer"]
O1["- CustomerID<br/>- Shipping Address<br/>- Payment Method<br/>- Credit Limit"]
O --> O1
end
subgraph Payment["💳 Payment BC<br/>Payment Context"]
P["Customer"]
P1["- CustomerID<br/>- Bank Card<br/>- Risk Score"]
P --> P1
end
EventBus["📡 Event Bus<br/>Loose Coupling Communication"]
Cart -->|Publish Event<br/>CartCreated| EventBus
Order -->|Publish Event<br/>OrderPlaced| EventBus
Payment -->|Publish Event<br/>PaymentSucceeded| EventBus
EventBus -->|Subscribe| Cart
EventBus -->|Subscribe| Order
EventBus -->|Subscribe| Payment
style C fill:#c8e6c9
style O fill:#f3e5f5
style P fill:#fff3e0
4. Domain Event – Async Decoupling
What is a domain event?
When an order is paid, the system shouldn’t directly call “deduct inventory,” “generate invoice,” “update recommendations.” Instead, publish an event: “Order Paid.”
Each system subscribes to this event: – Inventory system: Hears “Order Paid” → Deducts stock – Billing system: Hears “Order Paid” → Generates invoice – Recommendation system: Hears “Order Paid” → Updates model
Benefit: Adding new features requires no Order logic changes. Just subscribe to the event.
Three Perspectives: Real-World Stories
Story 1: The Backend Engineer’s Trap and Escape
Character: Li Ming, Backend engineer with 2 years experience at an e-commerce platform
Act 1: The Trap
Q1 2024, management decides to support “VIP Discounts.” Li Ming starts thinking through the order workflow:
Order Processing Flow: 1. User places order → calls OrderService.createOrder() 2. OrderService validates stock → calls InventoryService.checkStock() 3. OrderService calculates price → calls PricingService.calculatePrice() 4. OrderService applies discount → calls DiscountService.applyDiscount() 5. OrderService validates credit → calls CustomerService.validateCredit()
For the new requirement, Li Ming adds VIP discount logic:
// Without DDDpublic class OrderService {
public Order createOrder(String customerId, List<Item> items) {
inventoryService.checkStock(items); Money basePrice = pricingService.calculatePrice(items); Money discountedPrice = discountService.applyDiscount( basePrice, customerId, getCurrentPromoCode() ); // NEW: VIP special discount if (customerService.isVIP(customerId)) { discountedPrice = discountedPrice.multiply(0.9); // Extra 10% off } customerService.validateCredit(customerId, discountedPrice); return orderRepository.save(new Order(...));}
}
Seems reasonable. But 3 months later, discount rules multiply:
- VIP discounts double during promotions
- New customers get extra first-purchase discount
- Bundle deals have special pricing
- Some categories have discount caps
OrderService balloons to 500 lines. Every discount rule change risks breaking other logic. And updating becomes terrifying.
Visual Comparison: Without DDD vs With DDD
graph TD
A["Requirement: Support VIP discount doubling"]
A --> B["Modify OrderService"]
A --> C["Modify DiscountService"]
A --> D["Modify PricingService"]
A --> E["Modify CustomerService"]
A --> F["Modify InventoryService"]
B --> B1["Adjust order calculation logic"]
C --> C1["Add VIP discount rules"]
D --> D1["Modify price calculation"]
E --> E1["Modify customer validation"]
F --> F1["Inventory check logic"]
B1 --> G["❌ Problem:"]
C1 --> G
D1 --> G
E1 --> G
F1 --> G
G --> H["• Fix one place, break another<br/>• Difficult to test all combinations<br/>• Each change is nerve-wracking<br/>• New requirements trigger more changes..."]
style A fill:#ffebee
style G fill:#ffcdd2
style H fill:#ff9800,color:#fff
Act 2: Awakening
Li Ming attends a DDD workshop. Realization: Why can’t Order manage its own discount logic?
With DDD:
// With DDDpublic class Order {
private OrderId orderId;
private CustomerId customerId;
private List<LineItem> lineItems;
private OrderDiscount discount;
private OrderStatus status;
private List<DomainEvent> events = new ArrayList<>(); // Aggregate manages business rules
public static Order create(CustomerId customerId, List<Item> items) {
Order order = new Order(OrderId.generate(), customerId); order.validateItems(items); // Order validates stock itself for (Item item : items) { order.addLineItem(item); } return order;} // Order decides how to apply discount
public void applyDiscount(DiscountCode code, Customer customer) {
if (!code.isValid()) { throw new InvalidDiscountException(); } if (customer.isVIP()) { this.discount = OrderDiscount.createVIPDiscount(code); } else if (customer.isNewCustomer()) { this.discount = OrderDiscount.createNewCustomerDiscount(code); } else { this.discount = OrderDiscount.create(code); } if (this.getTotalPrice().isNegative()) { throw new InvalidDiscountAmountException(); }} // Place order - ensure all rules satisfied
public void place() {
if (lineItems.isEmpty()) { throw new EmptyOrderException(); } if (!this.status.equals(OrderStatus.DRAFT)) { throw new InvalidOrderStatusException(); } this.status = OrderStatus.PLACED; this.events.add(new OrderPlacedEvent( this.orderId, this.customerId, this.getTotalPrice() ));} public Money getTotalPrice() {
Money basePrice = lineItems.stream() .map(LineItem::getPrice) .reduce(Money.ZERO, Money::add); if (discount != null) { return basePrice.minus(discount.getAmount()); } return basePrice;}
}
With DDD: Order Aggregate Self-Manages
graph TD A["Requirement: Support VIP discount doubling"] A --> B["Modify Order.applyDiscount()"] B --> C["Order aggregate validates<br/>VIP discount rules itself"] C --> D["✅ Problem Solved:"] D --> E["• Only one place to change<br/>• Complete testing within Order<br/>• Other services completely unaffected<br/>• Safe, controlled updates"] style A fill:#c8e6c9 style B fill:#81c784,color:#fff style D fill:#4caf50,color:#fff style E fill:#a5d6a7Now, discount rule changes only affect Order. Need VIP discounts? Update Order. Bundle discounts? Update Order. Other services? Untouched.
Plus, Order becomes documentation. New hires read Order and immediately understand “how orders work.”
Story 2: How Product Managers See the Difference
Character: Wang, E-commerce Product Manager
Without DDD: The Frustrating Conversation
Wang: “We need VIP customer special discounts.”
Li Ming (Engineer): “That’ll affect OrderService, DiscountService, PricingService, CustomerService. 2-3 weeks.”
Wang: “But it’s just discounts! Why 4 services?”
Li Ming: “Because discount logic is scattered…”
Wang: Gives up understanding. Trusts the estimate. Worries about breaking things when updating.
With DDD: The Clear Conversation
Wang: “We need VIP customer special discounts.”
Li Ming: “That logic lives entirely in Order aggregate. Update Order class only. 5 working days.”
Wang: “Why just one place?”
Li Ming: “Order aggregate owns all order business rules, including discounts. Changes only affect Order, nothing else.”
Wang: Immediately confident and understands boundaries.
Another example: New customers get 15% extra off on first purchase
Wang: “Add 15% extra discount for first-time customers.”
Li Ming: “Add Customer.isNewCustomer() check in Order.applyDiscount(). 3 days.”
Wang: “What if we break something?”
Li Ming: “Order has comprehensive unit tests. All discount combinations are covered. Tests fail if we break things, deployment stops.”
Wang sees Order’s tests:
@Testvoid newCustomersShouldGetExtraFirstPurchaseDiscount() {
Order order = Order.create(customerId, items);
Customer newCustomer = new Customer(
customerId, CustomerType.NEW);
order.applyDiscount(promoCode, newCustomer);
assertEquals(expectedDiscountAmount, order.getDiscount().getAmount());
} @Test
void vipCustomersShouldGetHighestDiscount() {
Order order = Order.create(customerId, items);
Customer vipCustomer = new Customer(
customerId, CustomerType.VIP);
order.applyDiscount(promoCode, vipCustomer);
assertEquals(
expectedVIPDiscountAmount, order.getDiscount().getAmount());
}
Wang’s confidence soars. He sees complete business logic definition with full test coverage.
Impact Assessment Complexity Comparison
graph TD Req["\"Add 15% extra discount for first-time customers\""] subgraph NoDD ["❌ Without DDD"] N1["Assess impact:"] N2["Need to change OrderService?"] N3["Need to change CustomerService?"] N4["Need to change DiscountService?"] N5["Need to change PricingService?"] N6["...Need to change anything else?"] N1 --> N2 --> N3 --> N4 --> N5 --> N6 Result1["😕 Wang: Why is this<br/>so complicated?"] N6 --> Result1 end subgraph DD ["✅ With DDD"] D1["Assess impact:"] D2["Order aggregate's<br/>applyDiscount() method"] D3["Check customer type<br/>isNewCustomer?"] D4["Apply 15% discount rule"] D1 --> D2 --> D3 --> D4 Result2["😊 Wang: Got it!<br/>Just change Order in one place"] D4 --> Result2 end Req --> NoDD Req --> DD style Result1 fill:#ffcdd2,color:#c62828 style Result2 fill:#c8e6c9,color:#2e7d32
Story 3: The Architect’s Microservice Journey
Character: Zhang, System Architect managing single-to-microservices migration
Without DDD: The Microservices Nightmare
Company decides to split into microservices. Zhang’s plan:
- Shopping Cart Microservice
- Order Microservice
- Payment Microservice
- Recommendation Microservice
Problem: All four need Customer objects. What’s Customer?
- Cart needs: User ID, Shopping Preferences, Browse History
- Order needs: User ID, Shipping Address, Payment Method
- Payment needs: User ID, Credit Card, Risk Score
- Recommendation needs: User ID, Browse History, Purchase History
If all share one Customer object, everything breaks:
- Cart adds Shopping Preferences
- Order forced to update Customer
- Payment forced to update Customer
Customer becomes bloated, each service uses 20% of fields, maintains 100% of cruft.
With DDD: Clean Microservices
Zhang reframes using bounded contexts:
Cart BC: Cart (aggregate root)
- CartItem
- CustomerRef (ID only)
CartPreference
Order BC: Order (aggregate root) - OrderLineItem - ShippingAddress - BillingInfo - CustomerRef (ID only)
Payment BC: Payment (aggregate root) - PaymentMethod - RiskScore - CustomerRef (ID only)
Recommendation BC: RecommendationProfile (aggregate root) - BrowsingHistory - PurchaseHistory - CustomerRef (ID only)
Each BC owns its Customer concept. No shared databases; they communicate via events:
Cart BC publishes: – “CartCreated” → Recommendation BC updates history
Order BC publishes: – “OrderPlaced” → Inventory BC deducts stock – “OrderPlaced” → Billing BC generates invoice – “OrderPlaced” → Recommendation BC updates model
Payment BC publishes: – “PaymentSucceeded” → Order BC updates status – “PaymentFailed” → Order BC marks failure
Results:
- Independent evolution: Cart wants new fields? Only Cart changes. 2. Clear communication: Events, not API calls. Loose coupling. 3. Failure isolation: Recommendation down? Order flow unaffected. 4. Simple extension: Add points system? New BC, subscribe to events. No core changes.
Evolution From Monolith to Microservices
graph TD
A["Problem: How to separate into<br/>multiple microservices?"]
subgraph BadWay ["❌ Wrong approach: Shared Customer model"]
B1["All services share<br/>one Customer table"]
B2["Cart adds field<br/>↓ All services update"]
B3["Order adds field<br/>↓ All services update"]
B4["...Each service uses<br/>only 20% of fields<br/>but maintains 100% of complexity"]
B1 --> B2 --> B3 --> B4
end
subgraph GoodWay ["✅ DDD approach: Separate Bounded Contexts"]
G1["Cart BC owns<br/>its own Customer"]
G2["Order BC owns<br/>its own Customer"]
G3["Payment BC owns<br/>its own Customer"]
G1 -.->|Event| EventBus["📡<br/>Event<br/>Bus"]
G2 -.->|Event| EventBus
G3 -.->|Event| EventBus
EventBus -.->|Subscribe| G1
EventBus -.->|Subscribe| G2
EventBus -.->|Subscribe| G3
end
A --> BadWay
A --> GoodWay
BadWay --> Result1["😞 Monolith Hell<br/>Can't change"]
GoodWay --> Result2["😊 Microservices Heaven<br/>Independent evolution"]
style BadWay fill:#ffebee
style GoodWay fill:#e8f5e9
style Result1 fill:#ff5252,color:#fff
style Result2 fill:#4caf50,color:#fff
Common Misconceptions
Misconception 1: DDD = Microservices
Wrong: DDD requires microservices.
Truth: DDD is a design philosophy applicable to both monoliths and microservices. A well-designed monolith using DDD beats poorly-designed microservices.
Example: A small company with just Order and Customer doesn’t need microservices. DDD monolith is simpler.
Misconception 2: DDD = Complex Framework
Wrong: DDD needs special frameworks.
Truth: DDD is pure design thinking. Order, LineItem, DiscountCode are plain Java classes. Frameworks assist, not define DDD.
Misconception 3: Every Project Needs DDD
Wrong: DDD is a silver bullet.
Truth: DDD shines with complex business logic. Simple CRUD APIs don’t benefit.
Is DDD right for you?
- Complex business logic (50+ rules/use cases) → Yes
- Multiple teams developing → Yes
- Frequent business changes → Yes
- Simple CRUD API → No
- No long-term maintenance → No
Misconception 4: DDD = Over-Engineering
Wrong: DDD makes systems complex.
Truth: DDD’s goal is simplifying complex systems. Yes, DDD requires upfront thought, but it reduces future complexity.
Comparison:
Without DDD: 100-line simple Service → 6 months later: 2000-line monster With DDD: 200-line design + structure → 6 months later: Still clear, easy to modify
Practical Guide: Getting Started
Step 1: Identify Bounded Contexts (Don’t Code Yet)
Spend 30 minutes with business people listing:
- All business roles: Customer, Support, Warehouse, Finance… 2. All main processes: Place Order, Payment, Returns, Recommendations… 3. How concepts change: What’s “Customer” in order flow? In recommendations?
Example: – Order BC: Customer = Shipping Address + Payment Method – Recommendation BC: Customer = Browse History + Preferences
Step 2: Define Ubiquitous Language
Write a short glossary ensuring everyone speaks the same language:
Order: Customer's purchase record with items, quantity, discounts, payment
DiscountCode: Promotion applicable under specific conditions
Aggregate: Boundary's guardian enforcing all business rules
Step 3: Design Core Aggregates
Start with your most complex flow. For orders, Order is the aggregate containing:
- OrderLineItem (child)
- OrderDiscount (child)
- All validation rules
Step 4: Test-First, Then Code
@Testvoid orderShouldValidateDiscountCodeValidity() {
Order order = Order.create(customerId, items);
InvalidDiscountCode code = new InvalidDiscountCode();
assertThrows(
InvalidDiscountException.class, () -> order.applyDiscount(code, customer));
} @Test
void vipCustomersShouldGetExtraDiscount() {
Order order = Order.create(customerId, items);
DiscountCode code = new DiscountCode("VIP2024");
Customer vipCustomer = new Customer(
customerId, CustomerType.VIP);
order.applyDiscount(code, vipCustomer);
Money expectedDiscount = basePrice.multiply(0.2);
// 20% off
assertEquals(
expectedDiscount, order.getDiscount().getAmount());
}
With clear tests, business logic crystallizes. Then implement Order to pass.
Complete Workflow Example
Order Processing DDD Flow
sequenceDiagram actor User as User participant OrderBC as Order BC<br/>Aggregate Root participant InventoryBC as Inventory BC participant BillingBC as Billing BC participant RecommendBC as Recommend BC User->>OrderBC: Place order<br/>Order.create() activate OrderBC OrderBC->>OrderBC: ✅ Validate stock<br/>✅ Validate discount<br/>✅ Validate credit OrderBC->>OrderBC: Publish event<br/>OrderPlaced deactivate OrderBC Note over OrderBC: Order as complete<br/>business rule guardian<br/>All validation here OrderBC->>InventoryBC: OrderPlaced event OrderBC->>BillingBC: OrderPlaced event OrderBC->>RecommendBC: OrderPlaced event par Parallel async processing activate InventoryBC InventoryBC->>InventoryBC: Listen OrderPlaced<br/>Async deduct stock InventoryBC-->>User: Stock deducted deactivate InventoryBC and activate BillingBC BillingBC->>BillingBC: Listen OrderPlaced<br/>Async generate invoice BillingBC-->>User: Invoice generated deactivate BillingBC and activate RecommendBC RecommendBC->>RecommendBC: Listen OrderPlaced<br/>Async update recommendations RecommendBC-->>User: Recommendations updated deactivate RecommendBC endDDD’s Core Value
graph LR subgraph Before ["Without DDD<br/>Scattered Logic"] B1["Service A"] B2["Service B"] B3["Service C"] B4["Service D"] B5["Service E"] B_logic["❌ Business rules<br/>scattered everywhere"] B1 --- B2 B2 --- B3 B3 --- B4 B4 --- B5 end subgraph After ["With DDD<br/>Centralized Rules"] A1["Aggregate Root<br/>🏛️"] A_logic["✅ Business rules<br/>centrally managed"] A2["Other services"] end Before -->|Introduce DDD| After style B_logic fill:#ff5252,color:#fff style A_logic fill:#4caf50,color:#fff
Conclusion
DDD isn’t a silver bullet, but for complex business systems, it’s the right medicine. Its core promise: through clear boundaries and well-defined roles, transform complexity into simplicity.
Key takeaways:
- Aggregates centralize logic: Changes don’t ripple through the system 2. Bounded contexts clarify ownership: Different domains own their concepts 3. Domain events enable loose coupling: New features need no core changes 4. Ubiquitous language unifies communication: Everyone speaks the same language
Your next step: Pick your most complex flow, redesign it with DDD. You’ll discover scattered logic becomes clear, changes become safe.