Command handlers are the gatekeepers for changes in your system, but they’re often treated as simple data mappers. The real magic happens when they become the enforcers of your domain’s business rules.

Let’s see this in action. Imagine an OrderService that handles commands like CreateOrder and AddItemToOrder.

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final EventPublisher eventPublisher;
    private final CustomerService customerService; // For domain rules

    public OrderService(OrderRepository orderRepository, EventPublisher eventPublisher, CustomerService customerService) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
        this.customerService = customerService;
    }

    @Transactional
    public OrderId handle(CreateOrderCommand command) {
        // Domain rule check: Can this customer even place an order?
        if (!customerService.canPlaceOrder(command.getCustomerId())) {
            throw new BusinessRuleViolationException("Customer " + command.getCustomerId() + " is not allowed to place orders.");
        }

        Order order = new Order(new OrderId(UUID.randomUUID()), command.getCustomerId(), command.getOrderDate());
        orderRepository.save(order);

        eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getCustomerId(), order.getOrderDate()));
        return order.getId();
    }

    @Transactional
    public void handle(AddItemToOrderCommand command) {
        Order order = orderRepository.findById(command.getOrderId())
                .orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));

        // Domain rule check: Is the product in stock?
        if (!inventoryService.isInStock(command.getProductId(), command.getQuantity())) {
            throw new BusinessRuleViolationException("Product " + command.getProductId() + " is out of stock for quantity " + command.getQuantity());
        }

        // Domain rule check: Is the order already finalized?
        if (order.isFinalized()) {
            throw new BusinessRuleViolationException("Cannot add items to a finalized order.");
        }

        order.addItem(command.getProductId(), command.getQuantity(), command.getPrice());
        orderRepository.save(order);

        eventPublisher.publish(new ItemAddedToOrderEvent(order.getId(), command.getProductId(), command.getQuantity()));
    }
}

The OrderService isn’t just taking a CreateOrderCommand and spitting out an Order object. It’s first consulting customerService to see if the customer is even allowed to create an order. Then, when adding an item, it checks inventoryService for stock and verifies that the order itself isn’t in a state (like isFinalized()) that prevents modification. These aren’t infrastructure concerns; they’re fundamental to what an "order" means in this business.

The core problem this solves is the separation of concerns between what needs to be done (the command) and how it can be done according to the business’s established rules. Without command handlers enforcing these rules, your domain objects would be constantly bombarded with invalid states, leading to unpredictable behavior and difficult-to-debug issues. The command handler acts as a robust guard, ensuring that only valid transitions and operations are ever attempted.

Internally, the command handler orchestrates calls to repositories (for data retrieval and persistence) and other domain services or aggregates (for business logic validation). It’s the central point where the command’s intent is translated into a series of validated domain actions. The Transactional annotation is crucial here, ensuring that either all the changes and validations succeed, or none of them do, maintaining data consistency.

The exact levers you control are the dependencies injected into your command handlers. By injecting services like CustomerService or InventoryService, you’re giving the handler the context it needs to perform checks. The Order aggregate itself also exposes methods like addItem and isFinalized which encapsulate specific domain logic that the handler calls upon. The order in which these checks are performed can also be a critical domain rule – for example, you might want to check if a customer can place an order before attempting to check inventory for items they intend to buy.

A common pitfall is to delegate all validation to the Order aggregate itself. While aggregates should indeed enforce their own internal invariants (like not allowing negative quantities for an item), the broader business rules that span multiple aggregates or require external context (like customer eligibility or inventory levels) are best handled by the command handler orchestrating the interaction. This prevents the aggregate from becoming bloated with cross-cutting business logic that doesn’t strictly pertain to its own state.

The next step is to consider how to handle compensating actions when a sequence of commands within a larger business process fails.

Want structured learning?

Take the full Cqrs course →