Use readOnly for a read unit of work, but enforce write permissions and invariants elsewhere.
Spring read-only transaction: performance hint, not a write guard
Name the promise accurately
A receipt view service loads a tenant-scoped row and maps it to an immutable response within a read-only transaction. Spring passes the read-only flag to transaction infrastructure; with Hibernate, the chosen flush behavior can skip dirty-check work for a large loaded graph. The flag is not an authorization rule and should not be used as the only defense against a mutation. Proxy invocation still controls whether the annotation is applied.
Keep reads bounded and contained
Return a DTO while the transaction is active rather than exposing a lazy entity to the controller. Pair the read with disabled Open EntityManager in View so a later response serializer cannot silently query the database. A projection may avoid loading an entire entity graph. Inherited Spring Data CRUD reads are commonly configured read-only, while declared query methods and outer service transactions deserve explicit review.
Test what matters
Assert that a denied principal cannot call the write path; do not use a readOnly test as proof of denial. Measure statement count and allocation for a representative read, then inspect the transaction attributes at the service proxy. A database driver or provider may treat read-only differently, so benchmark instead of promising a fixed speedup.
Implementation contract
@Transactional(readOnly = true)
public ReceiptView readReceipt(UUID tenantId, UUID receiptId) {
ReceiptEntity receipt = receipts.findByTenantIdAndId(
tenantId, receiptId).orElseThrow(ReceiptNotFound::new);
return new ReceiptView(receipt.getId(), receipt.getTotalMinor());
}Cost and verification
A read-only hint can reduce flush and dirty-check overhead with some providers. The dominant cost may still be SQL, hydrated columns, and N+1 reads. A read transaction holds a connection until it exits.
Common Mistakes
- Do not interpret readOnly as a security boundary or universal database write prohibition.
- Do not return a lazy entity to be serialized after the transaction closes.
- Do not assume a self-invoked annotated method entered a transaction proxy.
Read next
Spring transactional methods: call paths and rollback assumptions, Spring Boot Open EntityManager in View: close the response-time query gap, Spring Data JPA projections: return selected fields without a full entity, Spring Data JPA entity graph for a bounded to-one read, Spring JWT tenant claims: reject missing or malformed ownership before conversion.
