← all writing

Spring Boot patterns that actually scale

  • java
  • backend

Most “does Spring Boot scale?” arguments are really arguments about four decisions that have nothing to do with Spring. I have watched the same four go wrong in services written in three different languages.

Connection pools are a capacity plan, not a default

The default pool size is a number someone picked so the framework would start. It is not a capacity plan. The pool is the narrowest part of your service, and its correct size is a function of the database, not the application: roughly connections ≈ cores × 2 + effective spindles, which for a managed Postgres instance is usually a much smaller number than people expect.

spring.datasource.hikari.maximum-pool-size=16
spring.datasource.hikari.connection-timeout=2000
spring.datasource.hikari.leak-detection-threshold=20000

The timeout matters as much as the size. A pool with no acquisition timeout converts database slowness into thread exhaustion, and thread exhaustion converts one slow query into a total outage. Two seconds and a clear error is almost always better than waiting.

Idempotency is a table, not a keyword

Every retryable write endpoint needs a key that the caller supplies and the server remembers. Not a lock, not a dedupe cache with a TTL you guessed — a row with a unique constraint, written in the same transaction as the effect.

ApproachSurvives restartSurvives two instancesHonest
In-memory setNoNoNo
Redis with TTLYesYesOnly within the TTL
Unique row in the same transactionYesYesYes

The third option is more work on the write path and it is the only one that is actually true. Everything else is a cache that people describe as a guarantee.

Circuit breakers protect you, not them

A breaker around a downstream dependency is not politeness toward that dependency. It is how you stop its latency from becoming your latency, and then your caller’s. The important setting is not the failure threshold — it’s what the fallback returns. A fallback that throws is a breaker that only changes the error message.

Decide, per dependency, what a degraded answer looks like: a stale value, an empty list, a partial response with a flag. If there is no acceptable degraded answer, you do not have a dependency, you have a hard requirement, and the breaker should be replaced by a health check that takes the instance out of rotation.

Observability before optimization

The last one is a habit rather than a pattern. Three signals — request rate, error rate, and a real latency histogram — answer nearly every “is it scaling?” question. Without them, every performance discussion is an argument between two people’s intuitions, and the framework usually gets blamed for a pool size.