Se você trabalha com aplicações Spring Boot em produção, já deve ter enfrentado aquele momento em que o sistema começa a engasgar sob carga. Latência subindo, threads bloqueadas, e quando você investiga, o gargalo está lá: conexões com o banco de dados.
Nos últimos meses, trabalhando com sistemas event-driven que processam milhares de transações por segundo, aprendi que a forma como você gerencia conexões com o PostgreSQL pode ser a diferença entre uma aplicação que responde em 11ms e uma que trava em produção.
E o HikariCP está no centro dessa conversa.
Antes de falar de pool, vale entender o custo real de abrir uma conexão com o banco.
// Sem connection pool
Connection conn = DriverManager.getConnection("jdbc:postgresql://...", "admin", "admin");
// Criar essa conexão demora ~100-200ms
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM transaction_events");
ResultSet rs = stmt.executeQuery();
// ... processar dados
conn.close(); // Fecha e destrói a conexão
Cada vez que a aplicação precisa falar com o PostgreSQL sem pool, acontece:
Funciona... até não funcionar.
Com 1000 requisições por segundo, você tem 1000 conexões simultâneas. O PostgreSQL tem limites. O servidor tem limites. O resultado é previsível: crash.
A ideia é simples. Em vez de criar e destruir conexões a cada request, você mantém um conjunto de conexões já abertas e prontas pra uso.
// Com connection pool
Connection conn = dataSource.getConnection(); // <1ms - reutiliza conexão existente
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM transaction_events");
ResultSet rs = stmt.executeQuery();
// ... processar dados
conn.close(); // NÃO fecha realmente - devolve ao pool
A diferença é brutal:
O conn.close() aqui é enganoso. Ele não fecha a conexão de verdade. Ele devolve ao pool pro próximo que precisar.
O Spring Boot escolheu o HikariCP como connection pool padrão por um motivo: ele é o mais rápido e leve do ecossistema Java.
O nome vem do japonês hikari (luz) - e não é exagero. Comparado com alternativas como C3P0 ou DBCP2, o HikariCP entrega latências significativamente menores com footprint de memória reduzido.
Se você usa Spring Boot com starter de JPA ou JDBC, o HikariCP já está lá. Sem configuração adicional. Mas usar o padrão é uma coisa. Configurar pra produção é outra.
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
minimum-idle=5 significa que o HikariCP mantém 5 conexões abertas o tempo todo, mesmo sem tráfego. São conexões "quentes", prontas pra responder imediatamente aos primeiros requests.
maximum-pool-size=20 é o limite. Se as 20 conexões estão ocupadas e chega o request 21, ele espera na fila. Esse limite existe pra proteger o PostgreSQL - um pool de 500 conexões não vai melhorar sua performance, vai matar seu banco.
Vale mencionar: a documentação do HikariCP recomenda configurar minimum-idle igual ao maximum-pool-size, criando um pool de tamanho fixo. Isso evita o overhead de criar e destruir conexões durante flutuações de carga. Na prática, uso pool dinâmico em ambientes com múltiplos serviços pra conservar recursos, mas pool fixo dá latência mais previsível sob carga sustentada.
Na prática, o dimensionamento depende do ambiente:
| Ambiente | Min Idle | Max Pool | Justificativa |
|---|---|---|---|
| DEV | 5 | 20 | Um developer, pouco tráfego |
| TST | 10 | 30 | Testes automatizados, carga média |
| PP | 15 | 40 | Simulação de produção |
| PRD | 20 | 50 | Múltiplos handlers e tipos de transação |
Um erro comum é configurar maximum-pool-size alto demais. O PostgreSQL tem seu próprio limite (tipicamente max_connections = 100 por padrão). Se você tem 4 instâncias da aplicação, cada uma com pool de 50, são 200 conexões competindo por 100 slots. Dimensione seus pools considerando a topologia completa de deploy.
spring.datasource.hikari.connection-timeout=30000 # 30 segundos
spring.datasource.hikari.idle-timeout=300000 # 5 minutos
spring.datasource.hikari.max-lifetime=1800000 # 30 minutos
Cada um tem um papel específico:
connection-timeout (30s) - Tempo máximo que uma thread espera por uma conexão livre. Se todas as 20 estão ocupadas e passam 30 segundos, o HikariCP lança exceção. Quando isso acontece, o problema geralmente é outro: queries lentas bloqueando conexões, pool subdimensionado ou connection leaks.
idle-timeout (5min) - Conexões ociosas além desse tempo são fechadas. Isso economiza recursos em períodos de baixo tráfego, mas o pool nunca fica abaixo do minimum-idle.
O comportamento na prática:
08:00 -> Sistema arranca -> cria 5 conexões (minimum-idle)
09:00 -> Pico de tráfego -> escala para 20 conexões
10:00 -> Tráfego normaliza -> apenas 3 conexões em uso
10:05 -> 12 conexões fechadas (ociosas há 5 min)
10:05 -> Ficam 5 conexões abertas (minimum-idle) + 3 em uso
max-lifetime (30min) - Toda conexão é marcada para reciclagem após 30 minutos. Porém, uma conexão em uso não é fechada forçadamente -- ela só é removida quando devolvida ao pool. Isso previne problemas reais: memory leaks no driver JDBC, conexões TCP em estado inconsistente e rotação de credenciais no PostgreSQL.
spring.datasource.hikari.leak-detection-threshold=60000 # 60 segundos
Um connection leak acontece quando uma conexão é retirada do pool e nunca devolvida. O efeito é lento e silencioso: o pool vai esvaziando até travar a aplicação inteira.
O código clássico que causa isso:
@Service
public class BadService {
@Autowired
private DataSource dataSource;
public void processTransaction() throws SQLException {
Connection conn = dataSource.getConnection();
// ... faz queries
// ESQUECEU de conn.close()!
// Conexão nunca volta ao pool
}
}
Com leak-detection-threshold=60000, o HikariCP identifica qualquer conexão que ficou fora do pool por mais de 60 segundos e loga um warning com o stack trace completo. Você sabe exatamente onde o leak está.
A correção é direta. Use try-with-resources:
public void processTransaction() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
// ... faz queries
} // Fecha automaticamente, mesmo com exceção
}
Ou melhor ainda, deixe o Spring gerenciar com @Transactional:
@Service
public class TransactionService {
@Autowired
private TransactionRepository repository;
@Transactional // Spring abre e fecha conexão automaticamente
public void processTransaction(TransactionEvent event) {
repository.save(event);
}
}
Além do pool em si, as propriedades do HikariCP permitem passar configurações diretamente pro driver JDBC do PostgreSQL (PgJDBC). Duas otimizações se destacam.
O driver JDBC do PostgreSQL suporta prepared statements no lado do servidor, que pulam as fases de parse e planejamento pra queries executadas frequentemente. Por padrão, após uma PreparedStatement ser executada 5 vezes (prepareThreshold=5), o PgJDBC automaticamente promove ela pra um prepared statement no servidor. As execuções seguintes reutilizam o plano de execução já cacheado.
spring.datasource.hikari.data-source-properties.prepareThreshold=5
spring.datasource.hikari.data-source-properties.preparedStatementCacheQueries=256
spring.datasource.hikari.data-source-properties.preparedStatementCacheSizeMiB=5
prepareThreshold=5 -- Número de execuções antes do PgJDBC usar prepared statements no servidor (padrão: 5)preparedStatementCacheQueries=256 -- Máximo de queries cacheadas no lado do cliente (padrão: 256)preparedStatementCacheSizeMiB=5 -- Memória máxima pro cache no lado do cliente (padrão: 5 MiB)Pra queries OLTP simples, o overhead de parse/plan é tipicamente 1-5ms. Pra queries complexas com múltiplos joins, pode chegar a 10-50ms. Em um sistema event-driven onde as mesmas queries executam milhares de vezes por minuto, server-side prepared statements eliminam esse overhead após as primeiras execuções.
Essa configuração é crítica pra quem usa padrões como Transactional Outbox.
spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true
Sem rewrite, inserir 100 registros na tabela outbox significa 100 round-trips individuais ao banco:
INSERT INTO outbox (id, topic, payload) VALUES ('1', 'order.created', '...');
INSERT INTO outbox (id, topic, payload) VALUES ('2', 'order.created', '...');
-- ... 98 mais ...
Com reWriteBatchedInserts=true, o driver JDBC reescreve automaticamente pra um único statement:
INSERT INTO outbox (id, topic, payload) VALUES
('1', 'order.created', '...'),
('2', 'order.created', '...'),
-- ... 98 mais ...
('100', 'order.created', '...');
A melhoria depende da latência de rede entre aplicação e PostgreSQL. Em ambientes de alta latência (cross-region ou cloud), a redução pode ser dramática -- de segundos pra centenas de milissegundos. Em ambientes de baixa latência (mesmo datacenter), o ganho absoluto é menor, mas a melhoria relativa ainda é significativa. Se você persiste eventos em tabela de outbox, essa otimização é obrigatória.
Pra deixar concreto, veja como tudo funciona junto em um sistema real que processa eventos do Solace.
@Component
public class OrderCreatedConsumer extends AbstractConsumer {
private final EventStoreService eventStoreService;
@Override
public void process(final Message message) {
final var event = parse(message);
eventStoreService.append(event, "order.created");
}
}
@Service
public class EventStoreService {
private final TransactionEventRepository eventRepo;
private final OutboxRepository outboxRepo;
@Transactional // Spring pede conexão ao HikariCP
public void append(TransactionEvent event, String topic) {
// HikariCP entrega conexão idle em <1ms
var eventEntity = toEntity(event);
var outboxEntry = toOutboxEntry(event, topic);
eventRepo.save(eventEntity); // INSERT com prepared stmt no servidor
outboxRepo.save(outboxEntry); // INSERT com prepared stmt no servidor
// COMMIT -> conexão volta ao pool
}
}
A timeline detalhada:
t=0ms -> Evento chega ao handler
t=1ms -> @Transactional pede conexão ao HikariCP
t=1ms -> HikariCP entrega conexão #7 (estava idle)
t=2ms -> BEGIN transaction no PostgreSQL
t=5ms -> INSERT transaction_events (prepared stmt em cache)
t=8ms -> INSERT outbox (prepared stmt em cache)
t=10ms -> COMMIT transaction
t=11ms -> Conexão devolvida ao pool
Total: 11ms (vs. 100ms+ sem pool)
De 100ms+ pra 11ms. Isso é o que separa uma aplicação que aguenta carga de uma que trava.
Configurar o pool é só metade do trabalho. Em produção, você precisa monitorar.
Com Spring Boot Actuator, as métricas do HikariCP ficam disponíveis via endpoint:
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
As métricas que importam:
| Métrica | Significado | Valor Saudável |
|---|---|---|
hikaricp.connections.active | Conexões em uso agora | < 80% do max |
hikaricp.connections.idle | Conexões disponíveis | > 20% do max |
hikaricp.connections.pending | Threads esperando conexão | 0 |
hikaricp.connections.timeout | Timeouts acumulados | 0 |
Três alertas que você deve configurar no Grafana/Prometheus:
HikariPool-1 - Connection is not available, request timed out after 30000ms.
Esse é o erro mais comum. Pool esgotado. Diagnóstico:
hikaricp.connections.active - se está no máximo, o pool está pequeno ou tem queries lentashikaricp.connections.pending - se maior que zero, threads estão bloqueadasdocker exec -it postgres psql -U admin -d mydb \
-c "SELECT pid, now() - query_start as duration, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY duration DESC;"
Se você vê no log:
HikariPool-1 - Added connection conn10
HikariPool-1 - Closing connection conn10 (idle timeout)
HikariPool-1 - Added connection conn11
O minimum-idle está baixo demais ou o idle-timeout está curto demais. Aumente ambos:
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=600000 # 10 minutos
Se a memória da aplicação cresce constantemente, ative leak detection agressivo temporariamente:
spring.datasource.hikari.leak-detection-threshold=10000 # 10 segundos
Procure no log por Connection leak detected, analise o stack trace e corrija com @Transactional ou try-with-resources.
Faça:
@Transactional - deixe o Spring gerenciar conexõestry-with-resources quando precisar de conexões manuaisNão faça:
Thread.sleep() dentro de @Transactionalmaximum-pool-size=500 achando que mais é melhorConnection pooling não é um detalhe de configuração. É uma decisão arquitetural que impacta diretamente a performance e estabilidade da sua aplicação em produção.
O HikariCP faz um trabalho excelente como padrão do Spring Boot, mas entender o que cada parâmetro faz e como monitorar é o que separa uma configuração que funciona em dev de uma que sobrevive em produção.
Nos próximos posts, vou explorar mais a fundo padrões como Transactional Outbox e Event Sourcing com PostgreSQL, onde o connection pooling se torna ainda mais crítico.
Se você quer trocar ideias sobre connection pooling, performance com PostgreSQL ou arquitetura event-driven, me procure no LinkedIn. Adoro essas conversas.
Recursos para aprofundar:
