Thursday 1 March 2012

Spring and Transaction Strategies

When it comes to transactions, the Spring manual suggests that marking your classes as @Transactional and enabling annotation-driven configuration is somewhat insufficient. When you keep reading it though, you pick up many interesting details but won't get  much further in terms of best practices. At the end of the day, you stick to the declarative approach, write a bunch of unit tests and there you go, the default settings work just fine so what's the matter?


Well, it all goes fine until the point your project complexity grows large enough for you to come across obscure data inconsistency issues. Your persistence code tends to misbehave and maintaining the key unit tests becomes a challenge due to unpredictable test failures.

To reclaim control over the unwieldy components you should take a closer look at transaction ownership in your data manipulation code. Understanding when a new transaction needs to be invoked and when it is better to join an existing one is key to success.

That would be it for theory, let's get some hands-on experience. Here is a simplistic demo on two major transactional strategies, the Client and the Domain Model Owner Pattern.

The example is an utterly simplified flight booking application. Domain model:
...
import javax.persistence.*;
...
@Entity
@Table(name="flights")
public class Flight {
   
    @Id
    private Long id;
    
    @Column(nullable=false)
    private String destination;

    @OneToMany(mappedBy="flight")
    private List bookings;
    ...
}

@Entity
@Table(name="bookings")
public class Booking {
    
    @Id
    private Long id;
    
    @Column(nullable=false)
    private String email;
    
    private boolean paid;
    
    @ManyToOne
    private Flight flight;
    ...
}
It comes as no surprise that a DAO layer provides elementary persistence operations:
public interface BookingDAO {
      
    Booking find(Long id);
    
    Flight findFlight(Long id);
    
    Long save(Booking booking);     
}
To make things more interesting, payments are processed separately:
public interface PaymentService {
    void processPayment(Long bookingId, String cardNumber) 
                     throws PaymentFailedException;    
}
Far-fetched as it is, should a payment fail the whole booking transaction has to be rolled back. Let's assume for now that there is no coarse-grained booking service available. Any client looking to book a flight would have to manage bookings on its own using the available DAO:
@Service("bookingDAOClient")
public class BookingDAOClient implements BookingClient {
    
    @Resource
    private BookingDAO dao;

    @Resource
    private PaymentService paymentService;
    
    @Override
    @Transactional(
       propagation= Propagation.REQUIRES_NEW,
       rollbackFor=PaymentFailedException.class)
    public Long bookFlight(Long id, String email, String cardNumber) 
                                      throws PaymentFailedException {       
        Booking booking = new Booking(email);
        booking.setFlight(dao.findFlight(id));
        Long bookingId = dao.save(booking);
        paymentService.processPayment(bookingId, cardNumber);
        
        return bookingId;        
    }   
}
Since the client takes ownership of the booking procedure, it is directly responsible for establishing a new transaction, hence the REQUIRES_NEW propagation level. The rollbackFor attribute mandates the transaction be rolled back should the payment fail. Spring guarantees automatic rollback for unchecked (runtime) exceptions which is not our case:
public class PaymentFailedException extends Exception {}
Looking at the code reveals that a booking is saved first, and subsequently a payment attempt is made using the booking ID and a credit card number. If all goes fine, the transaction is committed, otherwise it is fully rolled back and no new booking is made.

That would be it for the client part of the Client Model Owner Pattern. The service the client relies on (BookingDAO) assumes a transaction is already in place and makes most of it:
@Repository
@Transactional(propagation= Propagation.MANDATORY)
public class BookingDAOImpl implements BookingDAO {

    @Override
    @Transactional(propagation= Propagation.SUPPORTS, readOnly=true)
    public Booking find(Long id) {...}

    @Override
    @Transactional(propagation= Propagation.SUPPORTS, readOnly=true)
    public Flight findFlight(Long id) {...}
    
    @Override
    public Long save(Booking booking) {...}
}
As you can see the default MANDATORY propagation level is applied when a booking is being saved. There is no point in doing so if no transaction is around. On the other hand, read operations are perfectly fine even outside of a transaction scope.

Let's move on to the Domain Model Owner Pattern and start off with creating a coarse-grained service:
@Service
@Transactional(
  propagation= Propagation.REQUIRES_NEW,
  rollbackFor=PaymentFailedException.class)
public class BookingServiceImpl implements BookingService {
    
    @Resource
    private BookingDAO dao;

    @Resource
    private PaymentService paymentService;
    
    @Override
    public Long bookFlight(Long id, String email, String cardNumber)
                                      throws PaymentFailedException {
        Booking booking = new Booking(email);
        booking.setFlight(dao.findFlight(id));
        Long bookingId = dao.save(booking);
        paymentService.processPayment(bookingId, cardNumber);
        return bookingId;              
    }
}
The approach is not dissimilar from the previous example. Since the transaction ownership has been shifted to a dedicated service the actual client is no longer concerned about transactions:
@Service("bookingServiceClient")
public class BookingServiceClient implements BookingClient {

    @Resource
    private BookingService service;
    
    @Override
    public Long bookFlight(Long id, String email, String cardNumber)
                                      throws PaymentFailedException {
        return service.bookFlight(id, email, cardNumber);
    }   
}
The last point I want to make refers to audit logging. What makes it special is the need for an audit log be created under absolutely any circumstances. We want to be aware of all bookings made, even of those which were eventually rolled back. Let's enhance the BookingDAO with a new method:
@Override
@Transactional(propagation= Propagation.REQUIRES_NEW)
public void addAuditLog(Booking booking);
The method runs in an independent transaction and remains therefore unaffected by rollbacks in the existing code:
...
Long bookingId = dao.save(booking);         
dao.addAuditLog(booking);  // Rollback has no effect
paymentService.processPayment(bookingId, cardNumber);
...  
That last remark concludes the example of an unreal booking application. I merely scratched the surface, there is much more to transaction management. I hope you enjoyed the post nevertheless.

Download Source Code

Resources:

No comments:

Post a Comment