Transactional Basics

All or Nothing

A transaction groups several database changes so they either all succeed (commit) or none do (rollback). Placing an order must save the order, save its items and reduce stock together. If the payment step fails halfway, no half-order may stay in the database.

  • Atomicity: all or nothing.
  • Consistency: constraints hold before and after.
  • Isolation: concurrent transactions do not see each other's half-done work (to a chosen degree).
  • Durability: once committed, the data survives a crash.

@Transactional on a Service Method

import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orders;
    private final ProductRepository products;

    @Transactional
    public Order placeOrder(Long userId, Map<Long, Integer> cart) {
        Order order = new Order(userRef(userId));
        cart.forEach((productId, qty) -> {
            Product p = products.findById(productId).orElseThrow();
            if (p.getStock() < qty) throw new OutOfStockException(productId);   // rollback of everything
            p.setStock(p.getStock() - qty);                                      // dirty checking
            order.addItem(new OrderItem(p, qty, p.getPrice()));
        });
        return orders.save(order);
    }   // commit here: flush -> INSERTs and UPDATEs -> COMMIT
}

Example

Turn on transaction logging to see where transactions start and end:

application.properties

logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG

Example output

DEBUG o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.shopease.order.OrderService.placeOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(1730418221<open>)] for JPA transaction
DEBUG org.hibernate.SQL : select p1_0.id,p1_0.category_id,p1_0.description,p1_0.name,p1_0.price,p1_0.stock from products p1_0 where p1_0.id=?
DEBUG o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
DEBUG org.hibernate.SQL : insert into orders (status,total,user_id) values (?,?,?)
DEBUG org.hibernate.SQL : insert into order_items (order_id,price,product_id,quantity) values (?,?,?,?)
DEBUG o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
DEBUG org.hibernate.SQL : update products set category_id=?,description=?,name=?,price=?,stock=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1730418221<open>)] after transaction

"Participating in existing transaction" is the repository call (save is itself @Transactional) joining the service transaction.

How It Works: a Proxy

Spring wraps your bean in a proxy. The proxy starts the transaction, calls your method, then commits or rolls back. This has two big consequences: the annotation works only on public methods called from another bean, and a call from one method to another inside the same class (this.method()) skips the proxy.

Example

A JDK dynamic proxy plays the role of Spring's transaction proxy. Watch what happens to the inner call:

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        OrderService target = new OrderServiceImpl();
        OrderService proxy = (OrderService) Proxy.newProxyInstance(
                Main.class.getClassLoader(), new Class<?>[] {OrderService.class},
                (p, method, a) -> {
                    System.out.println("[proxy] begin transaction for " + method.getName());
                    Object result = method.invoke(target, a);
                    System.out.println("[proxy] commit transaction for " + method.getName());
                    return result;
                });

        System.out.println("1) controller calls proxy.placeOrder():");
        proxy.placeOrder();
        System.out.println();
        System.out.println("2) controller calls proxy.saveAudit():");
        proxy.saveAudit();
    }
}

interface OrderService {
    void placeOrder();
    void saveAudit();
}

class OrderServiceImpl implements OrderService {
    public void placeOrder() {
        System.out.println("   placeOrder body");
        this.saveAudit();          // self-invocation: goes to the real object, NOT through the proxy
    }
    public void saveAudit() {
        System.out.println("   saveAudit body");
    }
}

Output

1) controller calls proxy.placeOrder():
[proxy] begin transaction for placeOrder
   placeOrder body
   saveAudit body
[proxy] commit transaction for placeOrder

2) controller calls proxy.saveAudit():
[proxy] begin transaction for saveAudit
   saveAudit body
[proxy] commit transaction for saveAudit

Note: Because of this, putting @Transactional(propagation = REQUIRES_NEW) on saveAudit() and calling it from placeOrder() in the same class does nothing. Move saveAudit() into another bean (e.g. AuditService).

Where to Put @Transactional

  • On service methods (a use case), not on controllers and usually not on every repository.
  • Use org.springframework.transaction.annotation.Transactional. jakarta.transaction.Transactional also works, but has no readOnly, isolation or timeout.
  • On a class it applies to all public methods; a method-level annotation overrides it.
  • Keep transactions short: no HTTP calls, emails or file uploads while holding a DB connection.