Pular para o conteúdo

Design Patterns: Singleton — Entenda de vez esse padrão (com exemplos em NestJS)

Se você está começando a estudar programação ou já escreve código há algum tempo, provavelmente já ouviu falar em Design Patterns. Eles aparecem em entrevistas, cursos, artigos e até em discussões no trabalho. Mas a verdade é que, para quem está no início, esses conceitos costumam parecer mais complicados do que realmente são. O Singleton é um ótimo exemplo disso. Ele é um dos padrões de projeto mais conhecidos e, ao mesmo tempo, um dos mais mal compreendidos.

De forma simples, o padrão Singleton resolve um problema bem comum: garantir que uma classe tenha apenas uma única instância durante toda a execução da aplicação. Isso é muito útil quando falamos de coisas como conexões com banco de dados, serviços de configuração, cache em memória ou logs. Em vez de criar vários objetos iguais espalhados pelo sistema, você centraliza tudo em um único ponto, facilitando o controle e evitando comportamentos inesperados.

No mundo JavaScript e TypeScript, especialmente em frameworks modernos como o NestJS, o Singleton aparece de forma quase natural. Muita gente usa o padrão sem nem perceber, porque o próprio NestJS já trabalha com injeção de dependência e serviços singleton por padrão. Ou seja, entender esse design pattern não só ajuda a escrever código melhor, como também faz você compreender mais profundamente como o framework funciona por baixo dos panos.

Neste artigo, vamos conversar de forma bem tranquila sobre o padrão Singleton: o que ele é, quando faz sentido usar, quando evitar e como ele funciona na prática. Vamos trazer exemplos simples e depois conectar isso com o NestJS, sempre pensando em quem está aprendendo agora, sem complicar mais do que o necessário.

O que é o Design Pattern Singleton?

Antes de entrar em código, vale alinhar bem o conceito. O Singleton é um padrão de projeto criacional, ou seja, ele está relacionado à forma como objetos são criados dentro de uma aplicação. A ideia central é simples: uma classe deve ter apenas uma única instância e fornecer um ponto global de acesso a ela.

Na prática, isso significa que, não importa quantas vezes você tente criar esse objeto, o sistema sempre vai reutilizar a mesma instância já existente. Em vez de fazer vários new MinhaClasse() espalhados pelo código, você garante que todos usem exatamente o mesmo objeto.

Mas por que isso é importante? Imagine uma aplicação que precisa acessar configurações globais, como variáveis de ambiente, parâmetros de conexão com banco de dados ou um serviço de cache. Se cada parte do sistema criar sua própria instância dessas configurações, você pode ter inconsistências, consumo desnecessário de memória e até bugs difíceis de rastrear. O Singleton evita esse tipo de problema centralizando o controle.

Um ponto importante: Singleton não é sinônimo de variável global, apesar de muita gente confundir. Variáveis globais podem ser alteradas por qualquer parte do sistema, sem controle. Já o Singleton encapsula o estado dentro de uma classe, seguindo os princípios da orientação a objetos, como encapsulamento e responsabilidade única.

Outro detalhe que gera confusão é achar que Singleton é sempre a melhor solução. Não é. Ele resolve problemas específicos e, quando usado sem critério, pode dificultar testes automatizados e acoplar demais o código. Por isso, entender o quando e o porquê usar é tão importante quanto saber como implementar.

Quando usar (e quando não usar) o Singleton

Agora que o conceito do Singleton está mais claro, vem a parte mais importante: quando esse padrão realmente faz sentido. Esse é o ponto onde muita gente erra, principalmente no começo, usando Singleton para tudo só porque “aprendeu que é um design pattern”.

Quando o Singleton é uma boa escolha

O Singleton é ideal quando você precisa garantir um único estado compartilhado em toda a aplicação. Alguns exemplos bem comuns no dia a dia:

  • Configurações globais da aplicação
    Leitura de variáveis de ambiente, parâmetros de execução, feature flags, etc.
  • Conexão com banco de dados
    Criar várias conexões desnecessárias pode causar problemas de performance e consumo de recursos.
  • Serviços de cache em memória
    Como Redis clients, caches locais ou controle de sessões.
  • Serviços de logging
    Centralizar logs em uma única instância facilita manutenção e padronização.

