trilha

System Design/03 - Qualidade/Performance Antipatterns2 min

Improper Instantiation

Perguntas-guia
  • 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

Buscar

Busca por título, seção e texto das notas