Why Does Spring Boot Transaction Deadlock When Calling Another Service?
Problem
When I run my Spring Boot application that processes orders, I get this deadlock error:
2026-03-14 08:15:45,123 WARN [main] o.h.e.j.s.SqlExceptionHelper - SQL Error: 1213, SQLState: 400012026-03-14 08:15:45,456 ERROR [main] o.h.e.j.s.SqlExceptionHelper - Deadlock found when trying to get lock; try restarting transaction2026-03-14 08:15:46,789 ERROR [main] o.s.t.i.TransactionInterceptor - Application exception overridden by rollback exceptionorg.springframework.dao.DeadlockLoserDataAccessException: Deadlock found when trying to get lock; try restarting transactionThe application hangs for a while and then throws a deadlock exception.
Environment
- Spring Boot 3.2.0
- Java 21
- MySQL 8.0
- Spring Data JPA
What happened?
I was building an order processing system where OrderService calls ValidationService to validate orders. Hereβs my original code:
@Servicepublic class OrderService { private final OrderRepository orderRepository; private final ValidationService validationService;
public OrderService(OrderRepository orderRepository, ValidationService validationService) { this.orderRepository = orderRepository; this.validationService = validationService; }
@Transactional public void processOrder(Order order) { orderRepository.save(order); // Insert row - lock acquired validationService.validateOrder(order); // Calls another service order.setStatus("VALIDATED"); }}@Servicepublic class ValidationService { private final OrderRepository orderRepository;
public ValidationService(OrderRepository orderRepository) { this.orderRepository = orderRepository; }
@Transactional(propagation = Propagation.REQUIRES_NEW) public void validateOrder(Order order) { Order savedOrder = orderRepository.findById(order.getId()).orElse(null); // Validation logic... }}I can explain the key parts:
@TransactionalonOrderService.processOrder()starts Transaction 1orderRepository.save(order)inserts a row and acquires a row lockvalidationService.validateOrder(order)calls another service@Transactional(propagation = Propagation.REQUIRES_NEW)onValidationServicestarts a NEW transaction
The problem is that the new transaction cannot see the uncommitted row from Transaction 1, and it waits for the lock to be released. But Transaction 1 is waiting for Transaction 2 to complete. This creates a deadlock.
How to solve it?
I tried several approaches to fix this issue.
Approach 1: Use Default Propagation (REQUIRED)
The simplest fix is to remove the REQUIRES_NEW propagation:
@Servicepublic class ValidationService { private final OrderRepository orderRepository;
public ValidationService(OrderRepository orderRepository) { this.orderRepository = orderRepository; }
@Transactional // Default: REQUIRED - joins existing transaction public void validateOrder(Order order) { Order savedOrder = orderRepository.findById(order.getId()).orElse(null); // Validation logic... }}Now test again:
mvn spring-boot:run2026-03-14 08:25:15,123 INFO [main] o.s.b.SpringApplication - Started Application in 1.456 seconds2026-03-14 08:25:16,456 INFO [main] c.e.OrderService - Order processed successfullyYou can see that I succeeded to process the order without deadlock. The REQUIRED propagation (default) means the method joins the existing transaction instead of creating a new one.
Approach 2: Restructure Service Calls
If you really need a new transaction (for example, to commit validation separately), restructure the code:
@Servicepublic class OrderService { private final OrderRepository orderRepository; private final ValidationService validationService;
public OrderService(OrderRepository orderRepository, ValidationService validationService) { this.orderRepository = orderRepository; this.validationService = validationService; }
public void processOrder(Order order) { Long orderId = createOrder(order); // Transaction 1 commits here validationService.validateOrder(orderId); // Transaction 2 sees committed data }
@Transactional public Long createOrder(Order order) { orderRepository.save(order); return order.getId(); }}@Servicepublic class ValidationService { private final OrderRepository orderRepository;
public ValidationService(OrderRepository orderRepository) { this.orderRepository = orderRepository; }
@Transactional(propagation = Propagation.REQUIRES_NEW) public void validateOrder(Long orderId) { Order savedOrder = orderRepository.findById(orderId).orElse(null); // Validation logic... }}This approach commits Transaction 1 before starting Transaction 2, so the new transaction can see the committed data.
Approach 3: Use @TransactionalEventListener
For post-commit actions, use the event listener pattern:
public class OrderCreatedEvent { private final Long orderId;
public OrderCreatedEvent(Long orderId) { this.orderId = orderId; }
public Long getOrderId() { return orderId; }}@Servicepublic class OrderService { private final OrderRepository orderRepository; private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository, ApplicationEventPublisher eventPublisher) { this.orderRepository = orderRepository; this.eventPublisher = eventPublisher; }
@Transactional public void processOrder(Order order) { orderRepository.save(order); eventPublisher.publishEvent(new OrderCreatedEvent(order.getId())); order.setStatus("CREATED"); }}@Componentpublic class OrderEventListener { private final ValidationService validationService;
public OrderEventListener(ValidationService validationService) { this.validationService = validationService; }
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // Executes after transaction commits validationService.validateOrder(event.getOrderId()); }}The reason
I think the key reason for the deadlock is the transaction propagation behavior:
Transaction 1 (OrderService) Transaction 2 (ValidationService) | | | START TRANSACTION | | | | INSERT row (acquires row lock) | | | | CALL ValidationService ------------->| | | START NEW TRANSACTION | | | | SELECT row (waits for lock) | | | (waiting for T2 to complete) | (waiting for T1 to commit) | | v v DEADLOCK DETECTEDThe root causes are:
-
REQUIRES_NEW creates a suspended transaction: When
ValidationServiceusesREQUIRES_NEW, Spring suspends Transaction 1 and starts Transaction 2. -
Database locks are not released until commit: Transaction 1 holds a row lock on the inserted order. This lock is not released until Transaction 1 commits or rolls back.
-
Transaction 2 cannot see uncommitted data: By default, database transactions use READ COMMITTED isolation level. Transaction 2 cannot see uncommitted changes from Transaction 1.
-
Classic deadlock pattern: Transaction 1 waits for Transaction 2 to complete. Transaction 2 waits for Transaction 1βs lock. Neither can proceed.
Understanding Transaction Propagation
Spring provides several propagation options:
public enum Propagation { REQUIRED(0), // Join existing or create new (default) SUPPORTS(1), // Join existing if exists, else non-transactional MANDATORY(2), // Must join existing transaction REQUIRES_NEW(3), // Suspend existing, create new NOT_SUPPORTED(4), // Suspend existing, run non-transactional NEVER(5), // Must run non-transactional NESTED(6); // Nested transaction if supported}The default REQUIRED is the safest choice in most cases. Use REQUIRES_NEW only when you explicitly need a separate transaction boundary.
Summary
In this post, I showed why Spring Boot transactions deadlock when calling another service with REQUIRES_NEW propagation. The key points are:
- REQUIRES_NEW suspends the current transaction and creates a new one
- Database locks are held until commit, so suspended transactions keep their locks
- The new transaction cannot see uncommitted data from the suspended transaction
- Use REQUIRED (default) to join the existing transaction, or restructure your service calls
To prevent deadlocks:
- Use default
REQUIREDpropagation unless you have a specific reason forREQUIRES_NEW - Keep transactions as short as possible
- Consider using
@TransactionalEventListenerfor post-commit actions - Test your transaction boundaries with concurrent load
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- π¨βπ» Spring Framework Transaction Documentation
- π¨βπ» Spring Transaction Propagation
- π¨βπ» Reddit Discussion: Spring Boot Transaction Deadlock
Oh, and if you found these resources useful, donβt forget to support me by starring the repo on GitHub!
Comments