Nesses cenários, ter múltiplas instâncias do mesmo objeto não traz benefício nenhum. Pelo contrário, pode gerar bugs sutis e comportamentos inesperados. O Singleton resolve isso de forma direta e organizada.

Quando evitar o Singleton

Apesar de útil, o Singleton não é uma solução universal. Existem situações em que ele pode atrapalhar mais do que ajudar:

  • Dificulta testes automatizados
    Como o estado é compartilhado, um teste pode influenciar o resultado de outro.
  • Aumenta o acoplamento do sistema
    Partes do código passam a depender diretamente da mesma instância.
  • Esconde dependências
    Em vez de receber dependências por parâmetro, o código “busca” o Singleton sozinho, o que reduz clareza.
  • Pode virar um “Deus objeto”
    Um Singleton mal planejado acaba acumulando responsabilidades demais.

Por isso, sempre vale se perguntar:
“Esse objeto realmente precisa ser único em toda a aplicação?”
Se a resposta for “não necessariamente”, talvez outro padrão ou simplesmente uma instância normal resolva melhor.

Singleton no mundo moderno (especialmente no NestJS)

Aqui entra um ponto interessante: frameworks modernos como o NestJS já lidam muito bem com esse problema usando Injeção de Dependência. Na maioria dos casos, você nem precisa implementar o Singleton manualmente. O framework faz isso por você, de forma mais segura e testável.

Implementando o Singleton na prática (JavaScript e TypeScript)

Até aqui falamos bastante de conceito. Agora é hora de transformar isso em código e ver como o Singleton funciona na prática. Vou começar com um exemplo simples em JavaScript, depois evoluir para TypeScript, que é mais próximo do que usamos no NestJS.

Singleton básico em JavaScript

Em JavaScript, uma forma clássica de implementar o Singleton é usando uma variável estática que guarda a instância da classe. A ideia é: se a instância já existir, você retorna ela; se não existir, você cria uma nova.

O fluxo mental é mais ou menos assim:

  • A classe tem uma propriedade que armazena a instância
  • O construtor não deve ser chamado livremente
  • Um método controla a criação e o acesso ao objeto

Mesmo sem mostrar código agora, o importante é entender que quem controla a criação da instância é a própria classe, e não quem consome ela.

Evoluindo para TypeScript

Com TypeScript, tudo fica mais claro e seguro, porque podemos usar:

  • Tipagem explícita
  • Propriedades privadas
  • Construtores privados

Isso ajuda a reforçar a regra principal do Singleton: ninguém de fora pode criar uma nova instância diretamente.

Em TypeScript, o construtor costuma ser private, o que impede o uso do new fora da classe. Assim, o único jeito de obter a instância é através de um método estático, geralmente chamado de getInstance().

Essa abordagem deixa o padrão bem explícito e fácil de entender, principalmente para quem está aprendendo orientação a objetos.

Um exemplo mental comum

Imagine um ConfigService responsável por ler variáveis de ambiente.
Você não quer que cada parte da aplicação leia o .env de novo. Você quer:

  • Ler uma vez
  • Guardar o resultado
  • Reutilizar em qualquer lugar

Esse é um caso clássico de Singleton.

Mas atenção: JavaScript já tem um “pseudo Singleton”

Aqui vai um detalhe interessante que muita gente ignora: módulos JavaScript já funcionam como Singletons por padrão.
Quando você faz um import, aquele módulo é carregado uma única vez e compartilhado entre todos que o importam.

Isso significa que, em muitos casos, você nem precisa implementar o padrão formalmente. Basta organizar bem seus módulos.

Singleton no NestJS: como o framework resolve isso automaticamente

Aqui é onde muita coisa começa a fazer sentido, principalmente se você já usa ou está começando com NestJS. Diferente de implementações manuais de Singleton, o NestJS já nasce com uma solução moderna e bem estruturada para esse problema: Injeção de Dependência (Dependency Injection).

No NestJS, todos os providers (services, repositories, helpers, etc.) são Singletons por padrão. Isso significa que, quando você cria um service usando o comando padrão do framework, o NestJS garante que apenas uma única instância daquele service exista durante todo o ciclo de vida da aplicação.

Na prática, você já está usando o padrão Singleton o tempo todo, mesmo sem perceber.

Exemplo simples de Singleton no NestJS

Imagine um service básico:

import { Injectable } from '@nestjs/common';

@Injectable()
export class ConfigService {
  getDatabaseUrl(): string {
    return process.env.DATABASE_URL;
  }
}

