2

This is something that has been bothering me for quite some time, I tried different approaches over the past year but I always come back to reflect on this, so I'll throw myself out here !

So, first get some common ground: if we follow the blue book, transaction handling should be done in the application service. We open a transaction, retrieve 1 or multiple aggregates, only adapt 1 aggregate and commit that transaction. ( 1 aggregate per transaction ).

If we then follow the theory of domain events, it seems that we have 2 camps, the one publishing events before commiting, the one publishing events after commiting. I'm fine with the second one, so let's stay in that context.

Ok I had different approaches on this one, I'll explain them so you can judge by yourself or maybe find other alternatives.

I'm using .NET btw.

So my first attempt was quite easy, and is the one I'm still using now.

I have a generic repository with 2 methods, like this:

public interface IRepository<TId, T> where T : Aggregate
{
    T GetById(TId id);
    Save(T aggregate);
}

And in my save method I do a couple of things:

  • Check if the entity already exists, if not, add it, if it does exist, attach it back to the change tracker ( using E.F. Core for now, don't focus on this part, it's not important )
  • As we have only 1 aggregate per transaction, I decided to call my SaveChanges(); method, the one that commits the changes to the database if we don't have a parent scope open, in the Save method of my repository. So I'm basically committing in my repository.
  • My aggregate base class holds a list of events that have occured from the moment we get it to the moment we save it. So after my save changes in this method, I basically retrieve the unpublished events of my aggregate, dispatch them, and clear the list.

As you can see, the Save method does quite a few things, maybe some considered outside of it's responsability. But it has been quite a straight forward experience using this method, as my application services don't really have transactions involved anymore, and each save commits a transaction.

The second approach for me consisted of overriding the .SaveChanges method of entity framework. I would basically retrieve the list of tracked aggregates, retrieve the events on them. Then call the base.SaveChanges() method that will actually commit, and then dispatch the events.

The repository save method in this case would only attach the entity to the change tracker when saving it ( I tend to retriev emy entities without change tracking and only track the changes on the .Save part, as it is possible to retrieve multiple entities in the same method for information purposes, but only 1 of them will be changed ).

What I then do is wrap my DbContext into a IUnitOfWork interface, like this

public interface IUnitOfWork
{
    void Commit();
}

And the commit method calls the context.SaveChanges(); method, which has been overriden to dispatch events after the commit.

My application service would then inject the repositories needed, call the .Save() method on one of them, and the commit the uow who is also injected.

The problem I have with those 2 is in the context of E.F., they are not always 100% correct for what I want to do.

If I open a transaction scope in my application service, both of those methods will actually publish events when saving on the repo / committing in the UoW, but if I rollback the parent transaction scope, I will have an invalid state.

My ultimate goal would be to always use a transaction scope in my application service, using that transaction scope's commit to actually commit the changes instead of the context's .SaveChanges() method, and only dispatch the events after the transaction scope is committed.

Now, my question(s) is(are): does the 2 first approaches seem correct to you, and am I to worried about small details ? Is my third idea actually achievable in a correct way ? Do I maybe miss an extra alternative ?

Thanks you !

1

Have you considered to use the Outbox pattern? Saving the domain events into the database as well and dispatch them from the database (via a separate worker or by an out-of-the-box database-messaging platform connector).

With this approach the database and the events will be consistent, because in case of rollback the events would not be persisted into the outbox. The second advantage is that you can retry publishing the event if the messaging platform is temporarily unavailable, so there won't be lost messages.

For further details please check these articles: 1, 2, 3

| improve this answer | |

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.