The Hidden Danger of Misusing @Transactional in Spring Boot
The @Transactional annotation is one of the most useful features in Spring Boot applications.
With a single annotation, developers can ensure that multiple database operations are executed atomically:
@Transactional
public void createOrder() {
orderRepository.save(order);
paymentRepository.save(payment);
}
However, one of the most common mistakes in enterprise applications is placing @Transactional at the wrong architectural level.
A transaction should protect a database consistency boundary — not an entire business workflow.
The Common Mistake: Transaction at the Handler Level
A typical application structure looks like:
Controller
|
v
Handler
|
+-- Validate request
|
+-- Load data
|
+-- Execute business process
|
+-- Call external services
|
+-- Save database changes
A common implementation is:
@Transactional
public void processOrder(ProcessOrderCommand command) {
validate(command);
customerService.checkCustomer(command.customerId());
pricingService.calculatePrice(command);
externalPaymentService.reservePayment(command);
orderRepository.save(order);
notificationService.sendConfirmation(command);
}
At first glance, this looks convenient.
The entire business operation is wrapped in one transaction.
But this creates a hidden problem:
The database transaction now lives as long as the complete business workflow.
Why This Causes TransactionTimeoutException
A database transaction is not free.
While it is active:
- database connections are occupied
- locks may be held
- resources are consumed
Now imagine the handler performs:
- REST calls to external systems
- complex calculations
- message publishing
- file processing
- waiting for user-related services
Example:
@Transactional
public void processApplication(Application application) {
customerService.validateCustomer();
creditScoreClient.checkScore();
documentService.generateDocuments();
repository.save(application);
}
The database transaction is opened before validateCustomer().
But the database is not involved during:
customer validation credit score API call document generation
The transaction simply waits.
Eventually:
Transaction started
|
|
|---- External API call (20 seconds)
|
|---- Document generation (30 seconds)
|
|---- Database update
|
Transaction timeout exceeded
Result:
org.springframework.transaction.TransactionTimedOutException
The database operation itself may be perfectly fine.
The transaction was simply kept open too long.
A Transaction Is Not a Business Process
One of the most important architectural principles:
A business workflow is not automatically a database transaction.
A business process may contain:
- validations
- external communication
- calculations
- user notifications
- multiple services
A database transaction should only cover:
- reading data that must be consistent
- modifying related database entities
- committing atomic changes
Better Approach: Keep Transactions Close to Data Changes
Instead of:
@Transactional
public void handleOrder() {
validate();
callExternalService();
repository.save(order);
}
Prefer:
public void handleOrder() {
validate();
callExternalService();
orderService.createOrder(order);
}
With:
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
}
}
Now the transaction only exists when database work is actually happening.
Transaction Boundaries Should Follow Consistency Boundaries
A good transaction boundary answers this question:
Which data must change together, atomically?
Example:
Creating an order:
Order
Order Item
Order Status
These probably belong in one transaction.
But:
Send email
Call payment provider
Generate PDF
Publish notification
usually do not.
They require different reliability patterns.
What About Calling Other Services?
A common mistake:
@Transactional
public void checkout() {
orderRepository.save(order);
paymentClient.pay();
inventoryClient.reserve();
}
This does not create one transaction.
The database transaction only protects the local database.
The REST calls are outside the database transaction model.
If payment succeeds but inventory fails, the database cannot automatically roll back the remote service.
For these cases, patterns like:
- Saga
- Event-driven communication
- Eventual consistency
are usually more appropriate.
Why This Problem Is Common in Spring Boot
Spring makes transactions extremely easy:
@Transactional
The simplicity can hide the complexity.
Developers often think:
“This method performs one business operation, therefore it needs one transaction.”
But the correct question is:
“Which part of this method requires database atomicity?”
These are not always the same thing.
Long Transactions Can Exhaust the Database Connection Pool
Keeping transactions open longer than necessary has another serious consequence: database connection pool exhaustion.
When a method annotated with @Transactional starts executing, Spring typically obtains a database connection from the connection pool. That connection remains allocated until the transaction is committed or rolled back.
If the transaction spends most of its lifetime performing non-database work—such as:
- calling external REST services
- waiting for responses
- generating documents
- performing lengthy calculations
the database connection sits idle while remaining unavailable to other requests.
Consider a service with:
- a connection pool of 20 connections
- 20 concurrent requests
- each request spending 40 seconds waiting for an external API before writing to the database
Although the database itself is almost idle, all 20 connections remain occupied by open transactions.
As new requests arrive, they cannot obtain a database connection and eventually fail with connection acquisition or transaction timeout exceptions.
Ironically, the database may appear healthy while the application becomes unavailable simply because all connections are waiting inside unnecessarily long transactions.
Keeping transactions short not only reduces lock contention but also allows the connection pool to serve significantly more concurrent requests.
Practical Rules
Good places for @Transactional:
✅ Service methods performing database changes
✅ Domain operations requiring atomic updates
✅ Repository coordination logic
Risky places for @Transactional:
❌ REST controllers
❌ Message handlers containing workflows
❌ Large orchestration services
❌ Methods calling external systems
Final Thoughts
@Transactional is a powerful tool, but like every powerful abstraction, it can be misused.
A transaction should be:
- short-lived
- focused
- close to database operations
Do not use transactions to wrap business processes.
Use them to protect consistency.
A well-designed Spring Boot application separates:
- business workflow orchestration
- database transaction boundaries
- external communication
This results in better performance, fewer timeout issues, and a more reliable system.
Key Takeaways
- Do not put @Transactional on large workflow handlers
- A business process is not the same as a database transaction
- External calls should usually happen outside database transactions
- Keep transactions short and focused
- Design transaction boundaries around data consistency