11

Scenario:

A customer places an order, then, after receiving the product, provides feedback on the order process.

Assume the following aggregate roots:

  • Customer
  • Order
  • Feedback

Here are the business rules:

  1. A customer can only provide feedback on their own order, not someone else's.
  2. A customer can only provide feedback if the order has been paid for.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Now, assume the business wants a new rule:

  1. A customer can only provide feedback if the Supplier of the order's goods is still operating.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

I've placed the implementation of the first two rules within the Feedback aggregate itself. I feel comfortable doing this, especially given that the Feedback aggregate references all of the other aggregates by identity. E.g., the properties of the Feedback component indicate that it knows of the existance of the other aggregates, so I feel comfortable having it know of the read only state of these aggregates as well.

However, based on it's properties, the Feedback aggregate has no knowledge of the existance of the Supplier aggregate, so should it have knowledge of the read only state of this aggregate?

The alternative solution to implementing rule 3 is to move this logic to the appropriate CommandHandler. However, this feels like it's moving domain logic away from the "center" of my onion-based architecture.

Overview of my onion architecture

5
  • Repository interfaces are part of the domain. So a construction logic (which by itself is considered a service in DDD book) can call a Order's repository to ask if Order's supplier is still operating.
    – Euphoric
    May 20, 2016 at 6:17
  • 1
    Firstly, a Supplier aggregate's operating state would not be queried via an Order repository; Supplier and Order are two separate aggregates. Secondly, there was a question on the DDD/CQRS mailing list about passing aggregate roots and repositories to other aggregate root methods (including the constructor). There was a variety of opinions, but Greg Young mentioned that passing aggregate roots as parameters is common, while another person said that repositories are more closely related to infrastructure than domain. E.g., repositories "abstract in memory collections" and have no logic.
    – magnus
    May 20, 2016 at 6:33
  • Isn't Supplier related to Order? What happens when Supplier that is unrelated to Order is passed in? Well, "is supplier operating" is not a logic. It is simple query. Also, there is reason why it is common : Without it, your code becomes much more complex and requires passing around information where errors can happen. Also, "repository interface" is not infrastructure. The repository implementation is.
    – Euphoric
    May 20, 2016 at 6:39
  • You're right. Just like a Customer can only provide feedback on one of their own orders ($order->customerId() == $customer->customerId()), we also have to compare the supplier ID ($order->supplierId() == $supplier->supplierId()). The first rule guards against the user supplying incorrect values. The second rule guards against the programmer supplying incorrect values. Nevertheless, the check as to whether the supplier is operating has to be either in the Feedback entity, or in the command handler. Where is the question.
    – magnus
    May 20, 2016 at 6:44
  • 3
    Two comments, not directly related to the question. First, passing Aggregate roots as arguments to another aggregate looks wrong -- those should be Ids -- there's nothing useful an aggregate can do with another aggregate. Second, Customer and Supplier are... difficult, the book of record in both cases is the real world: you can't stop the supplier in the real world by sending a CeaseOperations command to your domain model. May 20, 2016 at 6:52

2 Answers 2

2

If transactional correctness requires one aggregate knowing about the current state of another aggregate, then your model is wrong.

In most cases, transactional correctness is not required. Businesses tend to have tolerance around latency and stale data. This is especially true of inconsistencies that are easy to detect and easy to remedy.

So the command is going to be run by the aggregate that changes state. To perform the not necessarily correct check, it needs a not necessarily the latest copy of the state of the other aggregate.

For commands on an existing aggregate, the usual pattern is to pass a Repository to the aggregate, and the aggregate will pass its state to the repository, which provides a query that returns an immutable state/projection of the other aggregate

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

But construction patterns are weird - when you are creating the object, the caller already knows the internal state, because it is providing it. The same pattern works, it just looks pointless

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

