Wednesday, December 20, 2006

Transactions and exceptions revisited

As Jonas (Hello, world!) embarrasingly enough pointed out, the default Spring transaction semantics with respect to exceptions are identical with EJB's (you can't escape EJB!). So that's why Spring doesn't roll back on checked exceptions, but the question remains: why would anyone ever not want to roll back a transaction when an exception is thrown, checked or unchecked?

We had a long discussion which I'm going to try and sum up here. Obviously you (almost) never want an exception to leave the transactional layer without rolling back the transaction, since that means that something went wrong, but the transaction commited anyway. Suppose you have something like this:

@Transactional
public void registerNewUser(User user) throws LDAPException {
log.info("Registering user: " + user);
userDao.insertUser(user);
ldapService.registerEntry(user.getFirstName(), user.getLastName()); // Let's assume this throws a (checked) LDAPException
}

in the service layer, and then you call this method from outside the service layer, for example in an MVC controller. Now if the call to create the LDAP entry fails (throws a checked exception), the user data is stored even though an exception is thrown out to the calling controller, which can't do anything about it.

This scenario could be handled in two different ways, depending on business requirements: either a failed LDAP registration is fatal, and we need to roll back the transaction to avoid storing user data, or an LDAP exception is not fatal and we should catch and gracefully handle the failure inside the registerNewUser() service method.

If we decide to roll back, we have a few options:
So far it seems that simplifying the rollback rules to "always roll back on an Exception, I don't care if it's checked" is a good idea. But if it's the case that an LDAPException isn't fatal after all, the situation becomes a little more complicated. We modify the method like this:

@Transactional
public void registerNewUser(User user) {
log.info("Registering user: " + user);
userDao.insertUser(user);
try {
ldapService.registerEntry(user.getFirstName(), user.getLastName()); // Let's assume this throws a (checked) LDAPException
} catch (LDAPException e) {
// Log and move on
log.error("LDAP registration failed: " + e.getMessage());
}
}

No checked exception is thrown anymore. Furthermore, assume that the LDAP service call is also transactional, and that we're using the default propagation behaviour, REQUIRED, so that the LDAP service call participates in the same transaction as the registerNewUser() started. If the rule is to always roll back on any transaction, the exception thrown in the LDAP service call will mark the current transaction for rollback, even though we catch it in the user service method! That means that we have lost the ability to pass on failure messages between horizontal transactional calls (service-to-service).

So to conclude:
  1. We need to keep a way to pass on failure messages between calls inside the same transaction, and checked exceptions are a good candidate for that (certainly better than returning a negative integer).
  2. You should not expose service methods that throw checked exceptions to layers above/outside the transactional layer.

1 comment:

silpa said...

Is there any way by which we can specify transactions shouldn't be rolled back on certain runtime exceptions