Spring Boot's Eight Built-in Methods for Controlling Database Connections
1. Introduction
The core objective of the Spring Framework in managing database connections is to ensure efficient, secure, and transaction-aware usage of this critical resource. It abstracts the complexity of obtaining and releasing connections at the underlying level.
The key lies in lifecycle management: Spring advocates acquiring a connection only when needed and releasing it back to the pool (or closing it) as soon as it’s no longer in use, preventing resource leaks.
More importantly, it binds the connection to the current execution thread—especially within a transactional context. This ensures that multiple database operations within the same transaction share the same physical connection, maintaining atomicity and consistency (ACID properties).
Spring’s transaction management infrastructure automatically coordinates connection acquisition, binding, commit/rollback, and release. Developers typically do not need to handle connection details manually.
By simply focusing on business logic and using declarative transaction management (e.g., with @Transactional), the framework transparently manages connection lookup, reuse, and cleanup—greatly simplifying development while improving reliability and performance.
In the following sections, I will introduce the eight built-in connection control strategies provided by Spring.
2. Database Connection Methods
2.1 Using DataSource
Spring obtains database connections through a DataSource. DataSource is part of the JDBC specification and serves as a generic connection factory. It allows containers or frameworks to abstract away connection pooling and transaction management concerns from the application code.
We can configure our own DataSource using connection pool implementations provided by third-party libraries. Traditional choices include Apache Commons DBCP and C3P0; for modern JDBC connection pooling, HikariCP and its builder-based API are recommended.
The DriverManagerDataSource and SimpleDriverDataSource classes (included in the Spring distribution) should be used for testing purposes only! These variants do not provide connection pooling and perform poorly under multiple connection requests.
Below is an example configuration using a HikariCP data source:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}
2.2 Using DataSourceUtils
The DataSourceUtils class is a convenient and powerful helper that provides several static methods for obtaining connections from JNDI and closing them when necessary.
It supports JDBC connections that are bound to the current thread via DataSourceTransactionManager, and also works with connections managed by JtaTransactionManager and JpaTransactionManager.
It's important to note that JdbcTemplate implicitly uses DataSourceUtils in its internal implementation to access connections.
For every JDBC operation, JdbcTemplate relies on DataSourceUtils, thereby implicitly participating in the ongoing transaction.
Here is an example of how to use it:
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public void updateUserName(Long userId, String newName) {
Connection conn = null;
try {
// If currently within a transactional context, the connection obtained here
// will be the one already bound to the context.
conn = DataSourceUtils.getConnection(dataSource);
try (PreparedStatement ps = conn.prepareStatement("UPDATE users SET name = ? WHERE id = ?")) {
ps.setString(1, newName);
ps.setLong(2, userId);
ps.executeUpdate();
}
} catch (SQLException e) {
throw new RuntimeException("Update failed", e);
} finally {
// Key point: Release the connection via DataSourceUtils (won’t actually close it if within a transaction)
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
}SmartDataSource InterfaceSmartDataSource interface.The SmartDataSource interface is an “enhanced” version of the standard DataSource interface.
While DataSource already provides the basic functionality for obtaining a database connection, SmartDataSource goes a step further: it allows the implementing class to be asked,
“Hey, database connection provider, after I’m done with this connection, would you like me to return it to you (i.e., close it)?”
Why is this useful?
Imagine you’re performing a series of database operations that all need to use the same connection. If you close the connection after each operation and then get a new one again and again, it would be both time-consuming and inefficient.
But if you know that you’ll need the same connection for upcoming operations, you can tell the connection provider,
“I’m not done yet—don’t close it now; I’ll need it again shortly.”
So, the SmartDataSource interface gives connection providers an intelligent mechanism to decide whether a connection should be closed or kept open, depending on the context. This is especially useful when connection reuse is important, helping you save time and resources.
Here’s the method signature of the interface:
public interface SmartDataSource extends DataSource {
// Ask whether the connection should be closed
boolean shouldClose(Connection con);
}
The DataSourceUtils utility class mentioned above includes logic to check whether a DataSource is an instance of SmartDataSource when closing a connection. The relevant code is as follows:
public abstract class DataSourceUtils {
public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException {
if (!(dataSource instanceof SmartDataSource smartDataSource)
|| smartDataSource.shouldClose(con)) {
con.close();
}
}
}
2.4 Extending AbstractDataSource
AbstractDataSource is an abstract base class in Spring for DataSource implementations. It provides common functionality shared across most DataSource implementations. If you plan to create your own custom DataSource, you should extend the AbstractDataSource class.
Example implementation:
public class DBConnection extends AbstractDataSource {
@Override
public Connection getConnection() throws SQLException {
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
}
You only need to implement the core method for obtaining a Connection object.
This approach is typically used when you need to implement a custom data source or proxy a third-party data source.
2.5 Using SingleConnectionDataSource
The SingleConnectionDataSource class is an implementation of the SmartDataSource interface. It wraps a single database connection, and this connection is not closed after each use. However, it's important to note that it does not support multithreading.
If any client code (such as a persistence framework) calls the close() method assuming a connection pool is in use, you should set the suppressClose property to true. This enables a close-suppressing proxy to be returned, which wraps the actual physical connection and prevents it from being closed.
SingleConnectionDataSource is primarily intended for testing purposes. It is often used in combination with simple JNDI environments to facilitate testing code outside of an application server.
Compared to DriverManagerDataSource, SingleConnectionDataSource continually reuses the same connection, avoiding the overhead of repeatedly creating physical connections.
If you've ever used UReport, a web-based report designer tool, you might be familiar with SingleConnectionDataSource—this is the data source used internally by that reporting tool.
2.6 Using DriverManagerDataSource
The DriverManagerDataSource class is a standard implementation of the DataSource interface. It configures a plain JDBC driver through bean properties and returns a new connection every time it is called.
This implementation is particularly useful in testing and standalone environments outside of a Jakarta EE container. It can be used as a DataSource bean within the Spring IoC container or in conjunction with a simple JNDI environment.
Since each call to Connection.close() will actually close the connection (assuming no connection pool is used), any persistence code that expects a DataSource should work properly.
However, even in test environments, using a JavaBean-style connection pool (such as Commons DBCP) is very straightforward and almost always preferable to using DriverManagerDataSource.
Here’s an example configuration:
@Bean
public DriverManagerDataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("order_db");
dataSource.setPassword("");
return dataSource;
}
2.7 Using TransactionAwareDataSourceProxy
TransactionAwareDataSourceProxy is a proxy class that wraps a target DataSource and adds awareness of transactions managed by Spring.
In most cases, you won't need to use this class directly—unless you're working with legacy code or external libraries that require a standard JDBC DataSource but still want the benefits of Spring-managed transactions.
TransactionAwareDataSourceProxy is provided by Spring to wrap a non-Spring-managed JDBC data source and expose it as a proxy that is fully transaction-aware within the Spring context.
Here’s an example usage:
@Configuration
public class LegacyIntegrationConfig {
@Bean
public DataSource realDataSource() {
// Create the original data source (e.g., DBCP, HikariCP)
return new HikariDataSource(...);
}
@Bean
public TransactionAwareDataSourceProxy transactionAwareDataSource() {
// Wrap the original data source
return new TransactionAwareDataSourceProxy(realDataSource());
}
@Bean
public PlatformTransactionManager txManager() {
// Transaction manager uses the original data source
return new DataSourceTransactionManager(realDataSource());
}
@Bean
public LegacyDao legacyDao() {
// Inject the proxy data source into legacy code
return new LegacyDao(transactionAwareDataSource());
}
}
// Legacy code (cannot be modified)
public class LegacyDao {
private final DataSource dataSource;
public LegacyDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public void updateData() throws SQLException {
// If we don't use a proxy, the Connection obtained here
// will not be managed by Spring's transaction mechanism
try (Connection conn = dataSource.getConnection()) {
conn.prepareStatement("UPDATE db_product x SET x.name = 'xxxooo' WHERE x.id = 2").execute();
}
}
}
@Service
public class Service {
private final LegacyDao legacyDao;
// At this point, the legacy code is now part of Spring-managed transactions
// So the Connection obtained in updateData() will be managed by Spring's transaction system
@Transactional
public void update() {
this.legacyDao.updateData();
}
}
2.8 Using DataSourceTransactionManager
The DataSourceTransactionManager class is a PlatformTransactionManager implementation specifically designed for a single JDBC DataSource. It binds JDBC connections—obtained from the specified DataSource—to the currently executing thread, potentially providing one thread-bound connection per DataSource.
Application code should obtain JDBC connections via DataSourceUtils.getConnection(DataSource) rather than using the standard DataSource.getConnection method from Jakarta EE.
The DataSourceTransactionManager class supports savepoints (PROPAGATION_NESTED), custom isolation levels, and proper application of timeout settings as JDBC statement query timeouts. To support timeouts, application code must either use JdbcTemplate or invoke DataSourceUtils.applyTransactionTimeout(..) for each created statement.
Here is an example usage:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager txManager = new DataSourceTransactionManager();
txManager.setDataSource(dataSource);
txManager.setDefaultTimeout(30); // Default timeout is 30 seconds
// We can add more configuration here if needed
return txManager;
}
In a Spring Boot environment, this will override the system's default transaction manager.

Comments
Post a Comment