How to Layer Code with DDD: Four‑Layer Architecture, DIP & Hexagonal
This article explains why and how to separate a system into four DDD layers—User Interface, Application, Domain, and Infrastructure—illustrates the benefits of clear responsibilities, demonstrates dependency inversion and hexagonal architecture with concrete Spring Boot examples, and warns of common pitfalls.
Why Layer (Escape the "Big Mudball")
Mixing business logic, technical details, and UI code in a single Service layer creates tangled dependencies: changing business code breaks technology code and vice‑versa. Layering separates concerns so that business rules, database handling, and presentation remain independent.
Business logic can be identified, tested, and evolved in isolation.
Technology changes (database, web framework) do not affect core business.
Package names immediately reveal where code belongs, easing onboarding.
DDD Four‑Layer Architecture
Call flow: request → UI layer → Application layer (orchestration) → Domain layer (business rules) → Infrastructure layer (DB, MQ, third‑party).
Dependency direction: the Domain layer does not depend on databases, message queues, or SDKs; the Infrastructure layer implements interfaces defined by the Domain layer, achieving dependency inversion.
Layer Responsibilities (Order‑placement example)
User Interface Layer : receives HTTP request, validates parameters, assembles response. Example class OrderController converts a request to a PlaceOrderCommand.
Application Layer : orchestrates workflow (fetch data → invoke domain → persist → publish events) and manages transactions. No business rules are written here. Example method PlaceOrderAppService.placeOrder().
Domain Layer : contains all business rules such as pricing, validation, and state transitions. Example calls: Order.place(), PromotionCalculator.
Infrastructure Layer : provides technical implementations (JPA repository, payment‑gateway adapter, Redis cache, etc.). Example classes: JpaOrderRepository, WxPaymentGatewayAdapter.
// User Interface Layer
@RestController
@RequestMapping("/orders")
public class OrderController {
private final PlaceOrderAppService placeOrderAppService;
@PostMapping
public OrderVO placeOrder(@RequestBody @Valid PlaceOrderRequest req) {
OrderId id = placeOrderAppService.placeOrder(req.toCommand()); // conversion only
return new OrderVO(id.value());
}
}
// Application Layer (only orchestration, no business logic)
@Service
public class PlaceOrderAppService {
private final ProductRepository productRepo;
private final OrderRepository orderRepo;
private final ApplicationEventPublisher publisher;
@Transactional
public OrderId placeOrder(PlaceOrderCommand cmd) {
List<OrderLine> lines = buildLines(cmd, productRepo); // fetch data
Order order = Order.place(cmd.memberId(), lines, cmd.address()); // invoke domain
orderRepo.save(order); // persist
order.domainEvents().forEach(publisher::publishEvent); // publish events
return order.getId();
}
}Dependency Inversion (DIP)
Traditional three‑layer architecture has a downward dependency ( Service → DAO), causing domain logic to rely on volatile technical details. Changing the database forces changes in business code.
DDD flips this: the Domain layer defines interfaces such as OrderRepository; the Infrastructure layer implements them ( JpaOrderRepository). The dependency arrow points upward, protecting the stable core.
Domain Layer (defines OrderRepository) ★ stable core
↑ implementation (arrow points up)
Infrastructure Layer (JpaOrderRepository implements the interface)The Domain layer has no technology dependencies, making it pure Java and easily mockable.
Technical details (DB, MQ, third‑party SDKs) become plug‑in adapters that can be swapped without affecting business rules.
Spring DI injects the concrete JpaOrderRepository into the Application layer at runtime.
Hexagonal Architecture (Ports & Adapters)
Hexagonal architecture visualizes the same principle: the core (Domain + Application) sits in the center, unaware of external concerns. External actors (HTTP, DB, MQ, third‑party services) interact through ports (interfaces) and adapters (concrete implementations).
External World (HTTP, DB, MQ, third‑party)
│
┌─────────────┼─────────────┐
│ Adapter (Infrastructure) │
│ ┌─────────────────────┐ │
│ │ Port (Interface) │ │
│ │ ┌───────────────┐ │ │
│ │ │ Domain + App │ │ │ ← Core, pure business, no knowledge of outside
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
└─────────────────────────────┘Core : domain + application, stable business logic.
Port : interfaces defined by the core; two kinds—driving ports (called by outside, e.g., application service) and driven ports (capabilities the core needs, e.g., OrderRepository, PaymentGateway).
Adapter : concrete implementations such as HTTP controllers, JPA repositories, payment adapters.
Comparison with Traditional Three‑Layer
Business logic location : traditional – all in Service; DDD – in Domain layer.
Dependency direction : traditional – Service depends on DAO (technology); DDD – Infrastructure depends on Domain (dependency inversion).
Testability of core : traditional – cannot test without DB; DDD – pure Java, mock interfaces.
Impact of DB change : traditional – business affected; DDD – business unchanged.
Application layer role : traditional – both orchestration and business, bloated; DDD – orchestration only, clean.
Verifying the Layering Has Not Degenerated
Command health‑check
# Domain layer should not depend on infrastructure tech
grep -R "import .*\(jpa\|mybatis\|redis\|kafka\|rocketmq\)" src/main/java/com/youxuan/store/*/domain
# Application layer should not contain business threshold checks
grep -R "if (.*amount\|if (.*status\|if (.*total" src/main/java/com/youxuan/store/*/application
# Infrastructure layer should implement domain interfaces
grep -R "implements .*Repository" src/main/java/com/youxuan/store/*/infrastructureNew‑comer mnemonic
@PostMapping, @RequestBody → User Interface layer @Transactional, "fetch then save" → Application layer if (status == PAID) throw … → Domain layer JpaRepository, Mapper, KafkaTemplate → Infrastructure layer
Common Pitfalls
Business logic leaking into Application layer : writing if business checks inside services violates the "application only orchestrates" rule.
Domain layer importing infrastructure classes : importing JpaRepository, RedisTemplate, or similar ties the domain to technology and should be avoided.
Over‑layering : splitting a small project into many sub‑modules creates unnecessary complexity; layering should serve clarity, not sheer depth.
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.
