Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Managing transactions in service layer + Clean Architecture/DDD principles

I know there are pros and cons when it comes to managing transactions in the service layer or repository layer. Some people will say to manage them in the service layer, as managing transactions in the repository layer could lead to multiple business rules coupled to the repository layer. On the other hand, managing the transaction in the repository layer keeps the service layer agnostic to which database is being used (please correct me if I'm mistaken). I'm tempted to manage them in the service layer, as I could, for example, roll back if my mailer service fails.

When managing transactions in the service layer, I have to find a way to propagate the transaction over the repositories being used by my service, and that's the part where I'm struggling. Clean Architecture + DDD principles say to create a repository interface that is agnostic to the database being used, but if I need to propagate the transactions, wouldn't my method signature in the interface need to receive the transaction as a parameter? If so, I would violate the principles of CA+DDD, as my repository interface should be agnostic, and now I'm referencing the database in my repository method.

When managing transactions in the repository layer, things are simpler, as I could have an interface like this:

type AddItem interface {
    Handle(ctx context.Context, item *domain.Item) (id uint, err error)
}

My AddItem interface hides which database I'm using, and the implementation is much simpler. However, I could not roll back if a service mailer fails.

If I add the transaction in the signature like this, I would reference the database in my domain layer:

type AddItem interface {
    Handle(ctx context.Context, tx *gorm.DB, item *domain.Item) (id uint, err error)
}

So the question is: how to properly manage transactions in the service layer without violating the CA+DDD principles? Feel free to write examples (if necessary) by using any programming language of your choice. As reference, I'm using GoLang.

like image 389
Christian Gama Avatar asked Sep 02 '25 02:09

Christian Gama


2 Answers

There are different ways to do it. Some depent on language features. E.g. In Java you would usually use a ThreadLocal to propagate the transaction.

A general way is to create a stateful repository. This means that the repository will take the transaction on construction and is bound to this transaction.

As a result the use case will also be bound to the transaction since it uses the bound repository. But this means that you can not use a singleton use case anymore. You must create one for each transaction.

I would use a template method pattern:

 public class TransactionAwareAddItemUseCase implements AddItemUseCase {


      public ResponseModel execute(RequestModel request){
           Transaction tx = beginTransaction();

           try {
              AddItemRepository repo = new AddItemRepository(tx);
              AddItemUseCase useCase = new AddItemUseCase(repo);
              return useCase.execute(request);
              tx.commit();
           } catch(Exception e){
              tx.rollback();
           }
      }

 }

Maybe you find a way to make it more generic so that you do not have to write an own template method for each use case.

But like I said before this is a common solution and there are usually much better ones depending on the language you use.

like image 53
René Link Avatar answered Sep 04 '25 22:09

René Link


I've done this in the past by abstracting the transaction and hiding it behind an interface that utilizes the saga pattern. The repos can add their connections/transactions to the saga, non-repos can create their own rollback strategies, and all of it can be hidden from the service layer, who can invoke the saga.commit() / rollback which in turn invokes all the database and non-database 2-phase commits/rollbacks.

like image 39
Rob Conklin Avatar answered Sep 04 '25 22:09

Rob Conklin