I'm developing a backend where controllers call services, and each service is responsible for handling an entity(table) in SQL. Now, what if I want to use a transaction, basically roll back previous service calls if any of them fail? What would be the right approach? The problem is that sql code is down below in each service. If you have ever written Onion architecture for a backend, sure thing you know what I mean.
I use Adonisjs + lucid form. But it does not matter for a question.
A controller is calling many services. This means that no single service is responsible for managing a transaction.
Rather, a transaction is an application layer (or a use case) concern. It is started before calling any of the services and then passed to them.
You have two ways to pass transaction to a service. Either in a constructor, or as method argument. I believe, passing as a method argument is fine, since a transaction is a perfect business concept. Not a technical SQL transaction, of course, but a business transaction. Which may encapsulate SQL transaction.
To sum it up:
A controller parses and validates request, instantiates app layer (or a use case) and calls it. Gets back response and renders HTTP response.
App layer starts and commits (or rolls back) a transaction and passes it around to services. Services use the transaction to call repositories.
In general this approach is language-agnostic, although different languages may bring their specifics to the implementation.