Transactions are very dependent on the use case. While there is no standard architecture, putting transaction logic in the layer of the application that has the most knowledge about the use case will give you the fewest headaches.
The advantage of placing transaction logic in the DAO is that you hide these data access details from the rest of the application. The downside is that your DAO is not very knowledgeable about the use case, so it has a hard time understanding when to start, commit, or roll back transactions. The DAO should provide general purpose data access logic that could be reused in multiple use cases.
With a service layer, I expect controllers to handle the semantics of the HTTP request-response lifecycle along with knowing which use case should be invoked. That leaves the service layer to handle business logic, coordination logic, validation, and transaction management. The trick is to create an abstraction to hide the specifics of your ORM, yet still acknowledge that a transaction is necessary.
While the following seems more prevalent in C#, the Unit of Work design pattern might be a good fit.
A unit of work is a behavioral pattern in software development. Martin Fowler has defined it as everything one does during a business transaction which can affect the database. When the unit of work is finished, it will provide everything that needs to be done to change the database as a result of the work.
Source: Wikipedia
If you research the unit of work pattern, don't get too hung up on the repository pattern. You will see a lot of references to that. Simply replace "repository" with "Data Access Object" and you should be fine. The important bits are:
- Define an interface for a specific unit of work.
- The interface should define methods to commit or roll back the transaction.
- The interface should not require types from your data access layer, if you desire strict decoupling of those concerns.
- Include all data access methods necessary to complete a particular use case.
Internally, the unit of work can reference any data access objects it needs. Don't think of this as a one-to-one mapping of DAOs with additional "commit" and "rollback" methods. The DAOs are an implementation detail of the unit of work that doesn't need to be exposed to the rest of the application. And don't get too hung up on naming things, either. I've seen unit of work implementations that dance around "commit" and "rollback" using methods named like "saveChanges" or "discardChanges". Relational databases don't have ownership over simple and concise terms like commit and rollback. The main point is to decouple data access from use case logic, rather than pretending a database doesn't exist.