6

I'm using CQRS + ES and I have a modeling problem that can't find a solution for.
You can skip the below and answer the generic question in the title: Where would you query data needed for business logic?
Sorry of it turned out to be a complex question, my mind is twisted at the moment!!!
Here's the problem:
I have users that are members of teams. It's a many to many relationship. Each user has an availability status per team.
Teams receive tickets, each with a certain load factor, that should be assigned to one of the team's members depending on their availability and total load.
First Issue, I need to query the list of users that are available in a team and select the one with the least load since he's the eligible for assignment.(to note that this is one of the cases, it might be a different query to run)
Second Issue, load factor of a ticket might change so i have to take that into consideration when calculating the total load per user . Noting that although ticket can belong to 1 team, the assignment should be based on the user total load and not his load per that team.
Currently a TicketReceivedEvent is received by this bounded context and i should trigger a workflow to assign that ticket to a user.
Possible Solutions:

  1. The easiest way would be to queue events and sequentially send a command AssignTicketToUser and have a service query the read model for the user id, get the user and user.assignTicket(Ticket). Once TicketAssignedEvent is received, send the next assignment command. But it seems to be a red flag to query the read model from within the command handler! and a hassle to queue all these tickets!
  2. Have a process manager per user with his availability/team and tickets assigned to that user. In that case we replace the query to the read side by a "process manager lookup" query and the command handler would call Ticket.AssignTo(User). The con is that i think too much business logic leaked outside the domain model specifically that we're pulling all the info/model from the User aggregate to make it available for querying

I'm inclined to go with the first solution, it seems easier to maintain, modify/extend and locate in code but maybe there's something i'm missing.

hsen
  • 435
  • 5
  • 13

3 Answers3

2

Always (well, 99.99% of cases) in the business/domain layer i.e in your "Command" part of CQRS. This means that your repositories should have methods for the specific queries and your persistence model should be 'queryable' enough for this purpose. This means you have to know more about the use cases of your Domain before deciding how to implement persistence.

Using a document db (mongodb, raven db or postgres) might make work easier. If you're stuck with a rdbms or a key value store, create querying tables i.e a read model for the write model, acting as an index :) (this assumes you're serializing objects). If you're storing things relationally with specific table schema for each entity type (huge overhead, you're complicating your life) then the information is easily queryable automatically.

Yves Reynhout
  • 2,982
  • 17
  • 23
MikeSW
  • 16,140
  • 3
  • 39
  • 53
  • Thx for the answer, currently i'm using an mssql db and modeling the event store in it. Im serializing the event and saving them. Just to make sure i understand correctly, you're saying i should query the read side from the command handler. – hsen Jun 18 '15 at 19:56
  • 1
    No, I'm saying, you should have a some query index used to identify business entities according to a business criteria. It's still a part of the 'command' model – MikeSW Jun 18 '15 at 20:45
  • This falls under the second option i presume. I cane save needed criteria in my process manager and then the querying is done by the router to locate the appropriate saga/process manager to handle the TicketCreatedEvent. – hsen Jun 18 '15 at 20:51
  • ES repository should *never* provide any query interface due to the nature of ES itself. It should only have Add, Get and Delete. Queries are executed against the read model, which is *not* accessed through the repository. – Alexey Zimarev Jun 20 '15 at 08:59
  • ES is an implementation detail. A business repository _should always_ provide the required queries for business needs only. The read model should be unknown for the write model, and not all persistence stores are easily queryable , that's why a form of persistence querying is needed at the persistence level. Btw, a repository hasn't only CRUD responsibilities. – MikeSW Jun 20 '15 at 14:16
  • ES is something that this question was asked about. This is point number one. Adding methods for specific queries to repositories is a bad practice in general. This is point number two. – Alexey Zimarev Jun 20 '15 at 19:23
  • 1. Regardless of ES, my answer would have been the same. This is a design issue. 2. You're considering all queries to be equal. They're not. A **domain** query , needed by the domain (you know that _Get_ is still a query right? _GetByName_ is another common query) is part of the domain repository's interface. – MikeSW Jun 20 '15 at 20:23
2

Why can't you query the aggregates involved?

I took the liberty to rewrite the objective:

Assign team-ticket to user with the lowest total load.

Here we have a Ticket which should be able to calculate a standard load factor, a Team which knows its users, and a User which knows its total load and can accept new tickets:

Update: If it doesn't feel right to pass a repository to an aggregate, it can be wrapped in a service, in this case a locator. Doing it this way makes it easier to enforce that only one aggregate is updated at a time.

public void AssignTicketToUser(int teamId, int ticketId)
{
    var ticket = repository.Get<Ticket>(ticketId);
    var team = repository.Get<Team>(teamId);
    var users = new UserLocator(repository);
    var tickets = new TicketLocator(repository);
    var user = team.GetUserWithLowestLoad(users, tickets);

    user.AssignTicket(ticket);

    repository.Save(user);
}

The idea is that the User is the only aggregate we update.

The Team will know its users:

public User GetGetUserWithLowestLoad(ILocateUsers users, ILocateTickets tickets)
{
    User lowest = null;

    foreach(var id in userIds)
    {
        var user = users.GetById(id);
        if(user.IsLoadedLowerThan(lowest, tickets))
        {
            lowest = user;
        }
    }
    return lowest;
}

Update: As a ticket may change load over time, the User needs to calculate its current load.

public bool IsLoadedLowerThan(User other, ILocateTickets tickets)
{
    var load = CalculateLoad(tickets);
    var otherLoad = other.CalculateLoad(tickets);

    return load < otherLoad;
}

public int CalculateLoad(ILocateTickets tickets)
{
    return assignedTicketIds
        .Select(id => tickets.GetById(id))
        .Sum(ticket.CalculateLoad());
}

The User then accepts the Ticket:

public void AssignTicket(Ticket ticket)
{
    if(ticketIds.Contains(ticket.Id)) return;

    Publish(new TicketAssignedToUser
        {
            UserId = id,
            Ticket = new TicketLoad
                {
                    Id = ticket.Id,
                    Load = ticket.CalculateLoad() 
                }
        });
}

public void When(TicketAssignedToUser e)
{
    ticketIds.Add(e.Ticket.Id);
    totalLoad += e.Ticket.Load;
}

I would use a process manager / saga to update any other aggregate.

Thomas Eyde
  • 3,820
  • 2
  • 25
  • 32
  • The problem is that ticket load can change based on its status or category for example. Which means that every time the user wants to update a tixket, i'll have to update 2 aggregates which violates the principles/guidelines of DDD – hsen Oct 10 '15 at 08:09
  • But can't the User then load its assigned tickets and calculate the actual load? My guess is that the ticket load changes at a different point in time. – Thomas Eyde Oct 10 '15 at 16:33
  • 1
    That is a solution but it's a decision i didn't commit to yet: should aggregates have access to repositories? – hsen Oct 11 '15 at 17:30
  • I think that no patterns should restrict us from doing the right thing, or encourage us to do the opposite. I don't see anything wrong in an aggregate to access the repository as long as it's passed as a dependency and there are no updates.You could, of course, wrap the repository in a service. That way there are no repository to save to, and it's easier to enforce no updates. – Thomas Eyde Oct 11 '15 at 22:12
-1

You can query the data you need in your application service. This seems to be similar to your first solution.

Usually, you keep your aggregates cross-referenced, so I am not quite sure where the first issue comes from. Each user should have a list of teams it belongs to and each group has the list of users. You can complement this data with any attributes you want, including, for example, availability. So, when you read your aggregate, you have the data directly available. Surely, you will have lots of data duplication, but this is very common.

In the event sourced model never domain repositories are able to provide any querying ability. AggregateSource by Yves Reynhout is a good reference, here is the IRepository interface there. You can easily see there is no "Query" method in this interface whatsoever.

There is also a similar question Domain queries in CQRS

Community
  • 1
  • 1
Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83
  • -1 You are making a mess out of the concepts. The command handler shouldn't have access to a read model, simply because the read model is not its concern and the read model can be generated _async_ thus the handler would work with an inconsistent model. An aggregate root doesn't query anything, the repository does. ES means expressing an object state as a stream of events, it's an _implementation_ detail, it doesn't have any saying in defining the interface of a repository. Lastly, you're confusing the UI/report query with Domain queries, which are a different matter. – MikeSW Jun 20 '15 at 14:21
  • I did not write that aggregate queries the read model. Since you are so sure, please show me a repository with query at https://github.com/gregoryyoung/m-r/tree/master/SimpleCQRS. The question was about CQRS + ES. How comes that we are now abstracting from the *implementation details* if this was directly in the question asked??? In an event sourced write model, no queries are possible, this is a clear statement. – Alexey Zimarev Jun 20 '15 at 19:04
  • I agree about querying from the command handler. This was my mistake, I changed the answer accordingly. But I really appreciate any reference to a term "domain query" from Greg Young or Eric Evans, thank you in advance. – Alexey Zimarev Jun 20 '15 at 19:20
  • You realize you're pointing to a simple example which doesn't need more than "Get"? Are all the domain and requirements identical? No. For many devs ES and CQRS go together, this is a CQRS question and a design one never the less. A repository interface never cares you're using ES or whatever. One or more DDD repos are used by a service which implements a (domain) use case. In some cases you need to get all relevant aggregates according to a criteria from the repository. That's your domain query. Feel free to come up with a better term for it. One approved by Greg Y. or Eric E. – MikeSW Jun 20 '15 at 20:32
  • Mike, I have read your post you were referring to in another SO question. I completely agreed with the statement about the domain separation from data model and that many EF-related DDD tutorials tightly bind them together and this is not good. However, many of your statements are a long subject for discussion and there is no black and white there. Repository pattern is one of the most subjective topis out there. Your vision more corresponds with Fowler's repository that has little to do with DDD. This is my final post in this discussion. – Alexey Zimarev Jun 21 '15 at 08:04
  • Just to be clear, when i say querying read model, i'm not saying my repositories offer a link querying ability. If you check the Conference management example by MS, you'll notice they're querying the read model, through a service, to determine the total price of the order. I'm asking about that scenario and whether it's an abuse to use it or perfectly legit! – hsen Jun 24 '15 at 09:08