DEV Community

Cover image for The Secret Life of Java: The Connection Pool #java #backend #springboot #startup
Aswin A
Aswin A

Posted on

The Secret Life of Java: The Connection Pool #java #backend #springboot #startup

How connection leaks happen and why "max-connections" is the bottleneck you didn't see coming.

James rubbed his temples. The marketing email for his SaaS startup had just gone out. Traffic was spiking. But instead of celebrating, he was staring at a stack trace that was growing longer by the second.

"I don't understand," James muttered, typing furiously. "The server has 32GB of RAM. The CPU is barely at 10%. Why is the API timing out?"

Sarah, a freelance site reliability engineer who had stopped by to help, pulled a chair up to his desk. She pointed at a specific line in the logs: ConnectionTimeoutException.

"Because you aren't running out of RAM, James. You're running out of phone lines."

The Pool (Resources are Expensive)

"The Database Connection Pool," Sarah explained. "Think of your Postgres database like a very exclusive customer service center. It involves a heavy 'handshake'—TCP connection, authentication, SSL—just to say hello. It's expensive to open a new line for every single user."

"So we use a pool," James said. "I know this. Spring Boot uses HikariCP by default."

"Right. You have a pool of, say, 10 open connections ready to be used. When a request comes in, your app borrows a connection, does its SQL business, and returns it."

"I have 10 connections," James argued. "But I have 500 users right now. Should I bump the pool to 500?"

"No," Sarah said sharply. "If you open 500 connections, the database CPU will spend all its time context switching. You'll kill the database. 10 to 20 is usually enough for thousands of transactions per second—if you return them quickly."

The Lease (Borrow and Return)

Sarah drew a bucket on the whiteboard.

"This is the Pool. It has 10 marbles."

She drew a standard request flow:

Borrow: Controller receives request -> Service asks for connection.

Use: Service runs SELECT * FROM users.

Return: Service finishes -> Connection goes back to the bucket.

"The total time for this should be milliseconds," she said.

The Leak (The Phantom Hold)

Sarah pointed at James's code on the screen. "Show me the processOrder function."

`@Transactional
public void processOrder(Order order) {
// 1. Save order status to DB (Borrows Connection)
repository.save(order);

// 2. Send confirmation email via 3rd party API (Slow!)
emailService.sendConfirmation(order); 

// 3. Update audit log
auditRepository.save(new Log("Email sent"));
Enter fullscreen mode Exit fullscreen mode

}
// Transaction ends, Connection returned.`

"Trace the Lease," Sarah commanded.

James looked at the code.

The method is @Transactional.

The transaction starts. A connection is borrowed from the pool.

He saves the order.

He calls the external Email API.

He updates the log.

The function ends. The connection is returned.

"I see the problem," James whispered, his face paling.

"The Email API," Sarah nodded. "How long does that take?"

"Sometimes... two seconds. Maybe three if their server is busy."

"Exactly. For those three seconds, you are holding a database connection hostage while waiting for an email server. You aren't even using the database! You're just holding the marble in your pocket."

The Bottleneck

"If your pool size is 10," Sarah calculated, "and every request takes 3 seconds because of that email call, your maximum throughput is... 3 requests per second."

"That's why the site is down," James realized. "The 11th user is waiting in line for a connection that is sitting idle in a Java thread waiting for an email response."

The Solution

"How do I fix it? I need to send that email."

"You must narrow the scope," Sarah said. "Don't hold the resource while you're doing non-database work."

She refactored the code on the whiteboard:

`public void processOrder(Order order) {
// 1. Save order (Transaction 1)
saveOrderInternal(order);

// 2. Send email (No DB connection held here!)
emailService.sendConfirmation(order);

// 3. Update log (Transaction 2)
saveAuditLog(new Log("Email sent"));
Enter fullscreen mode Exit fullscreen mode

}

@Transactional
public void saveOrderInternal(Order order) {
repository.save(order);
}
`

"By removing @Transactional from the top level, you only borrow the connection for the milliseconds it takes to write the SQL," Sarah explained. "During the 3-second email pause, the connection is back in the pool, serving hundreds of other users."

The Conclusion

James pushed the fix. The graphs on his dashboard instantly relaxed. CPU remained steady, but throughput skyrocketed.

"Full stack isn't just about knowing Java and React," Sarah said, picking up her bag. "It's about understanding the economy of resources. Your code might be logical, but if it's greedy with shared resources, your startup won't scale past the 'Hello World' phase."

Top comments (0)