Applying Domain‑Driven Design in a Java Backend Project: Layered Architecture and Practical Example
This article explains how to apply Domain‑Driven Design in a Java backend project, detailing the responsibilities of each layer—domain, infrastructure, application, adapter, client, and start—along with practical code examples for a work‑order system.
1. Introduction
Domain‑Driven Design (DDD) has become a hot topic in the backend community, often mentioned whenever architecture is discussed. The author, a senior developer, shares personal experiences of understanding the theory but struggling with its practical implementation.
After several DDD projects, the author summarizes his frontline understanding of DDD to help readers, noting that DDD is not a one‑size‑fits‑all set of rules; each project may adapt the concepts slightly.
2. Project Layering
When starting a DDD project, define module responsibilities and Maven POM dependencies. The author’s company uses the Alibaba‑Cola Maven skeleton, which provides a typical four‑layer guide.
The following sections explain each layer in detail and what kind of code belongs there.
2.1 Domain Layer (Core)
The domain layer focuses on the business core. Early small projects often create an entities package containing simple POJOs with only fields and getters/setters, which leads to anemic models lacking behavior.
Classes contain only attributes and simple getters/setters.
The package holds constants or simple classes, no interfaces, and no behavior.
Such a package contradicts object‑oriented design because it stores many data‑only objects.
In DDD, the domain layer is the most important. Proper domain modeling directly impacts system maintainability. Concepts such as event storming, story mapping, use‑case analysis, and strategic‑tactical design are useful but require extensive practice.
Key elements of the domain layer include:
Events – e.g., a "place order" event that triggers inventory locking.
Entities and Value Objects – Value Objects are immutable and interchangeable. Entities have an ID, lifecycle, and status (e.g., order status).
Aggregates – groups of entities and value objects with behavior.
Factories – create complex aggregates.
Service Interfaces – stateless domain services.
Repository Interfaces – define persistence contracts, implemented in the infrastructure layer.
Domain Access Interfaces – decouple inter‑domain communication via gateways.
Summary of domain layer characteristics:
No dependency on other modules.
Business logic is independent of infrastructure (e.g., database choice).
Classes contain behavior, not just data.
Package organization is recommended as shown in the right‑hand diagram to keep the domain isolated.
2.2 Infrastructure Layer
The infrastructure layer interacts with middleware. Ideally, swapping technologies (e.g., RocketMQ → Kafka, MySQL → Oracle) only requires changes in this layer, leaving other layers untouched.
Typical contents:
Data access code (MyBatis mappers, XML files).
Utility classes.
Implementations of domain repositories, services, and gateways.
Model conversion utilities (MapStruct, etc.).
Spring configuration classes.
2.3 Application Layer (Business Layer)
The application layer assembles input, creates context, and invokes domain services. It may also send notifications. Although it can bypass the domain layer, it usually coordinates domain and infrastructure calls.
Implementation of client‑side interfaces using domain or infrastructure capabilities.
Implementation of controller or RPC interfaces.
2.4 Adapter Layer
The Adapter Layer handles routing and adaptation for front‑end presentations (web, mobile, etc.). It is similar to MVC controllers and depends only on the application layer.
Controller interfaces.
Global exception handlers.
Parameter validation annotations.
Interceptors, filters.
Scheduled tasks.
Custom aspects.
2.5 Client Layer
The client layer is optional and provides a lightweight JAR for RPC frameworks (e.g., Dubbo). It contains only interfaces and DTOs; implementations reside in the application layer.
Request/response DTOs.
RPC interface definitions.
Shared enums.
Error codes for service consumers.
2.6 Start Module
The start module contains the Spring Boot entry point and configuration files. It depends on all other modules, making it suitable for packaging and for writing integration tests.
Highlights the location of the main class.
Acts as the Maven build entry point.
Hosts global configuration annotations such as @EnableXXX and @ComponentScan .
3. Practical Example – Work Order System
The author demonstrates a work‑order management system to illustrate the concepts.
A client can submit a work order, which goes through processing, possible rejection, and final confirmation.
Key behaviors: create order, process order, reject order.
Domain model (code omitted for brevity) includes the WorkOrder entity, value objects for content and status, and methods for starting, handling, finishing, and rejecting an order.
@Data
public class WorkOrder {
/** Work order unique ID */
private String workOrderId;
/** Parent order ID */
private String parentOrderId;
/** Initiator ID */
private String initiatorId;
private Date startTime;
private Date processTime;
private Date completeTime;
private Date rejectTime;
/** Content value object */
private WorkOrderContent content;
/** Status enum value object */
private WorkOrderStatus workOrderStatus;
/** Result value object */
private WorkOrderHandleResult workOrderHandleResult;
// Constructor, factory method, handle, finish, reject ...
}Service interface example:
@Service
public class WorkOrderServiceImpl implements WorkOrderService {
@Autowired
WorkOrderRepository workOrderRepository;
@Autowired
WorkOrderEventRepository workOrderEventRepository;
@Override
public WorkOrder startOrder(String userId, WorkOrderContent workOrderContent) {
WorkOrder order = WorkOrder.start(userId, workOrderContent);
workOrderRepository.save(order);
workOrderEventRepository.sendWorkOrderEvent(new WorkOrderEvent(order.getWorkOrderId(), order.getWorkOrderStatus().getValue()));
return order;
}
@Override
public void processOrder(WorkOrderHandleResult result, String handlerId, String workOrderId) {
WorkOrder workOrder = workOrderRepository.get(workOrderId);
workOrder.handle();
workOrderEventRepository.sendWorkOrderEvent(new WorkOrderEvent(workOrder.getWorkOrderId(), workOrder.getWorkOrderStatus().getValue()));
workOrderRepository.save(workOrder);
}
}Repository interfaces defined in the domain module and implemented in the infrastructure layer:
public interface WorkOrderRepository {
void save(WorkOrder workOrder);
WorkOrder get(String workOrderId);
}
public interface WorkOrderEventRepository {
void sendWorkOrderEvent(WorkOrderEvent workOrderEvent);
}Conclusion
The article shares practical DDD experience for developers who need to adopt DDD in real projects. It emphasizes understanding where to place classes to avoid architectural decay and suggests that DDD’s value lies in achieving high cohesion and low coupling.
While DDD is beneficial, it may be overkill for small projects, and not every DDD concept must be applied. The key is to use a layered structure that decouples code, improves readability, and fits the project’s complexity.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.