How DDD and Hexagonal Architecture Revamp Huolala’s CRM System
This article explores how Domain‑Driven Design and hexagonal (clean) architecture were applied to Huolala’s user CRM, detailing the tactical design patterns, module breakdown, code examples, and the step‑by‑step migration that improved scalability, maintainability, and development efficiency.
Introduction
In the fast‑growing internet industry, business needs and technical architecture are becoming increasingly intertwined. Maintaining system stability, flexibility, and scalability in a complex, ever‑changing environment is a major challenge for technical teams.
This article dives into the application of Domain‑Driven Design (DDD) in Huolala’s user CRM system, sharing architectural patterns, key technical points, and practical implementation experiences.
What is DDD?
DDD provides both strategic and tactical design. Strategic design guides business domain partitioning, while tactical design extracts the technical model from the business model, making code closely aligned with business concepts.
Key tactical concepts include:
Strategic design: split the domain into sub‑domains and define bounded contexts.
Tactical design: implement the problem space in the solution space using aggregates, entities, value objects, and repositories.
Hexagonal (Clean) Architecture Evolution
Hexagonal architecture (also known as clean or onion architecture) applies the Dependency Inversion Principle to flatten traditional layered structures, allowing independent replacement of infrastructure components such as databases, RPC, or messaging systems.
Benefits include:
Clear model boundaries improve maintainability and extensibility.
Business logic resides in the domain layer, isolated from technical details.
Infrastructure can be swapped without affecting core business code.
Practical Coding Example: Order Service
The following example demonstrates a typical e‑commerce order flow using DDD tactical design and hexagonal architecture.
Module Overview
ddd-demo/
├── ddd-order-demo-api # UI layer (REST controllers)
│ └── src/main/java/cn/huolala/demo/api/controller/OrderController.java
├── ddd-order-demo-application # Application service layer
│ └── src/main/java/cn/huolala/demo/application/service/OrderService.java
├── ddd-order-demo-domain # Domain layer (models, services, interfaces)
│ └── src/main/java/cn/huolala/demo/domain/model/order/Order.java
├── ddd-order-demo-infra-repository # Infrastructure: repositories (MySQL, Redis, ES)
│ └── src/main/java/cn/huolala/demo/infra/repository/OrderRepository.java
└── ddd-order-demo-infra-remote # Infrastructure: remote calls (OpenFeign)
└── src/main/java/cn/huolala/demo/infra/remote/ProductRemoteService.javaController (UI Layer)
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/create")
public OrderResponse create(OrderCreateCmd cmd) {
return OrderConvertor.convertToResponse(orderService.createOrder(cmd));
}
}Application Service (Orchestration)
@Transactional(rollbackFor = Exception.class)
public Order createOrder(OrderCreateCmd cmd) {
String orderNo = UUID.randomUUID().toString();
User user = userRepository.findById(cmd.getUserId())
.orElseThrow(() -> new DemoBusinessException("User not found"));
List<OrderItem> items = makeOrderItems(cmd.getProductItems(), orderNo);
orderDomainService.checkInventoryAndAssembleOrderItems(items);
DeliveryAddress address = deliveryAddressRepository.findById(cmd.getDeliveryAddressId())
.orElseThrow(() -> new DemoBusinessException("Address not found"));
Order order = Order.create(orderNo, address, items, user.getUserId());
orderDomainService.lockInventory(order);
orderRepository.createOrder(order);
orderMessageProducer.publish(order, OrderEventTypeEnum.INIT);
return order;
}Domain Service (Business Logic Coordination)
public void checkInventoryAndAssembleOrderItems(List<OrderItem> orderItems) {
if (CollectionUtils.isEmpty(orderItems)) {
throw new DemoBusinessException("No products selected");
}
List<Long> productIds = orderItems.stream()
.map(OrderItem::getProductId)
.collect(Collectors.toList());
List<Product> products = productRemoteService.getProductInfos(productIds);
if (CollectionUtils.isEmpty(products)) {
throw new DemoBusinessException("Product info not found");
}
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getProductId, p -> p));
for (OrderItem item : orderItems) {
Product product = productMap.get(item.getProductId());
if (product == null) {
throw new DemoBusinessException("Product [" + item.getProductName() + "] does not exist");
}
if (product.getInventoryCount() < item.getCount()) {
throw new DemoBusinessException("Product [" + product.getProductName() + "] insufficient inventory");
}
item.setPrice(product.getPrice());
item.setProductName(product.getProductName());
}
}
public void lockInventory(Order order) {
LockInventoryResponse resp = scheduleRemoteService.lockInventory(order)
.orElseThrow(() -> new DemoBusinessException("Inventory lock failed"));
if (resp.getLockEndTime().before(new Date())) {
throw new DemoBusinessException("Inventory lock expired");
}
order.setLockInventoryEndTime(resp.getLockEndTime());
}Infrastructure: Repository Implementation
@Repository
public class OrderRepository implements IOrderRepository {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Override
public Optional<Order> findById(long orderId) {
OrderEntity entity = orderMapper.selectById(orderId);
return Optional.ofNullable(OrderConvertor.toDomain(entity));
}
@Override
public void createOrder(Order order) {
OrderEntity entity = OrderConvertor.toEntity(order);
orderMapper.insert(entity);
order.setOrderId(entity.getId());
for (OrderItem item : order.getOrderItemList()) {
item.setOrderId(entity.getId());
orderItemMapper.insert(OrderItemConvertor.toEntity(item));
}
}
}Huolala CRM Refactoring Using DDD
The CRM system faced growing complexity and maintenance challenges. The refactor aimed to improve extensibility, stability, and delivery efficiency by migrating to a hexagonal architecture and applying DDD tactical design.
Key steps:
Identify pain points (code readability, onboarding difficulty, change risk).
Choose a reconstruction strategy. Two patterns were compared: the "killer" mode (new modules replace old code) and the "repairer" mode (incrementally extract domain layer). The repairer mode was selected for its lower risk.
Phase 1 – Invert dependencies of the data‑access layer, turning DAOs into infrastructure repositories that implement domain interfaces.
Phase 2 – Move business logic from services into the domain layer, introducing domain services to coordinate aggregates.
Phase 3 – Apply dependency inversion to remote‑call components, making them infrastructure.
Phase 4 – Clean up package names, extract shared components, and finalize the hexagonal module layout.
After refactoring, a typical CRM method shrank from over 200 lines to fewer than 50, with each line representing a distinct business operation.
Conclusion
The DDD‑driven hexagonal architecture dramatically simplified the CRM codebase, enhanced maintainability, and reduced the cost of swapping underlying technologies by more than 50 %.
Key take‑aways:
DDD has no single standard; adapt it to your domain.
Focus on business orchestration rather than strict adherence to “pure” DDD concepts.
Keep the domain model independent from data models to enable zero‑change business logic when infrastructure evolves.
Appendix 1 – Key Terminology
Bounded Context, Sub‑Domain, Aggregate, Aggregate Root, Repository, etc.
Appendix 2 – Common Questions
Using a rich (active) model is optional; a “DDD‑Lite” approach with plain data objects can be sufficient when the ORM (e.g., MyBatis) does not favor active models.
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.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
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.