Sempre que esse ConfigService for injetado em qualquer controller ou outro service, o NestJS vai reutilizar a mesma instância.

constructor(private readonly configService: ConfigService) {}

Não importa quantos controllers usem esse service. Ele será criado uma única vez e compartilhado. Isso é Singleton na prática, aplicado de forma elegante e segura.

Por que essa abordagem é melhor que o Singleton “clássico”?

O Singleton tradicional, com getInstance() e construtor privado, funciona, mas traz alguns problemas, principalmente em aplicações maiores:

  • Dificulta testes unitários
  • Cria dependências escondidas
  • Aumenta o acoplamento

O NestJS resolve isso usando injeção de dependência, o que traz várias vantagens:

  • Dependências ficam explícitas no construtor
  • Fica fácil substituir implementações em testes
  • O ciclo de vida dos objetos é controlado pelo framework

Ou seja, você obtém os benefícios do Singleton sem os problemas clássicos.

Escopos no NestJS: nem tudo precisa ser Singleton

Outro ponto importante: no NestJS, o Singleton é o comportamento padrão, mas não é obrigatório. O framework permite mudar o escopo de um provider:

  • DEFAULT (Singleton)
  • REQUEST (uma instância por request)
  • TRANSIENT (uma nova instância a cada injeção)

Isso é extremamente poderoso, porque te dá controle total sobre o ciclo de vida dos objetos, algo que o Singleton tradicional não oferece de forma simples.

Singleton consciente, não automático

Mesmo o NestJS facilitando tudo, é importante usar esse poder com consciência. Nem todo service precisa ser Singleton. Serviços que mantêm estado mutável sensível, por exemplo, podem causar problemas se compartilhados entre requisições.

Vantagens e desvantagens do Singleton na prática

Agora que você já entendeu o conceito, viu exemplos e percebeu que o NestJS usa Singleton por padrão, faz todo sentido olhar com mais cuidado para os prós e contras desse padrão no mundo real. Isso é essencial para não usar Singleton “no automático”.

Vantagens do Singleton

A primeira grande vantagem é o controle centralizado. Quando existe apenas uma instância de um objeto, fica muito mais fácil garantir consistência de dados e comportamento previsível. Isso é especialmente útil para configurações, logs e conexões externas.

Outra vantagem importante é a economia de recursos. Criar objetos pode ser caro, principalmente quando envolvem conexões com banco, serviços externos ou leitura de arquivos. Com o Singleton, esse custo acontece uma única vez.

Também vale destacar a facilidade de acesso. Em frameworks como o NestJS, basta injetar o service e pronto. Você não precisa se preocupar com criação, destruição ou reaproveitamento da instância.

Em termos de arquitetura, o Singleton pode ajudar a manter o código mais organizado, evitando duplicação de lógica e centralizando responsabilidades bem definidas.

Desvantagens do Singleton

Por outro lado, o Singleton pode virar um problema quando usado sem critério. Um dos maiores pontos negativos é o estado compartilhado. Se o objeto mantém dados mutáveis, qualquer parte da aplicação pode alterar esse estado, impactando outras partes de forma inesperada.

Outro problema comum aparece nos testes automatizados. Singletons dificultam o isolamento dos testes, já que o estado pode “vazar” de um teste para outro. No NestJS isso é amenizado, mas ainda exige atenção.

Além disso, o Singleton pode aumentar o acoplamento. Quando muitas partes do sistema dependem da mesma instância, mudanças nesse objeto tendem a ter impacto em várias áreas do código.

Por fim, existe o risco de criar o famoso objeto Deus: um Singleton que começa pequeno, mas vai acumulando responsabilidades até se tornar difícil de manter.

O equilíbrio é a chave

O Singleton não é vilão nem herói. Ele é uma ferramenta. Quando usado para o problema certo, no contexto certo, funciona muito bem. O segredo está em manter responsabilidades claras e evitar estado mutável desnecessário.

Erros comuns ao usar Singleton (e como evitar)

Mesmo sendo um padrão bastante conhecido, o Singleton é frequentemente usado da forma errada, principalmente por quem está aprendendo agora. Entender esses erros ajuda muito a escrever código mais limpo, previsível e fácil de manter.

Usar Singleton para tudo