We're following the rules by keeping all the domain logic in the domain objects, but we aren't really protecting the business invariant in any useful way by doing so (because all of the same information is available to the application component). For the creation pattern, it would be just as good to write

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
10
  • 1. Does the SupplierOperatingQuery query the read model, or "Query" in the name misleading? 2. Transactional consistency is not required. It doesn't matter if the supplier stops operations one second before a customer leaves feedback, but does that mean we shouldn't check it anyway? 3. In your example, does supplying a "query service" rather than the object itself enforce transactional consistency? If so, how? 4. How the use of such query services impact unit testing?
    – magnus
    May 20, 2016 at 6:56
  • 1. Query, in the sense that calling it doesn't change the state of anything. 3. There's no transactional consistency with the query service, there's no interaction between it and concurrently running command that is modifying the other aggregate. 4. In this case, it would be part of the domain model's spi, so just provide a test implementation. Hmm, that's a bit weird, though -- DomainService might not be the best term to use. May 20, 2016 at 12:39
  • 2. Keep in mind, because the data you are using here is across an aggregate boundary, your check may give you the wrong answer (ex: your check says it's not OK, but it should be because the other aggregate is changing). So it might be better to move that check to the read model (always accept the command, but create an exception report if the model is inconsistent). You can also arrange that the client only sends commands that are supposed to succeed - ie, the client shouldn't send commands that it expects to fail, based on its understanding of the current state. May 20, 2016 at 12:45
  • 1. It's generally frowned upon for the "write side" to query the "read side" (e.g., event sourced projections). "... in the sense that calling it doesn't change the state of anything" - neither does simply using an immutable accessor, which I would argue is much simpler. 2. It would be fine to duplicate the check in the read model, but if you move it (read: REmove it from the server), you're creating problems for yourself. Firstly, your business rule has to be duplicated in each client (web browser and mobile clients). Secondly, it's simple to bypass this check:
    – magnus
    May 20, 2016 at 15:36
  • 3. "... there's no interaction between it and concurrently running command that is modifying the other aggregate" - neither does loading the Supplier aggregate itself, since only the Feedback aggregate is being modified. 4. So SupplierOperatingQuery is an interface that requires a concrete implementation, meaning you have to create a mock implementation in your unit test simply to test the true/false value of a single variable that already exists in the other object? Smells like overkill. Why not create a CustomerOwnsOrderQuery and OrderIsPaidQuery as well??
    – magnus
    May 20, 2016 at 15:49
-1

I know this is an old question, but I'd like to point out that the issue directly stems from an incorrect premise. That is, the aggregate roots we are meant to assume exist are simply incorrect.

There is only one aggregate root in the system you have described: Customer. Both an Order and Feedback, while they may be aggregates in their own right, are dependent on the Customer for existence so are not themselves aggregate roots. The logic you provide in your feedback constructor seems to indicate that an Order MUST have a customerId and Feedback MUST also be related to a Customer. This makes sense. How can an Order or Feedback not be related to a Customer? Additionally, Supplier seems to logically be related to Order (so would be inside this aggregate).

With the above in mind, all of the information you want is already available in the Customer aggregate root and it becomes clear you are enforcing your rules in the wrong place. Constructors are terrible places to enforce business rules and should be avoided at all costs. This is what it should look like (Note: I'm not going to include constructors for Customer and Order because Factories should probably be used. Also not showing all interface methods).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Okay. Let's break this down a little bit. The first thing you will notice is how much more declarative this model is. Everything is an action, it becomes clear WHERE business rules should apply. The design above doesn't just "do" the right thing, it "says" the right thing.

What would lead anyone to assume rules are being executed in the following line?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

Second, you can see that all of the logic pertaining to validating business rules is carried out as closely as possible to the models to which they pertain. In your example, the constructor (a single method) is performing multiple validations against different models. That breaks SOLID design. Where would we add a check to make sure that the Feedback content doesn't contain bad words? Another check in the constructor? What if different kinds of Feedback need different content checks? Ugly.

Third, looking at the interfaces, you can see there are natural places to extend/modify the rules through composition. For example, different kinds of orders can have different rules regarding when feedback can be provided. Order can also provide different kinds of feedback, which in turn can have different rules for validation.

You can also see a bunch of ICustomer* interfaces. These are used to compose the Customer aggregate we need here (probably not just called Customer). The reason for this is simple. It's VERY likely that a Customer is a HUGE aggregate root that spreads out all over your domain/DB. By using interfaces, we can decompose that one aggregate (which is likely too large to load) into multiple aggregate roots that only provide certain actions (like ordering or providing feedback). You can see the aggregate in my implementation can BOTH place orders AND provide feedback, but cannot be used to reset a password or change a username.

So the answer to your question is that aggregates should validate themselves. If they can't you likely have a deficient model.

3
  • 1
    While aggregate boundaries are different depending on who is designing the system, I think “one aggregate” stemming from order is just plain silly. Your example of a Supplier being part of an order is a good case in point - can a Supplier not exist until after an Order is created? What about duplicate Suppliers:
    – magnus
    Feb 9, 2018 at 22:03
  • @user1420752 I think you may have it backwards. The model above implies the reverse. That an Order cannot exist without a Supplier. My example is simply using the information/rules/relationships I could glean from the code provided. I would agree that, much like Customer, Order is likely a large, complex aggregate in its own right (though not a root). One which also may require decomposition into a handful of concrete implementations a depending on the context. The point I am illustrating is that entities MUST validate themselves. As you can see, it’s cleaner that way. Feb 12, 2018 at 21:45
  • @user1420752 I'd like to add that often methods/constructors that require many arguments are a sign of an anemic model where the data is separated from the behavior (and thus needs to be injected in large chucks to the pieces that act on the data). The Feedback constructor you provided is an example of this. Anemic models tend to reduce cohesion and add extra coupling semantics (like checking IDs numerous times). High cohesion generally means that every method in an entity utilizes all of it's instance variables. This naturally leads to decomposition of large aggregates like Customer or Order Feb 12, 2018 at 21:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.