No Spring, singleton não significa uma única instância da aplicação

Existe uma frase que parece simples, mas costuma gerar decisões ruins em projetos Spring:

"esse Bean é singleton, então só existe uma instância dele na aplicação."

A frase parece correta.

Mas falta uma parte importante.

No Spring, singleton não significa uma única instância global da aplicação.

Significa uma instância por container do Spring.

Mais precisamente: uma instância daquele Bean dentro daquele ApplicationContext.

Essa diferença não é detalhe acadêmico.

Ela muda como você pensa sobre estado, cache, concorrência, testes, aplicações web, múltiplos contextos, workers, pods e processos separados.

Quando o time interpreta singleton como "objeto único no sistema inteiro", começa a colocar responsabilidade demais em um Bean que só é único dentro de uma fronteira bem menor.


Onde a confusão começa

Em Java, o termo singleton costuma lembrar o padrão de projeto clássico:

public final class ConfiguracaoGlobal {

    private static final ConfiguracaoGlobal INSTANCE = new ConfiguracaoGlobal();

    private ConfiguracaoGlobal() {
    }

    public static ConfiguracaoGlobal getInstance() {
        return INSTANCE;
    }
}

Esse modelo tenta controlar a criação usando static, construtor privado e um ponto global de acesso.

O Spring trabalha com outra ideia.

Quando você declara um Bean com escopo singleton, você não está escrevendo um singleton clássico.

Você está dizendo ao container:

"para este nome de Bean, dentro deste contexto, crie uma instância e reutilize essa instância nas injeções."

Por isso este código:

@Service
public class CalculadoraFrete {

    public BigDecimal calcular(Pedido pedido) {
        return pedido.valor().multiply(new BigDecimal("0.10"));
    }
}

normalmente resulta em uma única instância de CalculadoraFrete dentro do contexto Spring.

Mas essa unicidade pertence ao contexto.

Ela não pertence à JVM inteira.

Ela não pertence ao cluster.

Ela não atravessa outro processo.

Ela não impede outro ApplicationContext de criar outra instância do mesmo Bean.


Singleton é escopo, não garantia global

O detalhe importante é este:

singleton é o escopo padrão dos Beans no Spring.

Escopo fala sobre como o container cria e reutiliza instâncias.

Não fala sobre quantas vezes a classe pode existir no universo da aplicação.

Se a aplicação tem um único ApplicationContext, rodando em um único processo, é comum observar uma instância daquele Bean.

Mas isso é consequência da topologia de execução, não uma garantia global do conceito.

Em uma aplicação web com Spring Boot, normalmente existe um contexto principal.

Nesse cenário, dois services que dependem do mesmo repository recebem a mesma instância gerenciada daquele repository:

@Service
public class FecharPedidoService {

    private final PedidoRepository pedidoRepository;

    public FecharPedidoService(PedidoRepository pedidoRepository) {
        this.pedidoRepository = pedidoRepository;
    }
}

@Service
public class CancelarPedidoService {

    private final PedidoRepository pedidoRepository;

    public CancelarPedidoService(PedidoRepository pedidoRepository) {
        this.pedidoRepository = pedidoRepository;
    }
}

Essa é a parte que muita gente vê.

O que muita gente esquece é a fronteira.

Se outro contexto for criado, ele terá seu próprio ciclo de vida e seu próprio registro de Beans.

Se a aplicação subir em dois pods, cada pod terá seu próprio processo.

Se dois testes carregarem contextos diferentes, cada contexto poderá ter suas próprias instâncias.

Se uma aplicação modular criar contextos pai e filho, a conversa fica ainda mais importante.

singleton não é uma trava global.

É uma regra de reutilização dentro do container que está gerenciando aquele Bean.


O erro aparece quando o Bean guarda estado de execução

A confusão fica perigosa quando o Bean singleton começa a guardar estado mutável de uma operação.

Veja este exemplo:

@Service
public class ImportacaoPedidosService {

    private int totalProcessado;

    public void importar(List<PedidoCsv> pedidos) {
        totalProcessado = 0;

        for (PedidoCsv pedido : pedidos) {
            importarPedido(pedido);
            totalProcessado++;
        }
    }

    public int getTotalProcessado() {
        return totalProcessado;
    }
}

O problema não é apenas "isso não é thread-safe".

Esse já seria um motivo suficiente para desconfiar.

O problema de desenho é mais profundo:

um Bean singleton está sendo usado como se fosse uma instância de trabalho de uma execução específica.

