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=DEBUGExample 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.Transactionalalso works, but has noreadOnly,isolationortimeout. - 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.