Why Traditional Service Layers Fail and How DDD Restores Order
The article shows how a typical three‑layer OrderService accumulates scattered business rules, mixes technical details with domain logic, lacks clear boundaries, and diverges from business language, then explains how Domain‑Driven Design reorganizes code around aggregates and bounded contexts to solve these problems while outlining when DDD is unnecessary.
Problem: Traditional three‑layer OrderService code
Typical three‑layer implementation (Controller → Service → DAO) that can run but deteriorates as business grows:
@Service
public class OrderService {
@Autowired private ProductMapper productMapper;
@Autowired private StockMapper stockMapper;
@Autowired private MemberMapper memberMapper;
@Autowired private CouponMapper couponMapper;
@Autowired private OrderMapper orderMapper;
@Transactional
public Long placeOrder(PlaceOrderRequest req) {
// 1. Query product and calculate price
BigDecimal total = BigDecimal.ZERO;
for (OrderItemReq item : req.getItems()) {
Product p = productMapper.selectById(item.getProductId());
if (p == null || p.getStatus() != 1) {
throw new RuntimeException("商品不可售");
}
total = total.add(p.getPrice().multiply(new BigDecimal(item.getQty())));
}
// 2. Apply coupon
if (req.getCouponId() != null) {
Coupon c = couponMapper.selectById(req.getCouponId());
if (c.getThreshold().compareTo(total) <= 0) {
total = total.subtract(c.getAmount());
}
}
// 3. Deduct stock
for (OrderItemReq item : req.getItems()) {
int rows = stockMapper.deduct(item.getProductId(), item.getQty());
if (rows == 0) throw new RuntimeException("库存不足");
}
// 4. Add member points
Member m = memberMapper.selectById(req.getMemberId());
m.setPoints(m.getPoints() + total.intValue());
memberMapper.updateById(m);
// 5. Persist order
Order order = new Order();
order.setMemberId(req.getMemberId());
order.setTotalAmount(total);
order.setStatus(0);
orderMapper.insert(order);
return order.getId();
}
}Issues observed:
Business logic (product eligibility, discount calculation, stock deduction, point accrual) is scattered throughout a single method; domain objects are mere data bags (anemic model).
Duplicate "calculate total" logic appears in shopping‑cart, confirmation page, and order API, leading to missed updates when rules change.
Technical details (e.g., productMapper.selectById) are mixed with business rules, causing coupling between ORM changes and business code.
All concerns (order, stock, member, promotion) are handled in one transaction and one Service, resulting in a large method with many Mapper dependencies (tight coupling to five tables), described as a "big mudball".
Business language does not match code; e.g., "freeze coupon after order" becomes couponMapper.updateStatus(id, 2) with an opaque status value.
Different concerns mixed together → domain logic hard to spot → cohesion scattered, coupling tangled → changing business hurts technology and vice‑versa → maintenance becomes chaotic, duplication and inconsistency grow.
Why DDD can help
Core idea: Organize code around the business domain, not around database tables or technical frameworks.
Strategic design : Slice the large system into bounded contexts (order, inventory, promotion, member). Each context has its own model and boundary, so changes in promotion do not affect points code.
Tactical design : Within each context, place business rules inside aggregates. The Order aggregate now owns rules such as order.place() and order.cancel(), and Member encapsulates point logic.
DDD‑refactored service
@Service
public class PlaceOrderAppService {
private final ProductRepository productRepo;
private final OrderRepository orderRepo;
private final DomainEventPublisher eventPublisher;
@Transactional
public OrderId placeOrder(PlaceOrderCommand cmd) {
// Application layer only orchestrates; business rules live in the domain.
List<OrderLine> lines = cmd.toOrderLines(productRepo); // price lookup & validation
Order order = Order.place(cmd.memberId(), lines, cmd.coupon()); // rules inside aggregate
orderRepo.save(order);
eventPublisher.publish(order.domainEvents()); // async stock/points handling
return order.getId();
}
}Comparison of dimensions
Where business rules live : scattered in Service vs cohesive inside domain objects (aggregates).
Who guards the rules : programmer discipline vs aggregate root invariants.
Boundaries : none, fully coupled vs physical isolation via bounded contexts.
Will a promotion change affect points? : likely vs will not, due to isolation.
Code readability : SQL‑like script vs business‑centric calls such as order.place() that map directly to the ubiquitous language.
When not to use DDD (avoid over‑design)
Simple CRUD, admin panels, data reports where business logic is limited to create‑read‑update‑delete.
Projects with extremely simple logic where DDD adds unnecessary complexity.
Complex, long‑living, high‑value core business systems (e.g., order processing, promotion engines) are the appropriate targets.
Assessing whether a module really needs DDD
Score a real module against the following checklist. The more signals hit, the more DDD makes sense.
Business rules : stateful flows, composite rules, exception handling vs only field CRUD.
Change frequency : frequent, risky changes vs stable, rarely changed.
Business value : core revenue/experience/risk control vs back‑office configuration or report export.
Collaboration size : multiple product, business, and development teams vs a single‑person tool.
Current pain points : long Service, duplicated rules, frequent bugs vs simple, straightforward code.
Shell commands for a quick health check
# Find overly long Service classes – inspect for piled‑up business rules
find src/main/java -name "*Service.java" -print0 | xargs -0 wc -l | sort -nr | head
# Search for magic status values and scattered status checks
grep -R "status.*==" src/main/java
grep -R "setStatus" src/main/java
# Count how many Mapper/Repository dependencies a Service has – more dependencies often indicate a "big mudball"
grep -R "@Autowired.*Mapper\|private final .*Mapper" src/main/javaThese commands only highlight "suspicious points"; final judgment must consider the business context: is the module complex, valuable, and evolving?
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Yumin Fish Harvest
A deep‑sea salvage fisherman sharing architecture insights, practical tips, and lessons learned.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
