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?
... 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; ... }
public interface BookingDAO { Booking find(Long id); Flight findFlight(Long id); Long save(Booking booking); }
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.
Resources: