Design Patterns

Singleton

Garante uma única instância de uma classe e fornece um ponto de acesso global

Conceito e intenção

Singleton garante que uma classe tenha apenas uma instância durante toda a vida da aplicação, fornecendo um ponto de acesso global a ela.

O problema motivador é a existência de recursos que precisam ser únicos por design: um pool de conexões com banco de dados não pode ter duas instâncias criando conexões independentes; um cache em memória precisa ser compartilhado; um registrador de configurações carregado do disco deve ser lido uma só vez. Sem controle, new chamado em vários pontos do sistema criaria instâncias duplicadas, gerando inconsistências.

Singleton resolve isso tornando o construtor privado e expondo a instância através de um método ou campo estático. O padrão tem má reputação — não sem motivo — porque quando mal usado cria estado global e dificulta testes. Em aplicações modernas com contêineres de IoC (Spring, CDI), você raramente precisa implementar o padrão manualmente.


Estrutura

┌─────────────────────────────────────────┐
│              Singleton                  │
│  - instance: Singleton  (static)        │
│  - Singleton()          (private)       │
│  + getInstance(): Singleton (static)    │
│  + operacao()                           │
└─────────────────────────────────────────┘

Participantes:

  • Singleton — a classe que controla a própria criação e expõe a única instância

Implementação Java — Double-Checked Locking

// Gerenciador de configuração carregado do disco — deve existir uma única vez
public final class ConfigManager {
    // volatile garante visibilidade entre threads sem sincronização total
    private static volatile ConfigManager instance;
    private final Map<String, String> properties;

    private ConfigManager() {
        // construtor privado — impede new ConfigManager()
        this.properties = loadFromDisk(); // operação cara — feita uma vez
    }

    // Double-checked locking — thread-safe e lazy
    public static ConfigManager getInstance() {
        if (instance == null) {                      // 1ª verificação: sem lock (rápido)
            synchronized (ConfigManager.class) {
                if (instance == null) {              // 2ª verificação: dentro do lock
                    instance = new ConfigManager();
                }
            }
        }
        return instance;
    }

    public String get(String key) {
        return properties.getOrDefault(key, "");
    }

    public String get(String key, String defaultValue) {
        return properties.getOrDefault(key, defaultValue);
    }

    private Map<String, String> loadFromDisk() {
        // lê application.properties, env vars, etc.
        Map<String, String> props = new LinkedHashMap<>();
        // ... carregamento real
        return Collections.unmodifiableMap(props);
    }
}

// Uso
String dbUrl  = ConfigManager.getInstance().get("db.url");
int timeout   = Integer.parseInt(ConfigManager.getInstance().get("db.timeout", "5000"));

Variação com Initialization-on-Demand Holder

A abordagem mais elegante em Java. Usa a garantia da JVM de que classes internas estáticas são inicializadas apenas uma vez, na primeira vez que são acessadas:

public final class ConfigManager {
    private final Map<String, String> properties;

    private ConfigManager() {
        this.properties = loadFromDisk();
    }

    // Holder inicializado só quando getInstance() é chamado pela primeira vez
    private static final class Holder {
        static final ConfigManager INSTANCE = new ConfigManager();
    }

    public static ConfigManager getInstance() {
        return Holder.INSTANCE; // thread-safe sem synchronized
    }

    public String get(String key) {
        return properties.getOrDefault(key, "");
    }
}

Este idiom (também chamado de Initialization-on-demand Holder) é lazy, thread-safe e não usa volatile nem synchronized explícitos.


Variação com Enum (recomendada para casos simples)

public enum AppConfig {
    INSTANCE;

    private final Map<String, String> properties;

    // Bloco de instância — executado uma vez pela JVM
    {
        properties = loadFromDisk();
    }

    public String get(String key) {
        return properties.getOrDefault(key, "");
    }

    public String get(String key, String defaultValue) {
        return properties.getOrDefault(key, defaultValue);
    }

    private Map<String, String> loadFromDisk() {
        // carregamento real
        return new HashMap<>();
    }
}

// Uso — sem getInstance()
String url = AppConfig.INSTANCE.get("db.url");

A JVM garante que constantes enum são inicializadas uma única vez, mesmo em ambientes multi-thread. Adicionalmente, enums são protegidos contra desserialização e reflexão criarem instâncias extras — algo que o double-checked locking não garante sem mais código.


Singleton com Spring (forma recomendada em produção)

// Não implemente o padrão manualmente — o container garante singleton
@Configuration
public class InfrastructureConfig {

