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.

Yumin Fish Harvest
Yumin Fish Harvest
Yumin Fish Harvest
Why Traditional Service Layers Fail and How DDD Restores Order

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/java
These commands only highlight "suspicious points"; final judgment must consider the business context: is the module complex, valuable, and evolving?
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSoftware ArchitectureDomain-Driven DesignDDDService Layer
Yumin Fish Harvest
Written by

Yumin Fish Harvest

A deep‑sea salvage fisherman sharing architecture insights, practical tips, and lessons learned.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.