Spring Boot Advanced: Security and Production
Transactions and Performance
In this lesson you will
- Use @Transactional correctly and know its pitfalls
- Recognise and fix the N+1 query problem
- Choose sensible performance measures before optimising
Introduction
A correct app can still be wrong under load, or half-update your data when something fails halfway. This lesson covers the two biggest topics for back-end Java services: transactions and database performance. It is written for Spring Boot 4.1.2 (Hibernate 7), and the code was not compiled or run here. Configuration names mentioned here were checked against Boot 4.1.2 metadata.
Transactions
A transaction groups database changes so they succeed or fail together. Transferring money requires debiting one account and crediting another. If the second step fails, the first must be undone (rolled back).
Java
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountRepository accounts;
public TransferService(AccountRepository accounts) { this.accounts = accounts; }
@Transactional
public void transfer(long fromId, long toId, java.math.BigDecimal amount) {
Account from = accounts.findById(fromId).orElseThrow();
Account to = accounts.findById(toId).orElseThrow();
from.withdraw(amount); // may throw if funds are insufficient
to.deposit(amount);
}
}When the method returns normally, Spring commits. If it throws a RuntimeException, Spring rolls back, and neither account changes.
Rules and pitfalls:
- By default, rollback happens for unchecked exceptions and errors, not for checked exceptions. Use
@Transactional(rollbackFor = Exception.class)if you need that. - Put
@Transactionalon service methods, not on controllers or repositories you do not control. - Self-invocation does not work: calling a
@Transactionalmethod from another method in the same class bypasses Spring's proxy, so the annotation is ignored. Move it to another bean. - For methods that only read, use
@Transactional(readOnly = true), which allows optimisations and signals intent. - Keep transactions short. Do not call slow external services inside one, because it holds database resources.
- Money should use
BigDecimal, neverdouble.
Quick quiz
By default, when does Spring roll back a @Transactional method?
Lazy loading and open-in-view
Relationships such as @OneToMany are lazy by default: related rows load only when first accessed. That is good for performance, but accessing a lazy field after the transaction ends causes a LazyInitializationException.
Spring Boot has a setting, spring.jpa.open-in-view, whose default is true (I checked the Boot 4.1.2 metadata). It keeps a database session open for the whole web request, which hides the exception, but can hold connections longer and trigger queries from the view or JSON conversion layer. Many teams set it to false and load what they need explicitly in the service layer:
Properties
spring.jpa.open-in-view=falseReturn DTOs (response records) built inside the service, so JSON conversion never touches lazy fields.
The N+1 query problem
Suppose each Author has many Book rows, and you list authors with their book counts:
Java
List<Author> authors = authorRepository.findAll(); // 1 query
for (Author a : authors) {
System.out.println(a.getBooks().size()); // 1 more query per author
}With 100 authors that is 101 queries (1 + N). It is fast with a few rows and slow with thousands.
Fixes:
- Fetch join: load the relation in one query.
Java
@Query("select distinct a from Author a left join fetch a.books")
List<Author> findAllWithBooks();@EntityGraphon a repository method.- Query for exactly what you need, such as a DTO projection with counts computed in the database.
- Batch fetching settings in Hibernate.
Find these problems by looking at the SQL (spring.jpa.show-sql=true in development) and counting queries in tests. Beware of fetching huge collections together with pagination. Check what Hibernate does.
Pagination and indexes
- Paginate every list that can grow. Never return unbounded results.
- Add database indexes for columns you filter or sort on often. Check query plans with your database's tools. Indexes speed reads but slow writes and use space.
- Select only the columns you need for heavy queries.
Connection pools
Spring Boot uses a connection pool (HikariCP by default). A pool that is too small makes requests wait. A pool that is too large can overload the database. Start with defaults, measure, and tune spring.datasource.hikari.maximum-pool-size only with evidence.
Caching
@Cacheable stores a method's result, so repeated calls with the same arguments skip the work:
Java
@Cacheable("bookById")
public Book get(long id) { /* ... */ }Enable it with @EnableCaching and a cache provider dependency. Caching creates a staleness problem: cached data may be out of date, so decide expiry and invalidation rules. Cache only after you have measured a real bottleneck.
Virtual threads
Boot supports Java 21 virtual threads, lightweight threads that make blocking code scale better for I/O-heavy apps:
Properties
spring.threads.virtual.enabled=trueThey help when requests spend most of their time waiting. They do not make CPU-heavy work faster, and your database pool still limits throughput. Test before enabling in production.
Measure before you optimise
- Define the problem: slow endpoint, high CPU, too many queries.
- Measure: logs, metrics (next lesson), query counts, profilers.
- Fix the biggest cause first, usually the database queries.
- Measure again.
Premature optimisation wastes time and adds complexity.
Try it yourself
Create an Author with a @OneToMany list of books, insert 20 authors and list them with book counts. Turn on spring.jpa.show-sql and count the queries. Fix the N+1 with a fetch join and compare.
Summary
- @Transactional makes changes atomic, rolls back on unchecked exceptions, and does not work on self-invocation.
- Watch for lazy loading and the N+1 problem, return DTOs, paginate and index sensibly.
- Measure first, and treat caching, pool tuning and virtual threads as tools to apply after evidence.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading