Spring Boot JDBC & Connection Management: JdbcTemplate, HikariCP
Master Spring Boot JDBC with JdbcTemplate and HikariCP connection pooling. Learn setup, configuration, best practices, failure scenarios, and observability.
Master Spring Boot JDBC with JdbcTemplate and HikariCP connection pooling. Learn setup, configuration, best practices, failure scenarios, and observability. The guide uses practical examples to explain when to use jdbc/jdbctemplate, setting up the dependency and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.
Spring Boot JDBC & Connection Management: JdbcTemplate, HikariCP
When building enterprise Java applications that interact with relational databases, understanding how Spring Boot manages database connections can mean the difference between a performant system and one that collapses under load. Spring Boot abstracts away much of the boilerplate, but the underlying concepts of connection pooling, transaction management, and resource lifecycle remain critical knowledge for every backend developer.
This guide covers Spring Boot’s JDBC support, focusing on JdbcTemplate for executing SQL statements and HikariCP for managing a pool of database connections efficiently.
When to Use JDBC/JdbcTemplate
Introduction
JDBC connection management determines how a Spring Boot application acquires, uses, and releases database resources. This guide explains Spring’s connection abstractions, pooling behavior, transaction boundaries, timeout and leak risks, and practical patterns for keeping JDBC workloads efficient under concurrent traffic.
Setting Up the Dependency
Spring Boot’s JDBC support comes bundled with HikariCP by default. To add JDBC functionality to your project, include the Spring Boot JDBC starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
For Maven users, the starter brings in both spring-jdbc and HikariCP as transitive dependencies. Gradle users can add:
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
Spring Boot 3.x requires Java 17 minimum and includes HikariCP 5.x by default.
DataSource Configuration
Spring Boot auto-configures a DataSource bean using your application.properties or application.yml settings. The minimal configuration requires only the JDBC URL:
spring:
datasource:
url: "jdbc:mysql://localhost:3306/mydb"
username: "dbuser"
password: "dbpass"
Spring Boot automatically detects the database driver from the JDBC URL and loads the appropriate driver class.
HikariCP-Specific Properties
HikariCP offers extensive configuration options through the spring.datasource.hikari namespace:
spring:
datasource:
url: "jdbc:mysql://localhost:3306/mydb"
username: "dbuser"
password: "dbpass"
hikari:
pool-name: "MyAppHikariPool"
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
These settings control pool sizing, connection timeouts, and leak detection, which we will look at in more detail throughout this article.
JdbcTemplate in Action
JdbcTemplate is Spring’s central class for JDBC operations. It handles resource management (opening and closing connections, statements, and result sets) and translates SQL exceptions into Spring’s unified DataAccessException hierarchy.
Querying Data
The simplest query operations use the queryFor methods:
@Service
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(Long id) {
String sql = "SELECT id, username, email, created_at FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getTimestamp("created_at"));
return user;
}, id);
}
public List<User> findAllByActive(boolean active) {
String sql = "SELECT id, username, email FROM users WHERE active = ?";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
return user;
}, active);
}
public int countActiveUsers() {
String sql = "SELECT COUNT(*) FROM users WHERE active = true";
return jdbcTemplate.queryForObject(sql, Integer.class);
}
}
Update Operations
Insert, update, and delete operations use the update method:
public User createUser(User user) {
String sql = "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
return ps;
}, keyHolder);
Number key = keyHolder.getKey();
user.setId(key.longValue());
return user;
}
public void updateUserEmail(Long id, String newEmail) {
String sql = "UPDATE users SET email = ? WHERE id = ?";
int rowsUpdated = jdbcTemplate.update(sql, newEmail, id);
if (rowsUpdated == 0) {
throw new UserNotFoundException("User not found with id: " + id);
}
}
public void deleteUser(Long id) {
String sql = "DELETE FROM users WHERE id = ?";
jdbcTemplate.update(sql, id);
}
Batch Operations
For bulk inserts or updates, batchUpdate significantly improves performance:
public void batchInsertUsers(List<User> users) {
String sql = "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
Connection Pool Lifecycle
Understanding how HikariCP manages connections helps you configure it correctly and diagnose issues when they arise.
graph TD
A[Application Start] --> B[Initial Pool Creation]
B --> C[Minimum Idle Connections Created]
C --> D[Connection Request Received]
D --> E{Connection Available in Pool?}
E -->|Yes| F[Connection Borrowed]
E -->|No| G{Pool at Maximum Size?}
G -->|No| H[Create New Connection]
H --> F
G -->|Yes| I[Wait for Connection Timeout]
I --> J{Connection Available?}
J -->|Yes| F
J -->|No| K[Throw SQLException]
F --> L[Application Uses Connection]
L --> M[Connection Returned to Pool]
M --> N{Connection Still Valid?}
N -->|Yes| O[Mark as Idle, Ready for Reuse]
N -->|No| P[Remove from Pool, Create Replacement]
O --> Q[Connection Available in Pool]
P --> Q
Q --> D
HikariCP Configuration Deep Dive
Pool Sizing Principles
Getting pool size wrong leads to either wasted resources or threads starving for connections. The optimal pool size depends on several factors:
| Configuration | Formula | Default | Description |
|---|---|---|---|
maximum-pool-size |
((core_count * 2) + effective_spindle_count) |
10 | Maximum connections in pool |
minimum-idle |
maximum-pool-size for long-running apps |
same as max | Minimum idle connections maintained |
connection-timeout |
30 seconds | 30000ms | Max wait time for available connection |
idle-timeout |
10 minutes | 600000ms | Max idle time before connection is removed |
max-lifetime |
30 minutes | 1800000ms | Max connection age before retirement |
For a system with 4 CPU cores and a database on SSD storage:
spring:
datasource:
hikari:
maximum-pool-size: 10 # (4 * 2) + 2 = 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 300000
max-lifetime: 1200000
Leak Detection
HikariCP can detect connections that are borrowed but never returned:
spring:
datasource:
hikari:
leak-detection-threshold: 60000 # 60 seconds
When a connection is checked out for longer than leak-detection-threshold, HikariCP logs a warning with the connection stack trace, helping identify code paths that fail to return connections.
Failure Scenarios and Handling
Connection Timeout
When the pool is exhausted and no connection becomes available within connection-timeout, a SQLException is thrown:
SQLException: HikariPool - Connection is not available, request timed out after 30000ms.
Fix: Increase connection-timeout, optimize queries that hold connections too long, or increase maximum-pool-size.
Database Unavailability
If the database becomes unavailable, HikariCP continuously attempts to reconnect based on validation-timeout:
spring:
datasource:
hikari:
validation-timeout: 5000
connection-test-query: SELECT 1
Set connection-test-query only if your database requires it. HikariCP uses JDBC4’s isValid() method by default in most cases.
Connection Leaks
A connection leak happens when a connection is borrowed but never returned:
// BAD: Connection never closed
public void badPractice() {
Connection conn = dataSource.getConnection();
// ... do work but never close()
}
// GOOD: Proper resource management
public void goodPractice() {
jdbcTemplate.execute("SELECT 1"); // JdbcTemplate handles cleanup
}
Turn on leak-detection-threshold during development to catch leaks:
spring:
datasource:
hikari:
leak-detection-threshold: 30000
Security Considerations
SQL Injection Prevention
JdbcTemplate prevents SQL injection when you use parameterized queries correctly:
// SAFE: Parameterized query
jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE email = ?",
User.class,
userEmail // Parameter bound safely
);
// DANGEROUS: String concatenation - NEVER do this
// jdbcTemplate.query("SELECT * FROM users WHERE email = '" + email + "'");
Never concatenate user input directly into SQL strings. Even for table or column names, use a whitelist:
private static final Set<String> ALLOWED_SORT_COLUMNS = Set.of("id", "username", "email", "created_at");
public List<User> findAllSorted(String sortColumn) {
if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
throw new IllegalArgumentException("Invalid sort column");
}
String sql = "SELECT * FROM users ORDER BY " + sortColumn;
return jdbcTemplate.query(sql, new UserRowMapper());
}
Connection Leak Prevention
Always use try-with-resources or proper finally blocks:
// Using JdbcTemplate (recommended)
public User findById(Long id) {
// JdbcTemplate manages connection lifecycle
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new UserRowMapper(),
id
);
}
// If you must use DataSource directly
public void manualConnectionUse(DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
// Work with connection
} // Connection automatically closed
}
Credential Management
Never store database passwords in plain text. Use environment variables or a secrets manager:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
For Kubernetes deployments, use Kubernetes Secrets. For local development, consider tools like spring-cloud-config or HashiCorp Vault.
Observability Checklist
Monitoring your database connection pool matters in production.
Metrics to Track
- Active Connections: Current connections in use
- Idle Connections: Available connections in pool
- Waiting Threads: Threads waiting for a connection
- Connection Timeout Rate: Frequency of timeout errors
- Connection Creation Time: Average time to create new connections
- Connection Usage: Total connections served
Health Indicator Configuration
Spring Boot Actuator ships with built-in DataSource health indicators:
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
Hit /actuator/health to see DataSource status, or /actuator/metrics/hikaricp.connections.active for specific metrics.
Logging Configuration
Enable HikariCP logging for debugging:
logging.level.com.zaxxer.hikari=DEBUG
logging.level.com.zaxxer.hikari.pool.HikariPool=DEBUG
Security and Compliance Notes
Database connections hold sensitive data and require the same access controls you apply to the application layer itself.
Credential Management in JDBC
Never store database passwords in plain text in configuration files. Use environment variables or a secrets manager: spring.datasource.password=${DB_PASSWORD}. In Kubernetes, use Secrets mounted as environment variables. For enterprise environments, HashiCorp Vault or AWS Secrets Manager integrate with Spring Boot through add-on libraries. Rotate credentials regularly and audit access to the systems that store them.
TLS/SSL Connections
Enable SSL for any database connection crossing a network boundary. Cloud-managed databases increasingly require SSL and reject unencrypted connections by default. Configure ssl=true and sslmode=require in your JDBC URL, and verify the server certificate with sslcert= pointing to your CA certificate. For PostgreSQL, sslmode=verify-full validates the server certificate against a trusted CA.
Principle of Least Privilege for Database Users
Create application database users with only the privileges they need. Your application user rarely needs SUPERUSER or CREATE privileges. Separate read and write operations into different users if your ORM supports it. For reporting or analytics that only needs read access, use a dedicated read-only user. This limits the blast radius if credentials are compromised.
Audit Logging for Compliance
Enable database-level audit logging for compliance requirements. PostgreSQL’s pgaudit extension logs all sessions and statements. MySQL Enterprise Audit provides similar capabilities. Route audit logs to a centralized log system and retain them according to your regulatory requirements. Spring Boot’s spring.jpa.properties.hibernate.session.events.log can supplement database-level auditing with application-level context.
Connection Pool Resource Limits
Setting maximumPoolSize too high can starve the database’s connection limit for other applications or administrative sessions. Know your database’s max_connections setting and divide it among all applications that connect to that database, leaving headroom for administrative access. Monitor active connections via HikariCP metrics and alert when connection counts approach database limits.
Common Pitfalls / Anti-Patterns
Pitfall 1: Mismatched Pool and Database Limits
If your HikariCP maximum-pool-size exceeds the database’s max_connections, you will see connection failures under load.
Fix: Set maximum-pool-size below your database’s connection limit, accounting for other applications.
Pitfall 2: Long-Running Transactions Holding Connections
Analytic queries or report generation can hold connections for minutes, exhausting the pool.
Fix: Use read replicas for heavy reads, optimize queries, or create a separate DataSource with a larger pool for batch processing.
Pitfall 3: Missing Indexes Causing Slow Queries
Slow queries hold connections longer, reducing pool throughput.
Fix: Run EXPLAIN ANALYZE on queries and add appropriate indexes.
Pitfall 4: Not Closing Resources in Legacy Code
When migrating legacy JDBC code, make sure all Connection, Statement, and ResultSet instances are properly closed.
Fix: Refactor to use JdbcTemplate or wrap legacy code in try-with-resources blocks.
Production Failure Scenarios
Connection issues with databases tend to surface at the worst possible moments — during peak traffic, late on a Friday, or right before a major release. Understanding what breaks and why helps you build systems that either stay up or fail gracefully.
HikariPool Timeout Under Load
When your application hits peak traffic and the connection pool is exhausted, HikariCP waits up to connectionTimeout milliseconds for a connection to become available. If no connection arrives, you get PoolExhaustedException. The fix requires addressing both sides: increase pool size if your database max_connections allows, and optimize the queries holding connections longest.
Lost Connections During Database Failover
Cloud-managed databases fail over to a standby replica. During failover, existing connections are terminated. HikariCP detects this and creates new connections, but your application must handle SQLException during the brief window when no connections are available. Use connection pool health checks and configure validationTimeout so dead connections are detected before they are borrowed.
HikariCP Connection Leak in Application Code
A connection leak occurs when a thread borrows a connection from the pool but never returns it. Under sustained load, this gradually exhausts the pool until new requests start timing out. The leakDetectionThreshold setting logs a stack trace when a connection is held longer than the threshold, pointing directly at the problematic code path. Fix leaks by ensuring every getConnection() call has a corresponding close() in a finally block, or by using JdbcTemplate which handles this automatically.
Transaction Timeout Leaving Connections in Inconsistent State
Long-running transactions that time out at the application level sometimes leave connections in an inconsistent state if the transaction is not properly rolled back. HikariCP validates connections on checkout, but a transaction that times out mid-execution may leave locks held in the database. Configure isolationLevel and transaction timeouts consistently across your application and database.
Database Driver Incompatibility After Upgrade
Upgrading your database server version sometimes changes driver behavior. A driver that worked with PostgreSQL 13 might send queries that PostgreSQL 14 rejects or handles differently. Always test database upgrades against your application in a staging environment before applying them in production, and keep driver versions pinned in your build configuration.
Quick Recap Checklist
- Added
spring-boot-starter-jdbcdependency - Configured
spring.datasource.url,username, andpassword - Set HikariCP pool size appropriately for your workload
- Turned on
leak-detection-thresholdduring development - Used parameterized queries to prevent SQL injection
- Used
JdbcTemplatefor all database operations - Configured Spring Boot Actuator for monitoring
- Set up health checks for DataSource
- Reviewed slow queries and added necessary indexes
- Secured credentials using environment variables
Trade-off Analysis
When choosing and configuring JDBC with HikariCP, several trade-offs shape the right choice for your application.
JdbcTemplate vs JPA/Hibernate
| Aspect | JdbcTemplate | JPA/Hibernate |
|---|---|---|
| Performance overhead | Lower per-query | Higher due to ORM |
| Learning curve | Steeper for SQL | Steeper for ORM concepts |
| Flexibility | Full SQL control | Abstraction hides SQL |
| Boilerplate | Manual mapping | Automatic entity mapping |
| Best for | Simple CRUD, batch ops | Complex object graphs |
HikariCP Pool Size: Small vs Large
| Factor | Small Pool (5-10) | Large Pool (20+) |
|---|---|---|
| Memory usage | Lower | Higher |
| Throughput ceiling | Lower | Higher |
| Database connection limit | Less pressure | More pressure |
| Latency under load | Higher contention | Lower contention |
| Ideal workload | Steady, predictable | Bursty, high concurrency |
Connection Timeout Trade-offs
| Setting | Short Timeout (5s) | Long Timeout (60s+) |
|---|---|---|
| Failure detection | Faster | Slower |
| Thread blocking | Shorter | Longer |
| User experience | Quicker failure | Longer wait on slow DB |
| Recommended for | High-throughput APIs | Batch processing |
Batch Operations: Single vs Batch
| Approach | Single Insert | Batch Update |
|---|---|---|
| Network round-trips | N | 1 |
| Throughput | Low | High (10-50x) |
| Memory usage | Lower | Higher |
| Transaction scope | Per-row | Entire batch |
| Use case | OLTP | ETL, migrations |
Validation: On Borrow vs On Statement
| Validation | test-on-borrow (default) | test-while-idle |
|---|---|---|
| Detection speed | Faster (at checkout) | Slower (during idle) |
| Performance impact | Lower (per-use) | Higher (periodic) |
| DB load | Lower | Higher (periodic) |
| Recommended | Most cases | Long-lived connections |
Interview Questions
HikariCP is the default connection pool in Spring Boot 2.x and later. It was chosen because it offers the best performance among major connection pools, with faster connection acquisition times and lower memory overhead compared to alternatives like Apache DBCP2 or Tomcat JDBC. HikariCP achieves this through aggressive pool sizing, efficient batch connection creation, and optimized statement caching.
HikariCP validates connections using three methods, in order of preference: JDBC4's Connection.isValid() method (default), a custom validation query if configured via connection-test-query, or custom validation via validation-timeout. The validation occurs at connection checkout when test-on-borrow is true (default), at connection return when test-on-return is true, and periodically for idle connections when test-while-idle is true. For most cases, the default JDBC4 validation is sufficient and requires no configuration.
When all connections are in use and a new request arrives, HikariCP blocks the requesting thread and waits for a connection to become available. The maximum wait time is controlled by connection-timeout, which defaults to 30 seconds. If no connection becomes available within this window, HikariCP throws a SQLException with the message "Connection is not available, request timed out after Xms." To handle this gracefully, applications should implement retry logic with exponential backoff or circuit breakers.
JdbcTemplate prevents SQL injection through its parameterized query methods. Always use the ? placeholder for user-supplied values and pass them as arguments:
jdbcTemplate.queryForObject(sql, RowMapper, args...)jdbcTemplate.update(sql, args...)
Never concatenate user input directly into SQL strings, even for identifiers like table or column names. For dynamic identifiers, use a whitelist of allowed values. JdbcTemplate also provides named parameters via NamedParameterJdbcTemplate for more readable code.
maximum-pool-size and minimum-idle in HikariCP?minimum-idle defines the minimum number of idle connections that HikariCP maintains in the pool even when there is no activity. These connections are immediately available for new requests without the overhead of connection creation. maximum-pool-size is the absolute ceiling — HikariCP will never create more connections than this limit. When minimum-idle equals maximum-pool-size (the default), the pool maintains a fixed size. For short-lived applications or microservices with sporadic activity, setting minimum-idle lower than maximum-pool-size can reduce resource consumption.
The lifecycle begins at application startup when HikariCP creates initial connections up to minimum-idle. When a connection is needed, the application borrows from the pool. After use, the connection is returned to the pool if still valid. HikariCP validates connections on checkout (if test-on-borrow is true) and removes dead connections. Idle connections exceeding idle-timeout are closed until minimum-idle remains. Connections older than max-lifetime are retired and replaced.
Connection leak detection identifies when a connection is borrowed but not returned within a expected time. Set leak-detection-threshold to the maximum milliseconds a connection should be held before being considered leaked. When exceeded, HikariCP logs a warning with the connection stack trace showing where getConnection() was called. This helps identify code paths that fail to close connections. Recommended threshold: 60 seconds during development, lower in production to catch leaks faster.
idle-timeout and max-lifetime in HikariCP?idle-timeout controls how long an idle connection remains in the pool before being closed, down to the minimum-idle floor. This removes unused connections to free database resources. max-lifetime sets the maximum age of any connection, after which it is retired and replaced with a new one. This handles database server connection timeouts, stale connections from firewalls, and connection corruptions. Set max-lifetime lower than your database's server-side connection timeout.
JdbcTemplate does not handle transactions itself — it executes individual SQL statements. For transaction support, use Spring's @Transactional annotation on service methods. This creates a platform transaction (DataSourceTransactionManager by default) that manages connection commit/rollback. Within a transactional method, multiple JdbcTemplate calls share the same connection. Without @Transactional, each JdbcTemplate call is auto-committed individually.
NamedParameterJdbcTemplate over standard JdbcTemplate?NamedParameterJdbcTemplate uses named parameters (like :email) instead of positional ? placeholders, making SQL more readable and maintainable. Named parameters eliminate the risk of argument ordering errors when you have many parameters. It also enables better self-documenting SQL. Internally, it wraps JdbcTemplate and converts named parameters to positional at execution time, so there is no performance penalty.
JdbcTemplate's query methods return a List that materializes all results at once. For streaming large result sets, use queryForStream() which returns a Stream that processes rows lazily. For cursor-based processing of millions of rows, use JdbcTemplate.execute(CallableStatementCallback) with manual ResultSet.next() iteration and connection management. Always close resources in finally blocks or use try-with-resources.
During failover (primary goes down, standby promoted), existing connections are terminated. HikariCP detects dead connections on checkout via validation and throws SQLException. New connections are created against the new primary. Your application needs exception handling for SQLException during the brief failover window. Configure validationTimeout appropriately (5 seconds recommended) so dead connections are detected before being handed to the application.
The formula ((core_count * 2) + effective_spindle_count) provides a starting point, but actual needs depend on: your database max_connections limit (leave headroom for admin sessions), whether your queries are CPU-bound or I/O-bound (I/O-bound can use more connections), your application's concurrency patterns (steady vs bursty), and network latency to the database (higher latency may warrant smaller pools to avoid thread starvation).
test-on-borrow, test-on-return, and test-while-idle?test-on-borrow validates connections when borrowed from the pool (default: true) — catches dead connections before use. test-on-return validates when returned to the pool — rarely needed. test-while-idle validates idle connections periodically (default: false) — keeps connections warm but adds overhead. For most cases, test-on-borrow with JDBC4 isValid() is sufficient. Only enable test-while-idle if connections go stale during long idle periods.
HikariCP consistently outperforms alternatives in benchmarks due to: aggressive pool sizing that avoids unnecessary synchronization, optimized bytecode for fast path execution, minimal object allocation during connection retrieval, and efficient statement caching built into the pool itself. DBCP2 prioritizes stability over speed, and Tomcat JDBC offers similar performance to HikariCP but with less active development. Spring Boot chose HikariCP as default for its combination of performance and reliability.
RowMapper maps each row of a ResultSet to a single object, called once per row — ideal for List results. ResultSetExtractor processes the entire ResultSet at once, giving you full control to build complex results or handle multiple result sets — useful for complex queries or when you need to iterate multiple times. JdbcTemplate's queryForObject() uses RowMapper, while query() with ResultSetExtractor gives you complete ResultSet control.
max-lifetime lower than the database server connection timeout?Database servers close connections after a timeout period (MySQL's wait_timeout default is 8 hours). If HikariCP keeps connections past this, they die silently and cause errors when used. Setting max-lifetime 20-30% below the server timeout ensures connections are refreshed before they expire. For MySQL with default 8-hour timeout, set max-lifetime to about 30 minutes (1800000ms) to be safe.
Enable Spring Boot Actuator with spring-boot-starter-actuator and expose health and metrics endpoints. Key metrics: hikaricp.connections.active (currently borrowed), hikaricp.connections.idle (available), hikaricp.connections.pending (threads waiting), hikaricp.connections.timeout (connection timeouts). Add management.endpoint.health.show-details=always to see pool status in health checks. For production, export metrics to Prometheus or Datadog.
jdbcTemplate.query() and jdbcTemplate.queryForObject()?queryForObject() returns a single object and throws IncorrectResultSizeDataAccessException if zero or more than one row matches — ideal for lookups by unique key. query() returns a List of objects, which may be empty if no matches found — ideal for lists or when multiple results are possible. Using queryForObject() for queries that might return multiple rows is a common mistake that causes unexpected exceptions.
Use the primitive wrapper types (Integer, Long, Double) instead of primitives in your domain objects — primitives cannot represent null from SQL NULL. For Date/Time fields, use rs.getTimestamp() which returns null for SQL NULL, or rs.getObject(column, LocalDateTime.class) for Java 8+ types. When mapping, check rs.wasNull() after get operations on primitives to detect NULL values and set the wrapper type to null accordingly.
Further Reading
- Spring Boot JDBC Documentation - Official Spring Boot reference for JDBC support
- HikariCP GitHub Repository - HikariCP source code, configuration options, and performance benchmarks
- Understanding HikariCP Connection Pool Sizing - Detailed guidance on pool sizing calculations
- Spring JdbcTemplate Best Practices - Spring Framework’s official JdbcTemplate guide
- Database Connection Pooling Explained - Baeldung’s comprehensive overview of connection pooling patterns
- Spring Boot Actuator Metrics - Monitoring HikariCP metrics through Spring Boot Actuator
Conclusion
Spring Boot’s JDBC support with HikariCP gives you a production-ready foundation for database access in Java applications. The key things to get right: use JdbcTemplate for all database operations so connection lifecycle is handled automatically, tune HikariCP pool sizes based on your actual workload, turn on leak detection during development, and always use parameterized queries.
These practices together make a Spring Boot application that handles demanding database workloads without surprising you at 3 AM.
Category
Related Posts
Spring Boot Build Tools: Maven & Gradle
Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.
Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow
Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.
JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests
Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.