Search code examples
architecturedomain-driven-designcqrsevent-sourcingbusiness-rules

How to handle business rules in DDD + CQRS + Event Sourcing approach?


I'm trying to figure out how to deal with complex domain model using CQRS/ES approach. Let's imagine we have e.g. Order domain entity, which handles both state and behavior of order. It has a Status property with transition rules for switching between statuses (implementing State pattern or any other kind of state machine). According to DDD principles, this logic should be implemented in Order class (representing the Order model) itself, having methods like approve(), cancel(), ship(), etc.

Looking at different public examples of this kind architecture, it turns out that domain entity and aggregate root is the same, and it handles both the state and behavior and even its own projection from events. Isn't it a violation of SRP?

But my question is more concrete: if I want to process new command (and apply new event), should I reconstitute entity from event stream (i.e. from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules? Or just handle commands and events themselves, without having any write-model entity?

Pseudocode to illustrate:

class ApproveOrderHandler
{
    private EventStore eventStore

    // ...

    public void handle(ApproveOrder event)
    {
        Order order = this.eventStore.findById(event.getOrderId()); // getting order projection from event store
        order.approve(); // handling business logic
        this.eventStore.save(order.releaseEvents()); // save new events (OrderApproved)
    }
}

class Order extends AbstractAggregate
{
    private Uuid id;

    private DateTime takenAt;

    private OrderStatus status;

    // ...

    public void approve()
    {
        this.status.approve(); // business rules blah blah
        this.Apply(new OrderApproved(this.id)); // applying event
    }

    // ...
}

Isn't that overdoing or somewhat?

And what should I do with relationships between entities in event-sourcing? If they exist only in "read model", there is no point in domain entity class.

EDIT: or maybe I should store state snapshot in "read database" and recover entity for operations from it? But it breaks idea of "different models for read & write"...

EDIT2: fixed read/write models mistake


Solution

  • TL;DR

    But my question is more concrete: if I want to process new command (and apply new event), should I reconstitute entity from event stream (i.e. from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules?

    Yes.

    Or just handle commands and events themselves, without having any write-model entity?

    No.

    Once more, with feeling

    The command handler lives in the application component; the business model lives in the domain component.

    The motivation for keeping these components separated: making model replacement cost effective. What the domain experts care about, where the business gets its win, is the domain model. We don't expect to write the business model once and get it correct for all of time -- much more likely that we will learn more about how we want the model to work, and therefore be delivering improvements to the model on a regular basis. Therefore, its important that there not be a lot of drag to replace one version of the model with another -- we want the replacement to be easy; we want the amount of work required to make the change to be reflected in the business value we get.

    So we want the good stuff separated from "the plumbing".

    Keeping all of the business logic in the domain component gives you two easy wins; first, you don't ever have to guess about where the business logic lives -- whether the specifics of the use case are easy or hard, the business logic is going to be in the Order, not anywhere else. Second, because the business logic is not in the command handler, you don't have to worry about creating a bunch of test doubles to satisfy those dependency requirements -- you can test against the domain model directly.

    So, we use handlers to reconstitute entities and calling their business logic methods, NOT to handling business logic itself?

    Almost -- we use repositories to reconstitute entities and aggregates to handle the business logic. The role of the command handler is orchestration; it's the glue between the data model and the domain model.