How to expose read-only calculations from business domain model on back-end to front-end using CQRS? A Read-Model vs. Write-Model Issue

Viewed 603

I have two questions related to CQRS and Domain Driven Design (DDD).

As far as I understand the segregation idea behind CQRS, one would have two separate models, a read model and a write model. Only the write model would have access to and use the business domain model by means of commands. The read model, however, directly translates database content to DTOs by means of queries and has no access to the business domain at all.

For context: I am writing a web application back-end that provides calculation services for particle physics. Now my questions:

1.) My business domain logic contains some functions that calculate mathematical values as output from given measurement system configurations as input. So, technically, these are read-only queries that calculate the values on-the-fly and do not change any state in any model. Thus, they should be part of the read model. However, because the functions are heavily domain related, they have to be part of the domain model which again is part of the write model. How should I make these calculation functions available for the front-end via my API when the read model, that should contain all the queries, does not have any access to the domain model?

Do I really have to trigger a command to save all the calculations to the database such that the read model can access the calculation results? These "throw-away" calculations will only be used short-term by the front-end and nobody will ever have to access the persistent calculation results later on. It's the measurement configuration that has to be persistent, not the calculation results. Those will be re-calculated many times, whenever the user hits the "calculate" button on the front-end.

2.) I also feel like I duplicate quite a bit of data validation code, because both read model and write model have to deserialize and validate the same or very similar request parameters in the process chain http request body -> json -> unvalidated DTO -> validated value -> command/query. How should I deal with that? Can I share validation code between read model and write model? This seems to dissolve the segregation.

Thanks in advance for all your help and ideas.

2 Answers

I think that what do you have is a set of domain services that with a given input they return an output. As you said, this services are located in the domain. But, nothing denies you to use them in the read model. As long as you don't change the domain inside the functions, you can use them in any layer above the domain. If, for any reason, this solution is not viable, because for example the services require domain objects that you cannot/don't want to build in the query side, you can always wrap the domain services inside application services. There you take in input a base object, you do all the transformations to the domain one, you call the domain service and you return the resulting value.

For the second question, you can build a validation service in the domain layer, as a set of services or simple functions. Again, nothing denies you to use them in the validation steps. I've done the same in my last web app: the validation step of the form data calls a set of domain services, that are used also when I build the domain objects during the handling of a command. Changing the validation in the domain has as effect that also the Web related validation changes. You validate two times (before building the command and during the building of the domain object), but it's ok.

Take a look at the ports/adapters or the onion architecture: it helps a lot understanding what should stay inside a layer and what can be used by an overlapping layer.

Just to add my two pence worth.

We have microservices that use mongo as a backend. We use NET Core using mediatr (https://github.com/jbogard/MediatR) to implement a CQRS pattern in the MSs. We have .Query and .Command features.

Now in our world we never went to Event Sourcing. So, at the mongo level, our read and write models (entities) are the same. So what worked best for us was to have a single entity model which could be transformed into different models (if needed ) via each command/query handler(s).

If you are doing pure CQRS via event sourcing (so different read and write models) or something else then please ignore me! But this worked best for us.

Related