Transform Messy Code with DDD + CQRS: A Practical Guide for Clean Architecture

Learn how to break the cycle of unreadable, tangled code by applying Domain-Driven Design and Command-Query Responsibility Segregation, with concrete examples, step-by-step refactoring, architecture diagrams, code snippets, and practical tips for gradually adopting these patterns in real-world backend projects.

ITPUB
ITPUB
ITPUB
Transform Messy Code with DDD + CQRS: A Practical Guide for Clean Architecture

Why Your Code Is So Bad

Before prescribing a solution, identify the root causes: monolithic services that cram validation, business logic, database access and response handling into a single method; a “database-as-truth” mindset that makes business rules an after‑thought; and ambiguous terminology that causes miscommunication between product, developers and DB designers.

These problems stem from letting technical implementation dictate business intent. Domain-Driven Design (DDD) acts as a translator, aligning business language with technical code.

Quickly Practicing DDD

Using an online ordering system as an example, the key DDD concepts are introduced:

2.1 DDD Terminology

Domain : the problem space, e.g., the food-delivery domain covering ordering, payment and delivery.

Aggregate Root : the primary entity that controls modifications, such as an Order aggregate.

Value Object : an immutable attribute group without its own identity, like an order address.

Domain Service : operations that involve multiple aggregates, for example calculating order discounts.

Bounded Context : a self-contained sub-domain, e.g., separate contexts for order processing and menu management.

Domain Event : a significant business occurrence, such as OrderCreatedEvent.

The purpose of these terms is to eliminate communication gaps.

2.2 DDD Layered Architecture

Traditional three‑tier architecture mixes concerns, while DDD separates them into distinct layers.

// Traditional monolithic service
@RestController
public class OrderController {
    // 300‑line saveOrder method mixing validation, business rules,
    // price calculation, persistence and response handling
}

// DDD‑styled layers
@RestController
public class OrderController {
    @PostMapping("/orders")
    public Result createOrder(@RequestBody CreateOrderCommand command) {
        return orderApplicationService.createOrder(command);
    }
}

@Service
public class OrderApplicationService {
    public Result createOrder(CreateOrderCommand command) {
        Order order = orderFactory.create(command);
        orderRepository.save(order);
        return Result.success(order.getId());
    }
}

public class Order {
    public void applyCoupon(Coupon coupon) {
        if (!coupon.isValid()) throw new BusinessException("Invalid coupon");
        if (this.status != OrderStatus.CREATED) throw new BusinessException("Order status does not allow coupon");
        // coupon calculation logic …
    }
}

All core business logic resides in the domain layer; other layers merely coordinate or expose APIs.

2.3 Real‑World Refactoring: Online Ordering

Step 1 – Identify bounded contexts: Order, Inventory, Payment, Delivery.

Step 2 – Design aggregate roots: Order, DishStock, UserAccount.

Step 3 – Define domain events: OrderCreatedEvent, PaymentCompletedEvent, OrderAssignedEvent.

Step 4 – Implement domain logic:

public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private Money totalAmount;
    private OrderStatus status;
    private UserId userId;

    public static Order create(UserId userId, List<OrderItem> items, Coupon coupon) {
        Order order = new Order();
        order.id = OrderId.generate();
        order.userId = userId;
        order.items = items;
        order.status = OrderStatus.CREATED;
        order.calculateTotal();
        if (coupon != null) order.applyCoupon(coupon);
        order.addDomainEvent(new OrderCreatedEvent(order.id, userId, order.totalAmount));
        return order;
    }

    private void calculateTotal() {
        this.totalAmount = items.stream()
            .map(i -> i.getPrice().multiply(i.getQuantity()))
            .reduce(Money.ZERO, Money::add);
    }
}

After refactoring, the code reads like an IKEA instruction: each piece has a clear place and purpose.

Rapid CQRS Practice

CQRS separates commands (writes) from queries (reads). The analogy is a kitchen where the same chef both cooks and takes orders—splitting these roles prevents bottlenecks.

3.1 Read‑Write Separation

Command side : handles create, update, delete operations, enforces domain rules.

Query side : focuses on fast data retrieval, often using read‑optimized stores.

// Command side service
@Service
public class OrderCommandService {
    public OrderId createOrder(CreateOrderCommand command) {
        Order order = Order.create(...);
        orderRepository.save(order);
        return order.getId();
    }
}

// Query side service
@Service
public class OrderQueryService {
    public List<OrderSummary> getUserOrders(UserId userId, int page, int size) {
        return orderSummaryRepository.findByUserId(userId, page, size);
    }
}

3.2 Data Synchronization

Three common approaches:

Event‑driven sync (recommended) : after a command succeeds, publish an event that updates the read model, cache and statistics.

Dual‑write : write to both command and query databases in the same transaction (risking consistency issues).

CDC (Change Data Capture) : listen to database changes and propagate them to the read side with minimal intrusion.

3.3 CQRS Boosts Query Performance

Traditional query joins multiple tables and can take >200 ms. After CQRS, a query hits a pre‑computed view (e.g., Elasticsearch) and returns in < 20 ms.

// Before CQRS
@Query("SELECT o FROM Order o LEFT JOIN FETCH o.items i LEFT JOIN FETCH o.user u WHERE u.id = :userId")
List<Order> findUserOrdersWithDetails(@Param("userId") Long userId);

// After CQRS
public List<OrderSummary> getUserOrders(Long userId) {
    return orderSummaryESRepository.findByUserId(userId); // 20 ms
}

Putting DDD + CQRS into Practice

Adopt the changes incrementally:

4.1 Three Implementation Stages

Stage 1 – Small new project : apply DDD layers, separate command and query services within a single database.

Stage 2 – Growing project : introduce true CQRS with separate read/write databases and event‑driven sync.

Stage 3 – Large‑scale microservices : split bounded contexts into independent services, each using DDD + CQRS and communicating via events.

4.2 Pitfall Checklist

Over‑design : avoid turning simple enums into full‑blown value objects; use enums when appropriate.

Misusing CQRS : not every CRUD screen needs CQRS; reserve it for high‑traffic or complex read scenarios.

Ignoring eventual consistency : design UI and error handling to cope with the lag between command and query sides.

Learning Recommendations

Investing in DDD + CQRS yields:

Exponential improvement in code maintainability.

Qualitative boost in development speed because new features target the appropriate aggregate.

Higher professional credibility in interviews and performance reviews.

Reduced overtime thanks to clearer responsibilities and better performance.

Start by picking a complex module you own and refactor it step by step—splitting a 500‑line service into domain objects is already a huge win.

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.

microservicesBackend DevelopmentDDDRefactoringCQRSDomain Modeling
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

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.