Esse é, disparado, o erro mais comum. Depois que o desenvolvedor aprende o padrão, surge a tentação de transformar qualquer classe em Singleton. Isso geralmente acontece por medo de criar múltiplas instâncias ou por achar que Singleton é “mais profissional”.

Na prática, a maioria das classes não precisa ser Singleton. Se o objeto não representa um recurso compartilhado ou não mantém um estado global importante, criar várias instâncias não é um problema.

Como evitar:
Sempre se pergunte se existe um motivo real para aquela classe existir apenas uma vez na aplicação.

Manter estado mutável dentro do Singleton

Singleton com estado mutável é uma combinação perigosa. Se o objeto guarda dados que mudam ao longo do tempo, qualquer parte do sistema pode afetar esse estado, causando efeitos colaterais difíceis de rastrear.

Isso é ainda mais crítico em APIs, onde várias requisições acontecem ao mesmo tempo.

Como evitar:

  • Prefira Singletons stateless
  • Se precisar de estado, deixe-o bem controlado
  • No NestJS, avalie usar escopos REQUEST ou TRANSIENT

Criar Singletons manuais dentro do NestJS

Outro erro comum é tentar implementar Singleton “na mão” dentro de um projeto NestJS, usando getInstance() e construtor privado. Isso geralmente entra em conflito com o sistema de injeção de dependência do framework.

Resultado: código mais complexo, menos testável e fora do padrão do NestJS.

Como evitar:
Confie no container de dependências do NestJS. Se o provider for DEFAULT, ele já é Singleton.

Esconder dependências

Singletons clássicos muitas vezes são acessados diretamente, sem serem injetados. Isso esconde dependências e dificulta a leitura do código.

No NestJS, quando você injeta tudo pelo construtor, fica muito claro do que aquela classe depende.

Como evitar:
Sempre prefira injeção de dependência em vez de acesso direto a instâncias globais.

Não pensar em testes desde o início

Singletons mal planejados tornam os testes mais difíceis, especialmente quando não permitem substituição por mocks.

Como evitar:

  • Use interfaces quando possível
  • No NestJS, aproveite o TestingModule
  • Evite lógica complexa dentro do Singleton

Singleton clássico vs Singleton no NestJS

Depois de tudo o que vimos até aqui, fica claro que o Singleton clássico e o Singleton no NestJS resolvem o mesmo problema, mas de formas bem diferentes.

O Singleton clássico, aquele com construtor privado e método getInstance(), faz sentido em linguagens e contextos onde não existe um container de injeção de dependência. Ele coloca toda a responsabilidade de controle da instância dentro da própria classe. Funciona, mas cobra um preço: mais acoplamento, menos flexibilidade e mais dificuldade para testar.

Já no NestJS, o Singleton é tratado como uma decisão de arquitetura do framework, não como um truque de código. Você declara um service, marca ele como @Injectable() e o NestJS cuida de todo o resto. A instância é única, o ciclo de vida é controlado, as dependências são explícitas e os testes ficam muito mais simples.

Na prática, isso muda completamente a forma de pensar o padrão. Em vez de perguntar “como implementar um Singleton?”, a pergunta passa a ser:
“qual deve ser o escopo desse provider?”

Esse modelo é mais moderno, mais limpo e mais alinhado com aplicações backend reais.

Conclusão

O Design Pattern Singleton é simples na teoria, mas poderoso na prática quando usado do jeito certo. Ele existe para resolver um problema específico: garantir uma única instância de um objeto que representa um recurso compartilhado da aplicação.

Se você está começando agora, o mais importante não é decorar a implementação clássica, mas entender o conceito e reconhecer quando ele aparece no seu dia a dia. No NestJS, por exemplo, você já usa Singleton o tempo todo sem perceber.

O segredo está no equilíbrio. Singleton não é vilão, mas também não é solução para tudo. Use para configurações, logs, conexões e serviços que realmente precisam ser únicos. Evite estado mutável desnecessário e confie no sistema de injeção de dependência do framework.

Quando bem aplicado, o Singleton deixa seu código mais organizado, eficiente e fácil de manter. Quando mal usado, vira fonte de bugs e dor de cabeça.

E agora, quero saber de você

Você já usava Singleton no NestJS sem perceber?
Já teve algum problema por causa de estado compartilhado em services?

Deixa seu comentário aqui contando sua experiência ou dúvida. Essa troca é o que faz a comunidade evoluir de verdade.