1

I am working on a microservice architecture using CQS and with domain objects built using DDD aggregates. I am just beginning to learn about CQS, CQRS, Domain Driven Design, so excuse me if what I post is completely wrong from either perspective. Currently, the architecture within a microservice is as follows:

Write side: RestApi situated at the controller level deserializes json bodies to commands (Controller layer) => (Service layer) commands are handled in command handlers where business logic is invoked on the aggregates, or handled within the handler itself => (Repository layer) domain objects are saved into (and hydrated from) the database via a Write repository (Database is shared between read and write layer, so there is no separate DB for projections as in CQRS)

Read side: Rest api (Controller layer) => communicates directly with read repositories which return DTOs. Have introduced also query handlers for aggregating data from multiple repositories (and services), when this is necessary.

Also, the aggregates are attempting to follow concepts described in Vaughn Vernon's Efficient aggregate design, whereas if an aggregate root needs to hold a reference to a different aggregate root, it does so by only storing the id and not the whole aggregate.

For the purpose of this questions, I will introduce a basic aggregate:

class Order {
CustomerId;
...
CalculateDiscount();
}
class Customer {
Id;
Name;
Age;

IsStudent();
}

There are times within the application logic (on the write side), where a dependent aggregate needs to be loaded so that various items are checked. For example, even though the Order aggregate root is loosely coupled from the Customer and can be acted upon (persisted, invariants being maintained) individually, there are cases where business related to the customer needs to be checked. For example, when creating an order, the existence of the Customer must be first validated. Furthermore, as described in the small example, for calculating a discount, loading the customer and checking if they are a student would be a necessary step.

In order to do this, I have two solutions, but both have their pros and cons and I would like some opinions as to what is best (or if there are other alternatives).

Injecting the Customer write repository within the order command handler: Pro Seems to be more object oriented and enables business logic to be encapsulated within domain models. In the example above, the CalculateDiscount method can become CalculateDiscount(lCustomerWriteRepository) which encapsulates this logic within the aggregate.

Cons Apart from being able to retrieve the CustomerDomainModel the interface leaks methods to persist a CustomerDomainModel, which is a non-readonly operation that should only be done through handlers of CustomerCommands and not OrderCommands Refactoring the customer aggregate to a different service would lead to the logic being moved at a handler level

Injecting the Customer read repository within the command handler:

Pro The interface allows only readonly access to customers Read repository can be easily switched with proxy to other microservice if aggregate is separated to other microservice (As both read repository and communication over HTTP results in DTOs rather than domain model representations) Cons Logic involving calls to other aggregates is achieved at handler level. I prefer pushing as much logic towards the aggregates as possible. Changing UI requirements might affect DTOs being used in write operations and more and more unnecessary data might be fetched Aggregates are more anemic in business logic as opposed to the alternate solution.

I am hoping to find a solution to my above problem and perhaps understand what I am missing. Also, as a bonus question, I am failing to understand how Domain Services would come into play in such an architecture :)

1
  • Perhaps you could pass a customer as argument into the CalculateDiscount method.
    – Rik D
    Commented Aug 19, 2019 at 19:38

3 Answers 3

1

In the use case you describe, you are trying to use information stored in Customer to decide how to modify Order.

One good answer, as mentioned by Rik D, is to simply pass the Customer information to the Order as a command argument. That could mean passing the Customer domain entity, but that opens up the possibility that the Order will try to change the customer, which isn't what you want. So instead we normally represent the necessary information as an immutable value.

For something like this use case, we could just pass the isStudent value as an argument.

order::calculateDiscount(isStudent) {
    if (isStudent) {
        // ...

The application is responsible for finding the information that the domain model needs, the domain model calculates the consequences of that information.

An alternative approach, as you guessed, is to pass a "domain service" to the order object, so that the order object can ask the domain service for the information that it needs.

order::calculateDiscount(students) {
    if (students.includes(customerId)) {
        // ...

In the former design, the I/O all lives out in the application layer, which is where you really want it to be. That latter approach is more procedural.

Note that in either case, it's relatively straight forward to to isolate the domain model to test it - the first API is cleanly isolated by the use of values. The latter case can be isolated by using a substitute for the domain service that uses an in memory cache to find its answers.

3
  • I think my question refers more to what should the OrderApplicationService (Order handler) use to retrieve the Customer object from the database. If it should use the ReadRepository (which returns the CustomerDTO), it will just call the order::calculateDiscount(isStudent), after extracting the Boolean from the DTO. However, the order handler could be injected with the CustomerWriteRepository, whose interface lives with the domain models and implementation lives at the infrastructure layer.The call could then be order::calculateDiscount(ICustomerWriteRepository) and all logic encapsulated
    – Mike
    Commented Aug 20, 2019 at 6:07
  • What I have found: if a use case doesn't require changing an entity in your database, then that use case should not depend on the "write repository" for that entity. It makes it a lot easier to track which code can change which parts of the model. Commented Aug 20, 2019 at 17:16
  • @Mike What does CQRS stand for again? I wouldn't consider a system to be employing CQRS if the write side has any knowledge of the read side. This means that any given call stack can only contain objects that are part of it's "side". This is in contrast to what VoiceOfUnreason is commenting above, but we have to understand that the entire purpose of CQRS is in recognition of the fact that the write and read sides of an application can evolve independently. Creating coupling between them can introduce regression should either change. Is this really Responsibility Segregation? Commented May 17, 2020 at 16:25
1

I would choose neither option.

I am failing to see a compelling reason to pass any repository to a domain method. Not only does it invert a dependency, it likely adds I/O to your domain. This is not ideal.

Who is calling Order.CalculateDiscount(IRepository repo)? And what do the first few lines of this method look like? Something like var customer = repo.Find(this.customerId)?

Why not just have the caller deal with the repository (to which it has a reference already) and refactor your domain to Order.CalculateDiscount(Customer customer)?

Not only does the above achieve looser coupling and higher cohesion, it makes your domain more testable.

"Imperative shell, functional core"

Where your "imperative shell" is your command handlers (application layer) coordinating each use-case, and your "functional core" is your domain executing each use-case (mutating state).

0

For example, when creating an order, the existence of the Customer must be first validated

This is a consistency constraint for me. If Customers and orders were two different aggregates, then Customers could know anything about an order or vice-versa. But it's also true that there are many ways to model the same domain. We could consider orders as en entity of the Customer aggregate or keep them separate.

It's clear that Customer makes orders so the relationship betweeen these two concept is explicit. Though, In the Orders aggregate, the Customer it's just some guy who made an order, while in the Customers aggregate, a Customer means many different things. Thus, it's time to introduce the concept of bounded context to model at the best the domain. The order managemnt context relies on order but each order has its Customer, and vice-versa with the Customer management context

In CQRS different bounded contexts are not contemplated: each command mutates the state of the aggregate. The read side acts as consequence but can aggregate data on its own needs. If you decide to keep two BCs, you will have two separate moving parts of your software and an higher level mechanism to keep the contexts aligned, for instance you could have

  • two applications and a message broker that routes aggregate update events between the contexts.

  • two packages and an internal event bus that converts domain events from an aggregate to commands toward the other

  • whatever gets the job done

Then you can have separate read models that render separate views and an aggregation layer that "joins" information, or a single projection hydrated from the updates of both contexts.

Personally i would prefer the solution 1 beacuse makes the two projections more independent. Read models are made to grow in time, you could start with a simple Customer list along with its orders, then add an API which retreives hystorical information per customer, then customer statistics. The way your aggregate puts bricks together is up to aggregation layer

Your Answer

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

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