Spring Boot Database Performance Tuning – JPA, HikariCP & JDBC
Most Spring Boot performance issues are not caused by JVM, GC, or threads — they are caused by the database layer.
Teams often scale pods, increase thread pools, or enable Virtual Threads, while the real bottleneck remains unchanged: a saturated connection pool and inefficient SQL.
Why Database Tuning Matters More Than Threads
A typical Spring Boot request lifecycle looks like this:
HTTP Request
→ Controller
→ Service
→ Repository (JPA/JDBC)
→ Database
No matter how fast your controllers are, every request eventually waits for a database connection.
1. Understanding HikariCP (Before Tuning It)
HikariCP is a connection pool, not a performance booster. It controls how many concurrent DB connections your application can use.
| Concept | Meaning |
|---|---|
| maxPoolSize | Maximum concurrent DB connections |
| connectionTimeout | How long a thread waits for a connection |
| idleTimeout | When idle connections are removed |
If all connections are busy, new requests block, even if you have thousands of threads available.
2. The Biggest Mistake: Oversizing Hikari Pool
Many applications use this configuration:
spring.datasource.hikari.maximum-pool-size=50
This is usually wrong.
Rule of Thumb
maxPoolSize ≈ CPU cores × 2
For most production systems:
- Small service: 10–15
- Medium traffic: 15–25
- Heavy DB writes: lower is better
3. Connection Pool Exhaustion Symptoms
Your app may look healthy but behaves poorly under load. Common signs:
- Requests hang without errors
- Increased response time variance
- Thread dumps show many threads waiting on Hikari
HikariPool-1 - Connection is not available
4. JPA Performance Pitfall: N+1 Queries
This is the most common hidden performance killer.
List orders = orderRepository.findAll();
orders.forEach(o -> o.getItems().size());
This can produce:
1 query for orders
+ N queries for items
Fix Using Fetch Join
@Query("select o from Order o join fetch o.items")
List findAllWithItems();
5. Lazy vs Eager Loading – Reality Check
Developers often switch everything to EAGER to fix N+1.
This creates a new problem:
- Huge result sets
- Unnecessary joins
- Higher memory usage
Correct approach:
- Keep associations LAZY
- Use fetch joins only where required
- Create query-specific projections
6. JDBC Batching – Massive Write Performance Gains
By default, JPA inserts rows one by one.
Enable Batching
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
For large inserts, this reduces:
- Network round trips
- Transaction duration
- Connection hold time
7. Transaction Scope Matters
Long transactions hold DB connections longer.
@Transactional
public void process() {
fetch();
callExternalService(); // ❌
update();
}
Better:
fetch();
callExternalService();
@Transactional
update();
8. Read vs Write Separation
Mixing heavy reads and writes in one pool causes contention.
Advanced setups:
- Read replicas
- Separate data sources
- Dedicated pools per workload
9. Monitoring What Actually Matters
Key metrics to monitor:
- Hikari active connections
- Connection wait time
- Slow query logs
Without this visibility, tuning is guesswork.
10. Production Checklist
- Right-sized Hikari pool
- No N+1 queries
- Batch inserts enabled
- Short transactions
- Slow query monitoring
Final Takeaway
Fewer queries, shorter transactions, and realistic pool sizing beat adding more threads every time.