    @Bean  // escopo padrão = singleton
    public ConnectionPool connectionPool() {
        return ConnectionPool.builder()
            .url(env.getProperty("db.url"))
            .maxSize(20)
            .build();
    }

    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager("orders", "products");
    }
}

// Injeção normal — Spring cuida da instância única
@Service
public class OrderService {
    private final ConnectionPool pool;   // singleton injetado
    private final CacheManager cache;   // singleton injetado

    public OrderService(ConnectionPool pool, CacheManager cache) {
        this.pool  = pool;
        this.cache = cache;
    }
}

Vantagens sobre o Singleton clássico: injeção de dependência explícita, testável (injete mock no teste), sem estado global acessível por qualquer código.


Implementação TypeScript

// Módulo como singleton natural (Node.js/browser com bundler)
// O módulo é avaliado uma vez — o objeto exportado é a instância única

class ConfigManager {
  private static instance: ConfigManager | null = null;
  private readonly props: Map<string, string>;

  private constructor() {
    this.props = this.loadConfig();
  }

  static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  get(key: string, defaultValue = ''): string {
    return this.props.get(key) ?? defaultValue;
  }

  private loadConfig(): Map<string, string> {
    return new Map(Object.entries(process.env as Record<string, string>));
  }
}

// Versão idiomática em TypeScript: módulo singleton
// config.ts
const props = new Map<string, string>(Object.entries(process.env as Record<string, string>));
export const config = {
  get: (key: string, defaultValue = '') => props.get(key) ?? defaultValue,
};

// Uso
import { config } from './config';
const dbUrl = config.get('DATABASE_URL');

No mundo real

Java Runtime.getRuntime() — retorna a instância única do runtime JVM. É um Singleton clássico com método estático privado.

System.out e System.err — são campos estáticos finais que apontam para uma única instância de PrintStream. Singleton por atribuição estática.

Spring ApplicationContext — dentro de uma aplicação Spring, o contexto é efetivamente um Singleton. Todos os beans com escopo singleton (padrão) existem uma única vez no contexto.

Connection pools (HikariCP, c3p0) — geralmente configurados como singleton via Spring ou injeção manual. Criar múltiplos pools seria desperdício de conexões.

Logback/SLF4J LoggerFactoryLoggerFactory.getLogger(...) retorna instâncias a partir de um registry centralizado. O factory em si é um Singleton.

Node.js require() — módulos são cacheados após a primeira avaliação. Exportar um objeto de um módulo é o padrão singleton mais simples possível em Node.


Quando usar

  • Recursos que têm custo alto de inicialização e devem ser compartilhados (pool de conexões, cache)
  • Estado de configuração imutável carregado uma vez na inicialização
  • Registros de serviço (service registry, plugin registry)
  • Gerenciadores de recursos exclusivos de hardware (porta serial, acesso a arquivo de lock)

Quando NÃO usar

  • Quando você tem um contêiner de IoC: Spring, CDI, Guice — todos gerenciam singletons para você. Implementar manualmente é redundante e piora a testabilidade.
  • Quando o estado muda: Singleton mutável é estado global. Múltiplos threads acessando estado mutável sem sincronização resulta em race conditions.
  • Quando dificulta testes: getInstance() estático não pode ser mockado facilmente. Prefira injeção de dependência — o teste injeta um fake.
  • Para substituir injeção de dependência: O antipadrão “ServiceLocator” usa Singletons para resolver dependências globalmente — isso obscurece as dependências reais de cada classe.

Problemas clássicos e soluções

Serialização quebra o Singleton: objetos serializados e desserializados criam nova instância. Solução: implementar readResolve() ou usar Enum (imune por design).

// Proteção contra deserialização
protected Object readResolve() {
    return getInstance();
}

Reflexão pode invocar construtor privado: Constructor.setAccessible(true) burla o modificador privado. Enum é a única forma completamente segura.

Classloaders diferentes: Em ambientes como servidores de aplicação com multiple classloaders, a mesma classe pode existir em loaders distintos — cada um com seu “singleton”. Não é um problema comum em aplicações Spring Boot convencionais.


Combinações com outros padrões

Singleton + Factory Method: A Factory pode ser um Singleton, garantindo um único ponto de criação de objetos. NotificationFactory.getInstance() expõe a factory como instância única.

Singleton + Facade: Facades frequentemente são singletons — existe uma instância do CheckoutFacade gerenciada pelo container que orquestra os subsistemas.

Singleton + Registry: Um Registry de strategies, handlers ou plugins é naturalmente um Singleton — há um registro central compartilhado por toda a aplicação.