Understanding @Transactional(readOnly = true) in Spring: Mechanics, Performance Benefits, and Usage Considerations
This article explains how Spring's @Transactional(readOnly = true) works internally, shows the performance and memory advantages it brings, compares service‑layer and repository‑layer usage with concrete connection‑pool tests, and provides guidance on when to apply it in JPA‑based backend applications.
Today we discuss Spring's @Transactional(readOnly = true) annotation, why it is widely used in projects, and what performance benefits it can bring.
The readOnly flag is defined in the transaction attribute interface as a hint for the transaction manager:
/**
* A boolean flag that can be set to {@code true} if the transaction is
* effectively read‑only, allowing for corresponding optimizations at runtime.
* Defaults to {@code false}.
*/
boolean readOnly() default false;When a transaction starts, JpaTransactionManager delegates to the JPA dialect. Its doBegin method ultimately calls HibernateJpaDialect.beginTransaction , which prepares the flush mode based on the read‑only flag:
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
// ...
int timeoutToUse = determineTimeout(definition);
Object transactionData = getJpaDialect().beginTransaction(em,
new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
// ...
}Inside HibernateJpaDialect.beginTransaction , the session’s flush mode is set to MANUAL when the transaction is read‑only, preventing automatic flushing of changes:
@Override
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) {
FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly());
if (definition.isReadOnly()) {
session.setDefaultReadOnly(true);
}
// ...
}
protected FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException {
FlushMode flushMode = session.getHibernateFlushMode();
if (readOnly) {
if (!flushMode.equals(FlushMode.MANUAL)) {
session.setHibernateFlushMode(FlushMode.MANUAL);
return flushMode;
}
} else {
if (flushMode.lessThan(FlushMode.COMMIT)) {
session.setHibernateFlushMode(FlushMode.AUTO);
return flushMode;
}
}
return null;
}Consequences of using a read‑only transaction include:
Performance improvement: read‑only entities are not dirty‑checked.
Memory saving: no snapshot of persistent state is kept.
Data consistency: modifications to read‑only entities are not persisted.
Ability to route queries to a read‑only replica in master‑slave or cluster setups.
We then examine whether the annotation should always be placed on service‑layer read‑only methods. Two test methods were written: one with @Transactional(readOnly = true) on the service method, another with the annotation only on the repository method. Connection‑pool logs show that the service‑layer transaction holds a DB connection until the method returns, while the repository‑layer transaction releases the connection immediately after the query finishes.
@Transactional(readOnly = true)
public List
transactionalReadOnlyOnService() {
List
userDtos = userRepository.findAll().stream()
.map(userMapper::toDto)
.toList();
timeSleepAndPrintConnection();
return userDtos;
}
public List
transactionalReadOnlyOnRepository() {
List
userDtos = userRepository.findAll().stream()
.map(userMapper::toDto)
.toList();
timeSleepAndPrintConnection();
return userDtos;
}
// Sample connection‑pool log excerpts
activeConnections:0, IdleConnections:10, TotalConnections:10
start transactionalReadOnlyOnService!!
... (query executed) ...
activeConnections:1, IdleConnections:9, TotalConnections:10
end transactionalReadOnlyOnService!!
activeConnections:0, IdleConnections:10, TotalConnections:10
activeConnections:0, IdleConnections:10, TotalConnections:10
start transactionalReadOnlyOnRepository!!
... (query executed) ...
activeConnections:0, IdleConnections:10, TotalConnections:10
end transactionalReadOnlyOnRepository!!Therefore, when a service method contains heavy non‑database logic, using a read‑only transaction at the service layer can lead to prolonged connection holding, potentially causing connection starvation. The recommendation is to apply @Transactional(readOnly = true) when the read‑only query is the only work in the transaction, and to consider moving the annotation to the repository layer or removing it when additional processing is required.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.