I believe I know the answer to this but I’m looking for any holes or anything I may be missing.
This is focused on Spring and Java but could really apply to any programming stack.
Anyway, we have a typical service layer annotated with @Transactional
in Spring. For example, we could have:
EmailService
OrderService
HistoryService
Now, the user performs an action that calls a RESTful
service that does the following:
orderService.create(....); // wrapped in @Transactional
historyService.create(....); // wrapped in @Transactional
emailService.emailUser(....); // wrapped in @Transactional (also annotated with @Async)
While all of these are called from a controller (which is NOT @Transactional), if either the order service or the history service fail, I want both to be rolled back and the email service would abort.
I don’t like mixing the services. I think it would be ugly to have the order service call the history service just so that both of them are in the same transaction boundaries.
My first instinct was to create a hybrid service that would wrap both services together. Something like:
@Transactional
public void orderEntryService.create(....) {
orderService.create(....); // STILL wrapped in @Transactional
historyService.create(....); // STILL wrapped in @Transactional
}
This way, my controller could be:
public String create(...) {
orderEntryService.create(...);
emailService.emailUser(...); // this is @Async
// and will never be called if previous
// orderEntryService.create fails
}
While I think this keeps my service layer cleaner, it can quickly start adding up to bulky and forgetful “aggregate” service classes. How to handle this?
I understand your doubts, because such service-mixins don’t look natural for me either.
But why? Strictly speaking, even in complete SOA architecture service composition is not forbidden. But your case is different. Your services are not independent units located in own processes with independent transaction management. Your services are just level in your single-process architecture, plus methods of your services are wrapped in database-transactions. So, service composition could lead to nested transactions which itself is not a problem for Spring, but still looks unnatural for me. Plus, such composition could lead to circular dependencies between your services which is evil without a doubt.
You could solve your problem by adding one more level(let’s say DataAccess) in your architecture, which will contain repositories: EmailRepository, OrderRepository, HistoryRepository. Here you will have methods for managing emails, orders, history etc: adding, updating, deleting, querying. And you could share these repositories in your different services, having transaction wrappers where they are now – around your services.
It’s just the simplest solution that could help you, not talking about more sophisticated approaches such as DDD.
2
I do not have a definitive answer but here are my points:
In favor:
- I do not like business logic in the controller. They already have a lot of boilerplate code related to the view, format, transformations, bindings, mapping, etc. Three calls with its own error handling (and may be some specials cases) can become complex enough to bring bugs. The logic of how to create an order could be complex enough that that you want it separated from how do you call it.
-
Controllers are hard to test. Services are easy. You avoid mocking the WebApplicationContext, the json de/serialization, the controller error handling, SpringSecurityFilterChain, etc. You may even have to mock authorization. Of course you will try to test the controller anyway but those are heavier test that focus on the communication between server and client. With locale and input santiation they have enough complexity.
-
Task that need to be executed transactionally. Lets say that you need to execute two services: shipGoods and chargeCreditCard. shipGoods executes sucessfully and then chargeCreditCard fails. You would like to revert the shipping order. Alternative: You could create a “component” level between DAO and Service when only full business logic is called in services. And you do not want transactionall DAOs.
-
Allow smaller transactions when you do not need a big one. In the opossite case you may be calling a lot of functionallity like search services or webservices calls. If you do not need to rollback a transaction it is better to commit it as soon as possible. Open transactions can lock database records (or even full tables) reducing performance. Example: update entity in database, call slow remote webservice add create a different entity. In a big transation approach the entity won’t be committed until the last creation ends and nobody will be able to update the original record.
-
Code reuse: It is hard to reuse code without services calling another services. Lets say you have a very tested “chargeCreditCard” method. You may want to reuse it even in cases where “shipGoods” is changed to “preorderItem”. If you consider sending an email a service (and it seems so judging by emailService) then it seems that “forgottenPasswordService”, “newsLetterService”, “shippingService” could really benefit from reusing the funtionality.
Against:
- Complex transactions: if you create a big transaction that includes the three services you will have to think about what happens when each one fails. Nothings forbids the creation of a non-transactional service that calls transactional ones. That is the easy case. Nested transactions and autonomous transactions can be powerfull but hard and dangerous.
- Complexity: You can end with lots of services calling services calling services. Do you really have a business logic so complex? Beware of creating services like transactional DAOs with only CRUD operations but hardly any logic: For example: OrderService, HistoryService. In this case you can consider that OrderService should call historyDAO and include histories automatically when you create an order. (Avoid the Anemic Domain Model Antipattern).