0

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 :)

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

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.

  • 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 Aug 20 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. – VoiceOfUnreason Aug 20 at 17:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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