Practical Guide to Implementing Domain-Driven Design (DDD) in a Multi‑Layered Backend Project
This article shares hands‑on experience of applying Domain‑Driven Design in real projects, detailing a typical four‑layer architecture (domain, infrastructure, application, adapter), code examples, packaging guidelines, and practical tips for clean, maintainable backend systems.
The author reflects on the hype around DDD in the backend community and explains that DDD is not a one‑size‑fits‑all rule; implementation varies per project. After several DDD projects, the author summarizes key understandings to help developers.
1. Introduction
Building a DDD project starts with defining module responsibilities and Maven POM dependencies, often using Alibaba‑Cola's Maven skeleton as a quick start. Two diagrams from the Cola website illustrate a typical DDD project layering.
2. Project Layering
2.1 Domain Layer (Core)
The domain layer focuses on the business core and should contain entities, value objects, aggregates, factories, service interfaces, storage interfaces, and domain access interfaces. It must be independent of other modules and avoid the anemic model.
Events – domain events for actions like order creation.
Entities and Value Objects – immutable value objects and entities with ID, lifecycle, and status.
Aggregates – composed of entities and value objects with behavior.
Factories – simplify creation of complex aggregates.
Service Interfaces – stateless domain services.
Storage Interfaces – define persistence contracts, implemented in the infrastructure layer.
Domain Access Interfaces – decouple domain-to-domain data access via gateways.
2.2 Infrastructure Layer
This layer deals with technical concerns (e.g., MyBatis mappers, utilities, repository implementations, model conversion, Spring configuration). It implements the interfaces defined in the domain layer, allowing the domain to remain technology‑agnostic.
2.3 Application Layer
The application layer orchestrates use‑cases: it receives input, assembles context, invokes domain services, and may send notifications. It depends on both domain and infrastructure layers.
2.4 Adapter Layer
Adapter (or controller) layer handles routing and adaptation for front‑end clients (web, mobile, etc.). It should only depend on the application layer and mainly delegates to it.
2.5 Client Layer
Optional layer that packages a lightweight JAR exposing SDK interfaces and DTOs for RPC frameworks like Dubbo. It remains independent of other modules.
2.6 Start Module
Contains the Spring Boot entry point and configuration, aggregates all other modules, and is suitable for packaging and unit testing.
3. Example: Work Order Management
A concrete work‑order scenario demonstrates extracting entities, value objects, and behaviors, followed by code snippets.
@Data
public class WorkOrder {
private String workOrderId;
private String parentOrderId;
private String initiatorId;
private Date startTime;
private Date processTime;
private Date completeTime;
private Date rejectTime;
private WorkOrderContent content; // value object
private WorkOrderStatus workOrderStatus; // enum value object
private WorkOrderHandleResult workOrderHandleResult; // value object
private WorkOrder(Date startTime, String workOrderId, String initiatorId, WorkOrderContent content, WorkOrderStatus workOrderStatus, WorkOrderHandleResult workOrderHandleResult) {
this.workOrderId = workOrderId;
this.initiatorId = initiatorId;
this.content = content;
this.workOrderStatus = workOrderStatus;
this.workOrderHandleResult = workOrderHandleResult;
this.startTime = startTime;
}
public static WorkOrder start(String initiatorId, WorkOrderContent content) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMddHHmmssSSS");
String id = "WorkOrderID" + fmt.format(LocalDateTime.now()) + new DecimalFormat("000").format(new SecureRandom().nextInt(999));
WorkOrderStatus status = WorkOrderStatus.INITIAL;
return new WorkOrder(new Date(), id, initiatorId, content, status, null);
}
public void handle() {
this.workOrderStatus = WorkOrderStatus.HANDLEING;
this.processTime = new Date();
}
public void finish(WorkOrderHandleResult result) {
this.completeTime = new Date();
this.setWorkOrderHandleResult(result);
this.workOrderStatus = WorkOrderStatus.COMPLETED;
}
public WorkOrder reject() {
if (!workOrderStatus.equals(WorkOrderStatus.COMPLETED)) {
return null;
}
String parentId = workOrderId;
Date start = getStartTime();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMddHHmmssSSS");
String newId = "WorkOrderID" + fmt.format(LocalDateTime.now()) + new DecimalFormat("000").format(new SecureRandom().nextInt(999));
WorkOrder wo = new WorkOrder(start, workOrderId, initiatorId, content, WorkOrderStatus.HANDLEING, null);
wo.setParentOrderId(parentId);
wo.setRejectTime(new Date());
return wo;
}
}Service interface implementation:
@Service
public class WorkOrderServiceImpl implements WorkOrderService {
@Autowired
WorkOrderRepository workOrderRepository;
@Autowired
WorkOrderEventRepository workOrderEventRepository;
@Override
public WorkOrder startOrder(String userId, WorkOrderContent content) {
WorkOrder order = WorkOrder.start(userId, content);
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 wo = workOrderRepository.get(workOrderId);
wo.handle();
workOrderEventRepository.sendWorkOrderEvent(new WorkOrderEvent(wo.getWorkOrderId(), wo.getWorkOrderStatus().getValue()));
workOrderRepository.save(wo);
}
}Domain‑level repository contracts (implemented in the infrastructure layer):
public interface WorkOrderRepository {
void save(WorkOrder workOrder);
WorkOrder get(String workOrderId);
}
public interface WorkOrderEventRepository {
void sendWorkOrderEvent(WorkOrderEvent event);
}Conclusion
The domain layer is the most critical; proper domain modeling determines maintainability. While DDD brings high cohesion and low coupling, it may be overkill for small projects. The article emphasizes pragmatic layering, clear package boundaries, and adapting DDD concepts to real‑world needs.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
