Get data from Read model vs receiving data from Write model With DDD & CQRS

173 Views Asked by At

I am doing a project with DDD, for the first time. The project is related to car rental and there is something that really confuses me and I can't find an answer to it. I would be more than happy if someone could give me a detailed and clear answer.

I understand that in simple cases such as for example the manager wants to add/update a new/existing vehicle, he ultimately turns to the writing model that writes the data to the DB and in the case that he wants to see the existing vehicles, for example, he turns to the reading model, and so on for every read and write request.

My question is this and I will give a direct example from my project, I receive a request to rent a car with data such as: pick-up and drop-off locations, rental dates, driver's age, etc. and I need to return the available vehicles to the customer on those dates + price offers All of this, according to my understanding, should be calculated from several different aggregates, so I am making a domain service that will calculate the data, the question is where should I return the data to the client in this situation, after all, I will not save the calculation results in the DB just to make another request from the read model, so What do I do in situations where I need an immediate answer from the writing model and it does not need to go through the DB?

In such situations, are objects from the functions in the domain service returned directly to the client? And if so, which entities?

1

There are 1 best solutions below

9
Tseng On BEST ANSWER

example the manager wants to add/update a new/existing vehicle, he ultimately turns to the writing model that writes the data to the DB

First and foremost, the manager do not turn to any type of model. The manager is a user and do not care about the implementation, they care about the business process, which do not involve technical terms such as "database", "model", "repository" etc.

Read model and write model are abstract terms. Depending on your the implementation of your domain, it may mean

  1. Two different databases or tables
  2. One shared database or tables

Some architecture choices will influence whether you go with one of those. A reason to go with different tables/databases, can be when you want to use event sourcing or when your domains are very pure (No database/infrastructure related properties in your domain models or when the ORM impedance mismatch is particularly big) and different from your read models.

Another criteria for splitting your read and write models into multiple tables/databases is, when you expect them to scale differently, for example if the car rental company is so big or has so many visitors that you need many servers to serve the queries, while the actual reservations are little, then you can split them too. This allows easy scaling up for the read model and write model separately. Also a very high load on query side won't slow down your business when booking the actual rentals.

Shared tables you use when the domain is simpler and the differences are small enough or you are not very pragmatic/dogmatic about DDD and you don't need even sourcing.

In case of two different databases, you'd have the write model, when persisting the data, have send out messages that are used by the "read model" to create a read-only (or rather: query-able, because by default event sourced models can't be queried at all) model.

So far for the basics.

CQRS itself is more concerned about how the technical implementation of these is. In generally you do this with an in-process request/query framework, such as "MediatR" for .NET where you split the messages into "queries" and "command" where "command handler" contain your write logic or (less common) a read-only repository and an process manager (aka saga).

I receive a request to rent a car with data such as: pick-up and drop-off locations, rental dates, driver's age, etc. and I need to return the available vehicles to the customer on those dates + price offers

First it's unclear, why you need the drivers age for the query, as the data is kinda unrelated to the person wanting to rent it.

Anyways, you'd want to create a AvailableCarsQuery or something like that as a "read model"/query

public class AvailableCarRequest : IRequest<RentableCar[]>
{
    public string? PickUpLocation { get; set; }
    public string? DropOffLocation { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

public class RentableCar
{
    public int Id { get; set; }
    public string Brand { get; set; }
    public string Model { get; set; }
    public decimal Price { get; set; }
    public string Currency { get; set; }
}

The AvailableCarsQuery defines what's your query parameters and what is returned the expected return (IRequest<RentableCar[]>). Then implement the handler

public AvailableCarHandler : IRequestHandler<AvailableCarRequest, RentableCars[]>
{
    public async Task<RentableCar[]> Handle(AvailableCarRequest request, CancellationToken cancellationToken)
    {
        // do your query here
        RentableCar[] rentableCars = await ...;
        return rentableCars;
    }
}

How the concrete query looks, depends on whether you have shared or separate databases/tables. If you find yourself that your write models are to hard to query, i.e. you need to do dozens of calls to get the data or you have to search in memory, consider splitting the persistence into different tables, which are more optimized for query performance.

Then call it

[HttpPost("search/cars")]
public Task<ActionResult<RentableCar[]>> GetAvailableCars(AvailableCarRequest request)
{
    RentableCar[] rentableCars = await mediator.Send(request);
    return Ok(rentableCars);
}

The write side would look similar, instead you'd have a RentCarCommand instead and (in general) no return value.

public class RentCarCommand : IRequest
{
    public int CarId { get; set; }
    public string PickUpLocation { get; set; }
    public string DropOffLocation { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
    public int CustomerId { get; set; }
}

public class RentCarHandler : IRequestHandler<RentCarcommand>
{
    public async Task Handle(Jing request, CancellationToken cancellationToken)
    {
        Car car = await unitOfWork.Cars.GetById(request.CarId);
        Customer customer = await unitOfWork.Customers.GetById(request.CustomerId);
        car.RentTo(customer, request.From, request.To, request.PickUpLocation, request.DropOffLocation);
        await unitOfWork.SaveChangesAsync();
    }
}

Update to address some of the comments

The only question is about the reading model because I have a lot of data that does not appear in the tables but is calculated from the tables or from other places. ... Do you make an anemic model with classes of query data without behavior and only add services to calculate data that does not appear directly in the DB or is it more like aggregates?

Depends on the calculated data. If the data can be calculated at the time of the writing, you should write it into the read model every time the write model changes. Then you can simply query the read model w/o doing any calculations in your code. This is also a vital part of "optimizing read model for querying".

If it can't be calculated by the time of writing, then you can try to calculate it within the query (if simple enough), i.e. in the select clause of EF Core or have a service or the query handler calculate it, depending on whether or not the calculation logic is reusable in other places too.

And I want to display it in half hour increments from the opening time to the closing time which of course the DB only has the opening and closing time and another table of branch opening events.

(for example: the logic of the dates, the selection of the available vehicles in the selected branches that alone includes a lot of algorithms, price offers, which should also include complex algorithms and there are many other things) and if so, does this also include the parts of, for example, checking data for the results of queries?

Well yea, this kind of stuff sounds like a need for a service. I guess I'd fetch the read model like above with the necessary properties and enrich it within the service (i.e. calculate the prices and conditions there), then return it.