1

Let's say I am designing a TODO application and therefore have an aggregate root called Task. The business requires to keep a list of TaskLogEvent that provides them with a history of how the task changed over time. As the Task may have hundreds of these events, I will model this TaskLogEvent as a separate aggregate root (I do not want to load these elements every time and I am not using any lazy loading mechanism).

Now anytime I want to do task.complete() or any other opration modifying the Task I want to create a new TaskLogEvent. I have these options:

  1. Design a domain service that will make all changes to Task and create the event. Every comunication with Task would have to get through this service.
  2. Pass TaskLogEventRepository to any method in Task so that the Task itself can create the Event and save it into the repository.
  3. Let an application service handle this. I don't think that is a good idea.

What would be the ideal solution to this situation? Where did I make mistake in my thinking process?

  • If you don't want to pollute Task with this other responsibility, you could use a decorator (in some sense similar to your option 1, but more transparent to client code). – Filip Milovanović Jul 19 at 19:02
  • I actually think Task should be the one responsible for creating the events as these Events would have no meaning without the Task itself. The only reason they are separate aggregate roots is that it would be inefficient to load every event any time I want to work with Task. So the option 2 is OK, in your opinion? – Juraj Mlich Jul 19 at 19:04
  • 1
    Another discussion that talks about my problem, if anybody is looking for more resources: stackoverflow.com/questions/1488739/… – Juraj Mlich Jul 20 at 12:03
1

Your reasoning on creating the TaskLog as a separate aggregate is correct and makes sense.

The responsibility of raising an event lies with the business logic, so prima facie, Option 1 would be the best choice. You would come close to this code structure if you follow DDD anyway, with a Task Application Service, Task Aggregate, and a Task Repository.

But this is a starting point. You should not be dealing with the TaskLogRepository as part of your Task aggregate:

  • You should bubble the event up as a Domain Event (for example, a TaskUpdatedEvent) from the Task Aggregate, after a successful operation of creating/modifying the Task.
  • The event would typically contain a comprehensive payload so that subscribers would not have to query the Task aggregate again.
  • A subscriber would be present on the other side, waiting for this event. When the event bubbles up, the subscriber retrieves it and fires the associated Application Service. The service would then process the event payload and do whatever is necessary (In your case, the Task would be to persist the event to the TaskLogRepository).
  • If there are other parts of your application that need to act on these events, they would have subscribed to this event as well. Your Domain Event pub-sub mechanism will handle broadcasting and retrieval.

One step further: Prescriptive Events

You could use these Events as the mechanism to effect a change on the Task aggregate. Let me explain.

When you affect a change on the Task aggregate and then generate an event after the transaction, the event would be a Descriptive event, which has info on what just happened. With Descriptive events, there are two representations of each change, so they can potentially diverge.

Instead, Prescriptive events would be events that you trigger to effect the change on the Task. You would only generate the event as part of the transaction, and subscribers would be responsible for modifying the Task aggregate. There is just one representation in this case, and you have good sync between the event that was stored and the change that was affected.

Further Reading:


One step further: Event Sourcing

You could use Event Sourcing mechanism to build the Task aggregate in real-time from events. With this approach, you have a 100% sync between the history of task modifications and the Task's current representation.

Event Sourcing as a mechanism by itself will require some understanding and experimentation, so you need to make an informed call. Read about it, but do not implement it unless you discover an actual need for it.

Further Reading:

1

Creation patterns are weird

Notice that, in this use case, you are changing two things; the Task aggregate itself, and also your collection/repository/stream of TaskLogEvents.

Twenty years ago, when "the" database meant some RDBMS, both of these would be written as part of the same database transaction. It would be the responsibility of the application layer to manage the transaction itself. The logic for copying data from the Task to the TaskLogEvent would live in the aggregate. That data might just invoke the TaskLogEvent constructor, or it might use a factory. The application layer would query the aggregate for the event(s), and would be responsible for storing them in the "repository".

See, for instance Udi Dahan Reliable Messaging without Distributed Transactions, or Pat Helland Data on the Outside vs Data on the Inside.

If the aggregate and the events are being written to different places, then things get to be more complicated. First, you now have two transactions for the application service to manage, and you have to worry about the implications of a failure after the first transaction is committed. It's otherwise not too different from the first case; you have an in memory domain model that works in a universe where everything is easy, and the application coordinating a storage protocol under the guidance of the domain model.

If you've looked into Growing Object Oriented Sofware, Guided by Tests, this separation should be a familiar one; an in memory brain that knows what to do talking to a bunch of dependencies that know how to do it.

Of your three listed choices, C is by far the easiest to work with over time, because you get a cleaner separation of the parts. The domain model can easily be lifted into other environments (for instance, test) where the concept of "transaction" doesn't exist, none of the I/O side effects pass through the model code, and so on.

But it's not nearly as seductive as the illusion that you can do everything in memory, and treat persistence as an after thought.

1

While it's difficult to surmise exactly what an "ideal" solution would look like without a more comprehensive understanding of your system and its goals, I'm going to argue for none of the above.

You may have ruled out the simplest option (the one that creates the least amount of interleaving) without giving it enough consideration.

Keeping a single Task aggregate does not necessarily require loading every past TaskLogEvent each time a Task is constructed. For example, you could record and persist only newly created events each time you load, modify, and save a Task. This would result in a much simpler system than adding the requirements necessary to wire up domain events/handlers (which is a totally viable option).

I would further argue that TaskLogEvent is not a very good candidate for an entity at all (can it change?), but that is a separate discussion not entirely relevant to question at hand.

  • Hmm, that seems interesting. To make sure I truly understand what you're trying to say, you are proposing that the set/list/collection inside the Task aggregate root would be "write only", thus when loading the root using repository, the events would not be loaded, but during "save/persist", the events would be saved, right? ... The TaskLogEvent cannot change. Why do you think that the event is not a very good candidate? Thank you. – Juraj Mlich Jul 22 at 20:15
  • @JurajMlich Your understanding is correct. And since you asked... An object that cannot change would be better understood as a value object. Unless the the behavior of this object were somehow dependent on the value of its id (which would be a questionable practice at best), two instances made up of identical fields would certainly also exhibit identical behavior (even in when its execution is happening in threads at different times). – king-side-slide Jul 27 at 0:46
  • @JurajMlich This is because any query, issued at any time for this object, issued over the entire lifetime of the the system, will always return the same data set. The above also illustrates that the id is unnecessary except as an artifact to allow for optimization of storage and retrieval. – king-side-slide Jul 27 at 0:50
  • @JurajMlich It may sound pedantic because, functionally speaking, the labeling of an immutable object as an “entity” instead of a “value object” sounds like semantics that make no difference. Conceptually though, it’s incredibly important to understand that this extra bit of knowledge about an object (that it cannot change), can yield seriously impactful simplifications in terms of a system’s design. Value objects allows for other objects to hold in memory dependencies to them (and their data) with certainty that invariants cannot change. – king-side-slide Jul 27 at 1:16
  • @JurajMlich As a result value objects are suitable to be passed into constructors (where entities are not) and can therefore safely “cross” consistency boundaries. More generally, the set of all value objects within a system could be understood as the set of all possible configurations of that system (including, and in all likelihood, invalid configurations). – king-side-slide Jul 27 at 1:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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