System Design/03 - Qualidade/Performance Antipatterns2 min
Improper Instantiation
- Que problema isso resolve?
- Quando usar / quando NÃO usar?
- Qual o principal trade-off?
- Como isso falha em produção?
Conceito
Criar e destruir repetidamente objetos que deveriam ser compartilhados e reutilizados — tipicamente clientes que encapsulam conexões.
O custo escondido não é a alocação: é o handshake. Um cliente HTTP novo abre conexão TCP (1 RTT), negocia TLS (1 RTT) e descarta tudo ao final. Repetido por requisição, isso multiplica latência e esgota portas efêmeras.
Objetos que devem ser singleton
| Objeto | Por quê |
|---|---|
HttpClient |
Pool de conexões interno |
| Pool de conexões de banco | Handshake + autenticação caros |
| Cliente de SDK (S3, Redis, Kafka) | Conexões, cache de metadados, credenciais |
| Serializadores configurados | Compilação de schema |
| Regex compilada | Compilação do padrão |
| Logger | Handles de arquivo |
O sintoma clássico
SocketException: address already in use ou esgotamento de portas efêmeras sob carga. Cada conexão fechada fica em TIME_WAIT por ~2×MSL; criar milhares por segundo esgota o intervalo de portas disponíveis.
Trade-offs
O erro oposto também existe: compartilhar objetos que não são thread-safe. Um SimpleDateFormat em Java ou uma conexão de banco compartilhada entre threads produz corrupção silenciosa de dados — bem pior que lentidão.
A regra é: compartilhe o que é thread-safe e caro de criar; crie por operação o que é barato ou mutável.
Há um caso intermediário mal compreendido: o HttpClient do .NET é thread-safe e deve ser singleton, mas um singleton eterno não percebe mudanças de DNS. A solução (IHttpClientFactory, ou PooledConnectionLifetime) recicla conexões periodicamente sem recriar o cliente — um exemplo de que "singleton" nem sempre significa "para sempre".
Pool mal dimensionado é o outro lado: pequeno demais vira gargalo invisível (as requisições esperam por conexão), grande demais esgota os limites do banco.
Exemplo prático
// cada chamada: nova conexão, novo TLS, socket em TIME_WAIT
public async Task<string> Buscar(string url) {
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// compartilhado: pool reutilizado
private static readonly HttpClient _client = new HttpClient();
public async Task<string> Buscar(string url)
=> await _client.GetStringAsync(url);
A diferença medida costuma ser de uma ordem de grandeza em throughput. E o modo de falha da primeira versão é particularmente cruel: ela funciona perfeitamente em testes e em carga baixa, e só falha em produção sob pico — com um erro de socket que não aponta em nada para o using que o causou.
Relacionado
Parte de Performance Antipatterns · roadmap.sh/system-design