Database Transactions with Hibernate and Spring
Boot
Database transactions with Hibernate and Spring Boot are not only a persistence detail. They are a design
decision.
A transaction defines a unit of work: either all database changes succeed together, or all of them fail
together.
In Spring Boot, transactions are commonly managed with @Transactional. The annotation should
normally mark the boundary of a business operation, not every individual database call.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
@Transactional
public void placeOrder(OrderRequest request) {
Order order = [Link](new Order([Link]()));
[Link](new Payment([Link](), [Link]()));
[Link]();
}
}
If saving the payment fails, the order should not remain half-created. That is the point of the transaction.
The transaction usually belongs at the service level because the service method represents the business
use case.
A repository usually knows how to save, update, delete, or query one entity. A service method knows the
full business operation.
A weak design is to put separate transactions around small repository-style operations that are actually part
of the same business use case.
@Transactional
public void saveOrder(Order order) {
[Link](order);
}
@Transactional
public void savePayment(Payment payment) {
[Link](payment);
}
This creates two separate transaction boundaries. If the second operation fails, the first operation may
already be committed.
@Transactional
public void placeOrder(OrderRequest request) {
Order order = [Link](new Order([Link]()));
[Link](new Payment([Link](), [Link]()));
}
One business operation. One transaction. One rollback boundary.
Transaction: a database unit of work.
Commit: making all changes permanent.
Rollback: undoing all changes inside the transaction.
Persistence context: Hibernate's managed context for entities loaded or saved during a transaction.
Dirty checking: Hibernate automatically detects changed managed entities and prepares updates.
Flush: synchronizing Hibernate changes with the database. Flush does not always mean commit.
Propagation: how a method behaves when called inside an existing transaction.
Isolation: how much one transaction can see changes made by another transaction.
Best practices:
- Put @Transactional on service methods that represent business use cases.
- Keep transactions short.
- Use readOnly = true for read-only queries.
- Let exceptions bubble up when rollback is required.
- Be careful with lazy loading outside a transaction.
- Understand when Hibernate flushes changes.
- Use optimistic locking for concurrent updates.
- Separate database transactions from external API calls when possible.
@Transactional(readOnly = true)
public OrderDetails getOrder(Long orderId) {
return [Link](orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
Common mistakes:
- Putting @Transactional everywhere.
- Opening long transactions around slow network calls.
- Mixing database writes and remote API calls without thinking about failure.
- Assuming save() means the SQL was committed immediately.
- Catching exceptions and hiding them inside a transaction.
- Using transactions to cover bad domain design.
- Depending on lazy loading in controllers.
- Updating large amounts of data entity by entity without batching.
@Transactional
public void processOrder(Long orderId) {
Order order = [Link](orderId).orElseThrow();
[Link](order); // external network call inside transaction
[Link]();
}
This is risky because the database transaction stays open while waiting for a remote system.
That can hold connections longer, increase lock duration, reduce throughput, and make failures harder to
recover from.
A safer design is to persist the local state change in a short transaction, then call the remote system
outside the database transaction, or use an outbox/event-driven flow for stronger reliability.
@Transactional
public void markOrderPendingPayment(Long orderId) {
Order order = [Link](orderId).orElseThrow();
[Link]();
}
Pros:
- Clean business code.
- Declarative transaction boundaries.
- Automatic rollback behavior.
- Good integration with Hibernate and JPA.
- Consistent business operations.
- Less manual commit and rollback code.
Cons:
- Easy to overuse.
- Proxy-based behavior can surprise developers.
- Self-invocation may bypass @Transactional.
- Long transactions can hurt performance.
- Wrong propagation can create confusing bugs.
- Lazy loading problems often appear when transaction boundaries are unclear.
public void outerMethod() {
innerMethod();
}
@Transactional
public void innerMethod() {
// may not start a transaction when called from the same class
}
Spring transactions are usually applied through proxies. Calling a transactional method from the same
class may bypass the proxy.
The useful design question is: what must succeed or fail together?
That is usually the transaction boundary.