1

I'm in a scenario where, when a certain command is applied on an aggregate, I have to

  1. change the state of the aggregate itself
  2. if ALL related aggregates are in the same state, then notify the world outside

here is an example (in pseudocode)

finishRace(raceId, carId)
    cars = repository.findCarsByRaceId(raceId)
    cars.get(carId).finishRace()

    repository.save(cars.get(carId))

    if(allCarsHaveFinished())
        publishEvent(RaceFinishedEvent())

It is clear to me that this logic belongs to a domain service, since it is related with multiple aggregates. Usually domain events represent a side effect of an operation of a single aggregate, but here the side effect has to be somehow "filtered" based on the state of other aggregates

Another (elegant) option could be to introduce a Race aggregate root and make single Cars as child entities, so that I can check invariants inside the Race

finishRace(raceId, carId)
    race = repository.findRaceById(raceId)
    race.finishForCar(carId)

    repository.save(race)

but it seems to not be a good option since

  1. in general, there can be a lot of cars in a single race, so the Race aggregate would be quite big
  2. in my specific case, a car can switch between different races So I should change two Races time everytime this event happens

So coming back, the simplest solution would be to publish an event from a domain service. Is this considered a good practice? there are hidden downsides that I'm not seeing?

Thanks for the answers!

1 Answer 1

1

In general, events should never be raised/created outside of the boundaries of an aggregate. Given this premise, your first piece of code, what actually does, is updating two different aggregates even though they are not "visible" (The publishEvent(RaceFinishedEvent()) what does is essentially doing a race aggregate operation)

Since in DDD updating two aggregates in a single transaction is discouraged, you could use what is called a Saga or Process Manager (either by orchestration or choreography, up to you, I'm gonna use choreography for the sake of simplicity).

What I would do is the following:

finishRace(raceId, carId)
    cars = repository.findCarsByRaceId(raceId)
    cars.get(carId).finishRace()

    repository.save(cars.get(carId))

Let's say that finishRace raises the event CarRaceHasFinished. Then you would have, in another place, an event handler that would listen to the CarRaceHasFinished event. This event handler would perform the next operation over the race aggregate:

finishRace(raceId, carId)
    race = repository.findRaceById(raceId)
    race.finishForCar(carId)

    repository.save(race)

Inside the race aggregate you won't have all the cars (maybe just the identifiers and that's it), and that last check for the allCarsHaveFinished would be executed inside the finishForCar method of the aggregate to assess whether the RaceFinishedEvent must be raised as well.

By doing it this way, you keep the aggregates small as well as respecting the rule for not updating two aggregates at the same time. It's more complex, but also more robust when doing it well. You will probably need some messaging infrastructure in place to be able to set up the publishing of messages and the asynchronous handling of events.

Vaughn Vernon Red book "Implementing Domain Driven Design" has a similar example like yours but using backlog items and a backlog task to track the completion of a backlog task once all backlog items are completed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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