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.

Architect
Architect
Architect
Practical Guide to Implementing Domain-Driven Design (DDD) in a Multi‑Layered Backend Project

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.

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.

JavaDomain-Driven DesignDDDsoftware design
Architect
Written by

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.

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.