Em uma aplicação web, duas requisições podem chamar importar ao mesmo tempo.

Uma sobrescreve o totalProcessado da outra.

Em um worker, duas tarefas concorrentes podem disputar o mesmo campo.

Em um teste, o estado pode vazar entre cenários se o contexto for reaproveitado.

O Bean parecia "único" e conveniente.

Na prática, ele virou um ponto compartilhado de estado acidental.

Esse é um dos motivos pelos quais services singleton devem, na maior parte dos casos, ser sem estado.

Eles podem depender de repositories, gateways, clients, mappers e políticas.

Mas o estado da operação deve ficar em variável local, objeto de comando, entidade, DTO, mensagem ou registro persistido, conforme o caso.


Estado compartilhado precisa ser uma decisão explícita

Isso não significa que um Bean singleton nunca possa manter estado.

Pode.

Mas esse estado precisa ser uma decisão explícita de infraestrutura ou coordenação.

Um cache local, por exemplo, pode viver dentro de um Bean.

Mas aí a conversa muda.

Você precisa perguntar:

  • esse cache pode ser diferente em cada instância da aplicação?
  • o dado pode ficar desatualizado?
  • o cache precisa sobreviver a restart?
  • o cluster precisa enxergar a mesma informação?
  • existe invalidação?
  • existe concorrência?

Se a resposta exigir consistência entre múltiplas instâncias da aplicação, um campo em um Bean singleton não resolve.

Ele só compartilha estado dentro daquele contexto.

Em um ambiente com três pods, você terá três caches locais.

Em dois processos, terá dois estados.

Em dois contextos, terá duas instâncias.

O nome singleton não muda essa arquitetura.

Ele apenas descreve como o Spring reutiliza a instância dentro da fronteira do contexto.


@Bean e @Component seguem a mesma regra de escopo

Essa leitura também ajuda a conectar singleton com posts anteriores da série.

Quando uma classe é descoberta por @Component, @Service ou @Repository, ela vira Bean gerenciado pelo container.

Quando um método @Bean registra um objeto, o resultado também vira Bean gerenciado pelo container.

Nos dois casos, se você não alterar o escopo, o padrão é singleton.

@Configuration
public class RelatorioConfig {

    @Bean
    public GeradorRelatorio geradorRelatorio(Clock clock) {
        return new GeradorRelatorio(clock);
    }
}

Esse GeradorRelatorio será reutilizado pelo contexto.

Mas isso não transforma GeradorRelatorio em uma instância global da aplicação inteira.

Ele continua preso à mesma fronteira: o ApplicationContext que o registrou.

Por isso a discussão conversa diretamente com o post sobre @Bean e com o post sobre @Configuration.

O ponto não é decorar qual anotação cria singleton.

O ponto é entender quem controla a instância e até onde esse controle vale.


Prototype não é a correção automática

Quando alguém percebe que singleton compartilha estado, outra reação comum aparece:

"então vou trocar para prototype."

Nem sempre isso resolve.

@Scope("prototype")
@Service
public class ProcessamentoPedido {
}

prototype diz ao Spring para criar uma nova instância quando aquele Bean for solicitado ao container.

Mas se um Bean singleton recebe esse prototype pelo construtor, a injeção acontece uma vez na criação do singleton.

Depois disso, o singleton continua segurando a mesma referência.

Ou seja, mudar o escopo sem entender o ponto de criação só troca a confusão de lugar.

Se você precisa de um objeto novo por operação, talvez ele nem deva ser Bean.

Pode ser um objeto comum criado dentro do método.

Pode ser uma factory.

Pode ser um command object.

Pode ser um valor passado explicitamente.

Nem todo estado temporário precisa entrar no container.

Às vezes a correção não é escolher outro escopo.

É tirar aquele objeto do modelo de Beans.


O ponto que vale fixar

No Spring, singleton não significa uma única instância da aplicação.

Significa uma instância de um Bean por ApplicationContext.

Essa frase curta evita vários erros.

Evita tratar Bean como variável global.

Evita guardar estado de requisição em service.

Evita acreditar que cache local vale para o cluster inteiro.

Evita trocar para prototype como solução automática.

E, principalmente, obriga o time a enxergar a fronteira real do container.

O Spring gerencia objetos dentro de um contexto.

Fora dessa fronteira, entram processo, JVM, deploy, cluster, banco, mensageria, cache distribuído e arquitetura de execução.

Misturar tudo isso sob a palavra singleton é confortável.

Mas é justamente essa simplificação que costuma esconder o problema.