Card 01/ 07
@Transactional: What It Wraps and How
A transaction opens when the method is entered and ends when it returns or throws, through a proxy.
A database transaction groups several changes so that they are all saved together or none of them is. @Transactional on a Spring bean's method opens a transaction when the method is entered and ends it when the method finishes: it commits (saves everything) if the method returns normally, and rolls back (undoes everything) if the method throws an unchecked exception.
Booking a ride saves the ride and then charges the rider. If the charge fails with an unchecked exception, the ride should not stay booked:
@Transactional
void bookRide(RideRequest request) {
ride = repo.save(new Ride(request));
paymentService.charge(request.riderId(), fare);
// if charge() throws, the save() above rolls back too
}The boundary is the method, and only while it runs: not the HTTP request that triggered it, and not anything the caller does afterwards. If the method is called while a transaction is already open, it joins that transaction by default instead of starting a second one.
It works through a proxy: Spring hands other beans a stand-in object that wraps the real bean. Every call to bookRide from another bean arrives at the proxy first, which opens the transaction, calls the real method, and then commits or rolls back depending on how the method finished.
So @Transactional changes what happens around a call, not what the method itself does. Both surprises about it come from that: how the proxy treats different exceptions, and calls that never reach the proxy.
← → move through sections and cards