Struggling with defining aggregates for my domain

Hello!

In my company we are experimenting, for the first time, DDD, CQRS and Event Sourcing with Axon Framework and Axon Server. We are struggling with coming up with a “correct” model for our use case, so I am here to ask for your help.

We want to integrate our financial domain (CRM) with our productivity domain (projects, timesheets, invoices related to those timesheets). The flow is:

  1. We have a Spring Boot service that periodically calls the external APIs of both domains to keep local copies of their data. For example, each day, we make API calls to download our Projects, together with all the related data (Sections, Tasks, Timesheets, Invoices). Same thing happens for the PurchaseOrders and WorkOrders from our CRM. This service would send commands such as Create/Update/Delete for each one of those entities.

  2. Our finance officer now wants to assign each WorkOrder to a PurchaseOrder (a PurchaseOrder can have multiple WorkOrder). Each WorkOrder and PurchaseOrder has a monetary value, and this assignment allows to consume the value of the PurchaseOrder through the WorkOrders.

  3. Now, our finance officer wants to create partitions of each WorkOrder, called WorkOrderItems. This mirrors the logic of the previous point.

  4. The other important thing that our finance officer does is going through the list of Invoices, and for each invoice, which has a monetary value, he consumes this value by splitting it between several WorkOrderItems. So, an Invoice can be split between several WorkOrderItems, and a WorkOrderItem can be assigned to several Invoices.

  5. Finally, to put it all together: the finance officer creates an entity called Budget, which acts like a container and has a monetary value. You can see it as a budget for a big project. He can assign several Projects and several WorkOrderItems to a single budget (but a Project and a WorkOrderItem can only be assigned to a single budget). The idea is that two Projects could refer to the same PurchaseOrder (it happens that we have a Project for each year, so if a bigger work spans over two years, we have two Projects that should consume the same budget).

With all these connections, he is now able to see, for a budget, how much money was invoiced, how much time is used by the personnel (by looking at the Timesheets of the Projects) and so on.

You can see how all entities are related to each other, in a way or the other. I have made this sketch for the model, but I am not convinced.

aggregati-Pagina-3.drawio

I have modeled the relationships by deciding a single owner. For example, the WorkOrderItem would receive and handle the event EventWorkOrderItemIntoWorkOrderIncluded, which would assign the WorkOrder id in its state. Same thing with EventWorkOrderIntoPurchaseOrderIncluded handled by the WorkOrder aggregate. Like this, I can verify that each WorkOrder is included at most in one PurchaseOrder. What I cannot verify is, though, if a PurchaseOrder has any WorkOrders included before deleting it. In our case, this is not really a problem, since 1) we are not usually deleting work orders, and 2) even if we delete them, I suppose we can create a service on our query model that checks if there are dangling references, and send the proper commands to dereference them.

Is this a right way to model this domain? What do you think? I know this is a lot to ask, so thank you if you even managed to reach this point!

Bonus question about parallelization of event handlers. In our view model, with this kind of flow, we cannot parallelize the execution of event handlers (following a PerAggregatePolicy). For example, consider if we have two processing groups, one for the PurchaseOrder, and one for the WorkOrder. If the events are processed in this way: 1) EventWorkOrderCreated, 2) EventWorkOrderIntoPurchaseOrderIncluded, 3) EventPurchaseOrderCreated. If, in event 2, we want to update the view model of our PurchaseOrder to reflect this inclusion (i.e. PurchaseOrder.value -= WorkOrder.value), we cannot do this because the WorkOrder object is not yet created. The same thing happens with pretty much everything else, since we want to update some values of the container object once a child object is included into it. I cannot find a way to solve this, are we stuck with sequential execution? Or is this problem given by a bad modelling of our aggregates and events? Thank you!

Not sure this helps much, It probably is quite wrong and would need lots more questioning and discussion, but from you statements I would model it like so:

You will notice I mainly follow your aggregates but try to define explicit bounded contexts (and you will most likely find different ones or different names for them).
I am assuming that you are talking about a fullfilment process in general, so purchase orders and invoices would be exchanged with a customer rather than a supplier (again, I might be wrong). I have no idea what a Section is, so I omitt that one.

I would discuss if Invoice should really be in the productivity context rather than in finance, maybe in both with different meaning. I would also start poking around to figure wether Purchase Order and Workorder are modelled right. Do they belong in same context? Who creates them when ? Maybe they are both aggregates? I would also discuss if the finance officer is the actual one making the two assignments modelled and checking timesheets and tasks. In my experience, a project manager would have a much better understanding of these items ( if you have such roles, of course).

Once such a model starts to make sense for you and your stakeholders, you should try to find domain events within and spanning across the contexts.

P.S.:
The plantuml code is here (use a VS or IntelliJ plugin to reproduce the graphics):

@startuml
'https://plantuml.com/class-diagram

skinparam packageStyle rectangle
hide members

package productivity <<bounded context>> {

    class Project <<aggregate root>>
    class Timesheet <<entity>>
    class Task <<entity>>
    class Invoice <<entity>>

    Project *--- "*" Timesheet
    Project *--- "*" Task
    Project *--- "*" Invoice

}

package fullfilment <<bounded context>> {
    class PurchaseOrder <<aggregate root>>
    class WorkorderItem <<entity>>
    class Workorder <<entity>>

    Workorder *-- "*" WorkorderItem
    Workorder "*" --- "1" PurchaseOrder: assigned
    WorkorderItem "*" --> "1" PurchaseOrder: consumes
}

package finance <<bounded context>>{
    class Budget <<aggregate root>>

    Invoice "*" --- "*" WorkorderItem: assigned
}

Budget "1" --- "*" Project
Budget "1" --- "*" WorkorderItem
@enduml