CriacionalIniciante6 min de leitura8 seções1994, Marco de referência: a ideia é mais velha que a data

Singleton

Garante que uma classe tenha uma única instância no processo e oferece um ponto global de acesso a ela — uma conveniência que cobra caro em acoplamento e testabilidade.

Em 10 segundos

Uma classe que garante a própria instância única e a expõe globalmente. Funciona — mas é estado global disfarçado de padrão: entenda o mecanismo, use com parcimônia e prefira injeção de dependência sempre que ela estiver disponível.

  • #gof
  • #instancia-unica
  • #estado-global
  • #injecao-de-dependencia
Analogia

O banco central do país

Um país tem exatamente UM banco central: só ele emite moeda e define a taxa básica de juros. Dois bancos centrais imprimindo dinheiro em paralelo seriam o caos — a unicidade não é detalhe, é a definição da instituição. Todo banco, empresa e cidadão se refere à mesma entidade, sem precisar carregá-la no bolso. Mas repare no reverso da moeda: quando o banco central muda a taxa, muda para o país inteiro de uma vez. Poder centralizado e globalmente acessível é exatamente o que torna o padrão útil — e perigoso.

01Conceito

O problema

Alguns objetos precisam existir uma única vez por processo: o pool de conexões, a configuração carregada do disco, o logger que escreve no mesmo arquivo. Duas cópias significam recursos desperdiçados ou dados inconsistentes entre elas.

Confiar em disciplina não é garantia: o construtor público continua lá, e basta um import distraído para nascer a segunda instância.

O Singleton do GoF resolve fazendo a classe policiar a si mesma: construtor privado + método estático que cria na primeira chamada e devolve sempre o mesmo objeto. Só que o padrão amarra DUAS responsabilidades num pacote só: garantir instância única E fornecer acesso global. A primeira é legítima; a segunda é a fonte de quase todos os problemas que deram ao padrão a fama de anti-pattern.

Acesso global significa que qualquer linha de código pode ler e mutar o singleton sem declarar essa dependência: a assinatura da função mente sobre o que ela realmente usa, os testes passam a depender da ordem de execução e o acoplamento se espalha invisível. É estado global com nome de padrão — a crítica não é moda, é consequência estrutural do ponto de acesso.

A alternativa moderna separa as responsabilidades: crie UMA instância no ponto de composição da aplicação (o main, o container de injeção de dependência) e injete-a em quem precisa. A unicidade vira decisão de configuração — visível na assinatura, trocável por um dublê no teste. É o que Spring e NestJS fazem com o escopo 'singleton' do container: uma instância só, sem o acesso global do padrão GoF.

Como se encaixa
  1. Qualquer código
  2. acessa de qualquer lugargetInstancia()
  3. cria na 1ª chamada, reusa depoisInstância única
  4. todos leem e escrevemEstado compartilhado
Qualquer ponto do código chega à mesma instância — a conveniência do acesso global e o risco do estado compartilhado são o MESMO mecanismo.
Passo a passoO fluxo, passo a passo
  1. 1

    Fechar o construtor

    Construtor privado impede `new` externo — a unicidade deixa de ser convenção.

  2. 2

    Guardar a instância

    Campo estático privado começa vazio e recebe o único exemplar.

  3. 3

    Acessar via getInstancia()

    Na primeira chamada cria; depois devolve sempre o mesmo objeto.

  4. 4

    Preferir injeção

    Mesmo existindo o singleton, receba a instância por parâmetro quando puder — testável e visível.

02Anatomia

Navegue pelas camadas

Camada 1/4Construtor privado

Camada 1

Construtor privado

fecha a porta do new fora da classe

É o que transforma 'combinado entre devs' em garantia estrutural: nenhum código externo consegue criar a segunda instância, porque o compilador (ou o runtime) recusa o new. A classe passa a ser a única dona do próprio ciclo de vida.

No código

private constructor() {}

Se violar: com o construtor público, a unicidade volta a ser convenção — e a segunda instância nasce exatamente no dia em que ninguém está olhando.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

Código

class BancoCentral {
  private static instancia?: BancoCentral;
  private taxaSelic = 15;

  private constructor() {}   // ninguém fora da classe dá new

  static getInstancia(): BancoCentral {
    if (!BancoCentral.instancia) {
      BancoCentral.instancia = new BancoCentral();
    }
    return BancoCentral.instancia;
  }

  definirSelic(taxa: number): void { this.taxaSelic = taxa; }
  selic(): number { return this.taxaSelic; }
}

const a = BancoCentral.getInstancia();
const b = BancoCentral.getInstancia();
console.log(a === b);       // true — sempre o mesmo objeto

a.definirSelic(12.25);
console.log(b.selic());     // 12.25 — mudou "para todo mundo"
// É exatamente isso que torna o padrão controverso:
// qualquer canto do código altera estado que todos leem.
05No mundo real

Casos de uso

Pool de conexões em um backend SaaS

Cenário

Uma API Node.js fala com Postgres; abrir conexão é caro e o servidor de banco aceita um número finito delas. Se cada requisição criar o próprio pool, o limite estoura no primeiro pico.

Como o padrão entra

Um pool único por processo, criado na primeira chamada e reusado por todas as rotas — o singleton (ou seu primo idiomático, a instância exportada de um módulo) garante que as N rotas disputam as mesmas 10 conexões, não 10 por rota.

Trade-off: Nos testes de integração o pool global vaza entre suítes (conexões abertas, estado residual). A versão madura cria o pool no boot e o injeta — mesma unicidade, sem o acesso global.

Configuração e feature flags em uma fintech

Cenário

Credenciais, URLs de provedores e flags de rollout são carregadas do ambiente no boot. Reler o ambiente a cada uso é lento e permite que dois módulos enxerguem valores diferentes no meio de um deploy.

Como o padrão entra

Um Config singleton carrega tudo uma vez e todos leem do mesmo lugar — consistência garantida durante a vida do processo.

Trade-off: Se a config aceita escrita em runtime, virou estado global mutável com outro nome. Congele o objeto após o boot; mudanças dinâmicas de flag pedem um serviço próprio, com invalidação explícita.

Logger central em uma plataforma de streaming

Cenário

Milhares de pontos de log em dezenas de módulos precisam escrever no mesmo destino, com buffer único — N writers abrindo N handles intercalam linhas e derrubam o throughput.

Como o padrão entra

Um logger único com buffer interno, acessado globalmente — é o desenho de bibliotecas como o módulo logging do Python, em que getLogger() devolve instâncias registradas centralmente.

Trade-off: Testes que verificam logs precisam resetar o singleton entre casos, ou um teste enxerga as linhas do outro. Loggers modernos atenuam criando filhos injetáveis (child loggers) por módulo ou requisição.

Quando o Singleton vira variável global

Compila, roda, passa no code review. E quebra depois.

A instância única virou um depósito de estado mutável que qualquer parte do sistema lê e escreve. Ninguém declara que depende dela — a dependência entra por dentro, invisível na assinatura de todo mundo.

class Config {
  private static instancia: Config;
  private valores = new Map<string, string>();

  static obter(): Config {
    if (!Config.instancia) Config.instancia = new Config();
    return Config.instancia;
  }

  set(k: string, v: string) { this.valores.set(k, v); }  // <- estado mutavel
  get(k: string) { return this.valores.get(k); }
}

// Nenhuma assinatura revela esta dependencia.
class Cobranca {
  cobrar(valor: number) {
    const moeda = Config.obter().get("moeda"); // acoplamento invisivel
    // ...
  }
}

// E aqui mora o problema: o teste precisa mexer no estado global
// para preparar o cenario — e nao tem como desfazer depois.
Config.obter().set("moeda", "BRL");

O que quebra

  • No testeDois testes que precisam de configurações diferentes não podem rodar na mesma suíte, e a ordem de execução passa a mudar o resultado.
  • Em produçãoA dependência não aparece em nenhum construtor, então ninguém descobre quem usa o quê sem ler todo o código.
  • Ao paralelizarO estado compartilhado vira uma condição de corrida silenciosa entre requisições concorrentes.

A saída: Singleton controla o ciclo de vida (uma instância só), não o acesso. Mantenha a instância única na composição da aplicação e injete-a por parâmetro: quem depende, declara. E se o objeto guarda estado mutável, provavelmente ele não deveria ser único.

07Cuidado

Armadilhas

  1. Estado global disfarçado

    Se o singleton guarda estado mutável de negócio, qualquer módulo pode alterá-lo e todos os outros sentem — sem que nenhuma assinatura declare a dependência. Bugs passam a depender da ordem de execução e a leitura do código mente sobre os efeitos. Singleton aceitável é quase sempre imutável após a criação (config congelada) ou um recurso técnico sem regra de negócio (pool, logger).

  2. Testes reféns da instância

    A instância estática sobrevive de um teste para o outro: o que o teste A escreveu, o teste B lê. E como getInstancia() está hard-coded no código de produção, substituir por um dublê exige hacks (resetar o campo estático, recarregar módulos). A correção estrutural é injeção de dependência: quem recebe a dependência pelo construtor aceita um mock de graça — a unicidade fica a cargo de quem compõe a aplicação.

  3. 'Único' só dentro do processo

    A garantia vale por processo — não por sistema. Com quatro workers no cluster, quatro pods no Kubernetes ou N execuções serverless, existem N 'singletons' independentes; um contador global ou um lock implementado assim simplesmente não funciona. Unicidade entre processos pede outro ferramental: lock distribuído, constraint no banco, fila com partição única.

08Decisão

Quando usar × quando evitar

Use quando

3
  • Um recurso físico ou caro deve existir uma única vez por processo (pool de conexões, cache em memória, handle de hardware).
  • Duas instâncias causariam conflito real, não só desperdício (dois schedulers disparando o mesmo job, dois writers no mesmo arquivo).
  • Não há injeção de dependência disponível (script, plugin, legado) e o ponto único de acesso é o mal menor documentado.

Evite quando

3
  • A motivação é só conveniência de acesso global — injeção de dependência entrega a mesma instância única sem esconder o acoplamento.
  • O objeto carrega estado mutável de negócio — vira estado global e os testes passam a depender da ordem de execução.
  • Você precisa substituir a dependência por dublês nos testes — singleton hard-coded não aceita mock sem hacks.

Testar o que ficou

3 armadilhas de Singleton viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Qualquer módulo ESUm módulo importado dez vezes é avaliado uma só; o export já é uma instância única do processo.
  • Pools de conexãoAbrir um pool por requisição derruba o banco — por isso ele é sempre único e compartilhado.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Um modulo ES ja e um singleton. Nao precisa de classe.
export const pool = criarPool();

O que ele cobra

Nenhum salto a mais entre a chamada e o efeito.

  • A dependência some das assinaturas e vira invisível
  • Testes deixam de poder rodar em paralelo se houver estado mutável

Não vale se: o objeto guarda estado que muda. Aí o que você quer é uma instância injetada, não uma global.

Onde isto aparece

Continue por aqui