Hook: Interviewers love this question because it reveals whether you understand resource starvation, not just SQL errors.
Question: What does Database Connection Pool exhausted mean in Spring Boot?
Answer: It means every connection in the pool is already borrowed, so a new request must wait. If no connection is returned before connectionTimeout, the pool throws an error such as SQLTransientConnectionException or a Spring wrapper around it. In practice, this usually comes from a leaked connection, a very long transaction, or a pool that is too small for the traffic.
Interview-Ready Answer: In Spring Boot, a database connection pool is a limited set of ready-to-use database connections. If all of them are in use and my code asks for another one, the request waits until a connection is returned; if that wait exceeds the pool timeout, I get a pool-exhausted failure. My first checks are always: are connections being closed properly, are transactions too long, and is the pool size reasonable for the database and traffic?
Detailed Explanation: In modern Spring Boot, the usual pool is HikariCP, which keeps a fixed number of open database connections ready to borrow. Pool exhausted does not always mean the database is dead; it usually means the app has run out of available connections right now. That is a form of starvation: a thread keeps waiting because a shared resource is busy.
JdbcTemplate, JPA, or a custom DAO.@Transactional is dangerous.connectionTimeout, Hikari throws a timeout error and Spring may wrap it in a data-access exception.| Situation | What it usually means | Best first fix |
|---|---|---|
| Leak | Connections never come back | Use try-with-resources and close every resource |
| Long transaction | Connections are held too long | Shorten the transaction and remove remote calls from it |
| Legit spike | Demand briefly exceeds supply | Measure before increasing pool size |
| Slow query | DB work is the real bottleneck | Add indexes, tune SQL, inspect the plan |
Performance notes: Borrow and return are effectively O(1) when a connection is free; the pain is the wait, which is bounded by connectionTimeout. The pool itself uses space proportional to its size, and each extra live connection also costs memory and backend work on the database server. HikariCP commonly defaults to maximumPoolSize=10 and connectionTimeout=30000ms, with idleTimeout=600000ms and maxLifetime=1800000ms. Too large a pool can actually reduce throughput because the database still has finite CPU, memory, and worker threads.
Version note: In Spring Boot 2.x and 3.x, HikariCP is the default pool when it is on the classpath. Older setups could use a different pool depending on dependencies, so always check which pool implementation your app is really using.
Edge cases: Pool exhaustion and database outage are different. If the database is down, the app may fail while trying to create or validate new connections; if the pool is exhausted, the connections exist but are all busy. Also, a slow external call inside a transaction can make the pool look broken even when the SQL itself is fine.
Memory model: A pool is not the database; it is the waiting room in front of the database. If everyone keeps a seat forever, the next person at the door has nowhere to sit.
Real-World Example: In an ecommerce checkout service, one method wrapped payment validation and order writes in a single @Transactional block. During a flash sale, the payment provider slowed down, so each request held a connection for 4 to 5 seconds. The 10-connection pool emptied, new requests started timing out, and customers saw failed checkouts even though the database CPU looked normal.
The logs showed messages like Connection is not available, request timed out after 30000ms, Tomcat worker threads piled up, and support tickets mentioned carts that never completed. The real bug was not the database itself; it was the app holding scarce connections while waiting on something unrelated to SQL.
package com.example.pooldemo;\n\nimport com.zaxxer.hikari.HikariDataSource;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.jdbc.core.JdbcTemplate;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\n\n@SpringBootApplication\npublic class PoolExhaustionDemoApplication {\n\n public static void main(String[] args) {\n SpringApplication.run(PoolExhaustionDemoApplication.class, args);\n }\n\n @Bean\n DataSource dataSource() {\n // A tiny pool makes exhaustion easy to reproduce in a demo.\n // HikariCP is Spring Boot's usual default pool.\n HikariDataSource ds = new HikariDataSource();\n ds.setJdbcUrl("jdbc:h2:mem:pooldemo;DB_CLOSE_DELAY=-1;MODE=PostgreSQL");\n ds.setUsername("sa");\n ds.setPassword("");\n ds.setMaximumPoolSize(2);\n ds.setConnectionTimeout(2000);\n ds.setPoolName("DemoPool");\n return ds;\n }\n\n @Bean\n CommandLineRunner demo(DataSource dataSource, JdbcTemplate jdbcTemplate) {\n return args -> {\n jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS demo (id INT PRIMARY KEY, name VARCHAR(50))");\n jdbcTemplate.update("INSERT INTO demo VALUES (?, ?)", 1, "Alice");\n\n ExecutorService executor = Executors.newFixedThreadPool(2);\n CountDownLatch twoConnectionsBorrowed = new CountDownLatch(2);\n List<Future<?>> futures = new ArrayList<>();\n\n for (int i = 1; i <= 2; i++) {\n final int workerId = i;\n futures.add(executor.submit(() -> {\n try (Connection connection = dataSource.getConnection()) {\n System.out.println("Worker " + workerId + " borrowed a connection.");\n twoConnectionsBorrowed.countDown();\n\n // Hold the connection on purpose.\n // This simulates a long transaction or a slow call inside @Transactional.\n Thread.sleep(5000);\n\n System.out.println("Worker " + workerId + " returned the connection.");\n } catch (SQLException e) {\n throw new RuntimeException(e);\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n throw new RuntimeException(e);\n }\n }));\n }\n\n // Wait until both pool slots are taken before we test the failure path.\n if (!twoConnectionsBorrowed.await(3, TimeUnit.SECONDS)) {\n throw new IllegalStateException("Workers did not borrow connections in time.");\n }\n\n try (Connection ignored = dataSource.getConnection()) {\n System.out.println("Unexpected: third connection was available.");\n } catch (SQLException e) {\n System.out.println("Expected exhaustion: " + e.getClass().getSimpleName() + " - " + e.getMessage());\n }\n\n for (Future<?> future : futures) {\n future.get();\n }\n\n Integer rows = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM demo", Integer.class);\n System.out.println("Pool recovered; rows in demo table = " + rows);\n\n executor.shutdown();\n };\n }\n}Follow-up & Tricky Questions:
@Transactional methods contribute? They hold one connection for the whole method, so slow loops or remote calls inside the transaction can starve the pool even if the SQL is simple.Common Mistakes:
try-with-resources. Correction: Let the language close the connection automatically.Memory Hook: Think of the pool as a parking lot: if every car stays parked, the next driver circles until the ticket machine times out.
Cheat Sheet:
Practice Tasks:
try-with-resources.