No Spring, Lazy Initialization não é otimização automática
Existe uma reação comum quando uma aplicação Spring começa a demorar para subir:
"vamos ativar lazy initialization."
A ideia parece tentadora.
Se o Spring não criar todos os Beans na inicialização, o startup tende a ficar mais rápido. A aplicação sobe antes, o deploy parece melhor, o log fica menos barulhento e aquele Bean pesado só nasce quando alguém realmente precisar dele.
Mas essa leitura esconde uma armadilha.
Lazy Initialization não elimina custo.
Ela muda o momento em que o custo aparece.
E, dependendo do Bean, também muda o momento em que o erro aparece.
O que falharia no startup pode falhar na primeira requisição. O que consumiria tempo durante a subida pode consumir tempo no primeiro fluxo de negócio. O que era visível no deploy pode virar uma latência estranha em produção.
No Spring, inicialização preguiçosa é uma decisão de ciclo de vida.
Não é uma otimização automática.
O que Lazy Initialization realmente adia
Por padrão, muitos Beans singleton são criados durante a inicialização do contexto.
Isso permite que o Spring descubra cedo se consegue montar a aplicação:
- dependências obrigatórias;
- propriedades necessárias;
- métodos
@Bean; - proxies;
- inicializações de infraestrutura;
- validações feitas no construtor ou em callbacks de ciclo de vida.
Quando um Bean é lazy, a lógica muda.
O container sabe que o Bean existe, mas posterga a criação da instância até que ela seja realmente solicitada.
Em vez de nascer durante o startup, ele pode nascer no primeiro ponto de uso:
@Service
public class RelatorioService {
private final ExportadorPdf exportadorPdf;
public RelatorioService(@Lazy ExportadorPdf exportadorPdf) {
this.exportadorPdf = exportadorPdf;
}
public byte[] gerar(Relatorio relatorio) {
return exportadorPdf.exportar(relatorio);
}
}
Nesse exemplo, o ExportadorPdf pode ser adiado até que RelatorioService precise dele.
Isso pode fazer sentido se gerar relatório é uma operação rara e se o exportador tem criação cara.
Mas repare no verbo correto: adiar.
Não é reduzir por definição.
Se o Bean for usado, ele ainda será criado.
Se a criação for pesada, o peso ainda existe.
Se a configuração estiver errada, o erro ainda existe.
A diferença é que talvez tudo isso aconteça no caminho de uma requisição real.
Startup mais rápido pode esconder falha mais tarde
Um dos benefícios do startup ansioso é simples: a aplicação falha cedo.
Quando o contexto sobe criando os Beans necessários, muita coisa errada aparece antes de receber tráfego.
Imagine um client HTTP configurado por properties:
@Configuration
public class CobrancaConfig {
@Bean
public CobrancaClient cobrancaClient(CobrancaProperties properties) {
return new CobrancaClient(
properties.baseUrl(),
properties.token()
);
}
}
Se baseUrl ou token estiverem inválidos e o construtor validar esses dados, a aplicação pode falhar na subida.
Isso é desconfortável.
Mas é bom.
O deploy quebra antes de atender usuários.
Com inicialização lazy, esse erro pode aparecer só quando alguém tentar cobrar um pedido:
@Service
public class FecharPedidoService {
private final CobrancaClient cobrancaClient;
public FecharPedidoService(@Lazy CobrancaClient cobrancaClient) {
this.cobrancaClient = cobrancaClient;
}
public void fechar(Pedido pedido) {
cobrancaClient.cobrar(pedido);
}
}
Agora a aplicação pode subir aparentemente saudável.
O problema continua lá.
Só ficou esperando o primeiro uso.
Em alguns cenários, isso é aceitável. Em outros, é uma troca ruim: você ganhou alguns segundos no startup e perdeu previsibilidade operacional.
Lazy pode mover latência para o usuário
Outro engano comum é olhar apenas para o tempo de inicialização da aplicação.
Se antes o startup levava 18 segundos e depois passa a levar 10, parece que houve ganho.
Talvez tenha havido.
Mas a pergunta incompleta é:
"quanto tempo a aplicação leva para subir?"
A pergunta melhor é:
"onde o custo de inicialização aparece agora?"
Se um Bean pesado deixa de nascer no startup e passa a nascer na primeira requisição, a primeira pessoa que usar aquele fluxo paga a conta.
@Service
public class MotorRecomendacao {
private final Modelo modelo;
public MotorRecomendacao() {
this.modelo = Modelo.carregarDoDisco();
}
public List<Produto> recomendar(Cliente cliente) {
return modelo.recomendar(cliente);
}
}
Se esse Bean nasce no startup, o custo aparece no deploy.
Se ele nasce no primeiro uso, o custo aparece em uma requisição.
Dependendo do caso, isso pode ser pior.
Uma operação rara de administração talvez tolere esse atraso.
Um fluxo de checkout provavelmente não.
Por isso lazy initialization precisa ser discutida junto com latência, readiness, aquecimento da aplicação e criticidade do fluxo.
O número do startup sozinho não conta a história inteira.
O problema aumenta quando lazy vira configuração global
O Spring permite usar lazy em pontos específicos, como em um Bean ou dependência.
O problema costuma crescer quando a decisão vira uma chave global.
Em aplicações Spring Boot, é possível configurar a aplicação para inicializar Beans de forma preguiçosa de maneira ampla.
Essa chave pode parecer um ajuste inocente de performance.
Mas ela muda o comportamento de montagem da aplicação inteira.
O risco é transformar uma decisão local em uma política invisível:
spring.main.lazy-initialization=true
Depois disso, uma parte relevante do contexto pode deixar de ser validada no startup.
Beans que antes eram criados cedo passam a nascer sob demanda.
Erros de configuração podem demorar para aparecer.
Dependências circulares podem ficar menos óbvias.
Fluxos pouco usados podem carregar falhas por dias até alguém executá-los.
Isso não significa que a chave global nunca possa ser usada.
Mas ela exige intenção.
Em teste local, protótipos, ferramentas internas ou aplicações com muitos módulos opcionais, pode fazer sentido.
Em serviços críticos, a conversa precisa ser bem mais cuidadosa.
Lazy global não deveria ser o primeiro remédio para startup lento.
Normalmente, antes disso, vale descobrir o que está caro: scan amplo demais, auto-configurações desnecessárias, Beans fazendo I/O no construtor, conexões abertas cedo demais, inicializações escondidas em @PostConstruct ou bibliotecas trazendo mais infraestrutura do que o fluxo precisa.
Lazy não corrige desenho pesado
Um Bean caro demais para nascer também pode ser um sinal de desenho ruim.
Às vezes o problema não é o momento da criação.
É o que foi colocado dentro da criação.
Veja este exemplo:
@Service
public class CatalogoService {
private final Map<String, Produto> produtos;
public CatalogoService(ProdutoRepository produtoRepository) {
this.produtos = produtoRepository.buscarTodos()
.stream()
.collect(Collectors.toMap(Produto::codigo, produto -> produto));
}
}
Esse construtor faz I/O, carrega dados e monta cache.
Ativar lazy pode fazer a aplicação subir mais rápido.
Mas a primeira chamada ao CatalogoService ainda precisará buscar tudo.
O desenho continua acoplando criação do Bean com carregamento operacional de dados.
Talvez o cache precise de uma estratégia explícita.
Talvez precise de refresh.
Talvez precise carregar sob demanda por chave.
Talvez precise ser externo ao processo.
Talvez nem devesse existir.
Lazy não responde essas perguntas.
Ele só muda quando elas cobram a conta.
Quando Lazy Initialization faz sentido
Lazy initialization é útil quando o adiamento é uma decisão consciente.
Alguns exemplos razoáveis:
- componentes usados apenas em fluxos raros;
- integrações opcionais;
- ferramentas administrativas;
- objetos caros que talvez nunca sejam usados naquela execução;
- dependências que precisam quebrar uma relação de inicialização específica;
- módulos carregados sob demanda em aplicações maiores.
Mesmo nesses casos, a decisão precisa vir com perguntas práticas:
- se falhar no primeiro uso, quem percebe?
- existe monitoramento para esse fluxo?
- o atraso inicial é aceitável?
- o Bean deveria ser aquecido depois do startup?
- a aplicação deve ser considerada pronta antes desse Bean nascer?
- o custo foi medido ou apenas presumido?
Essa última pergunta importa muito.
Sem medição, lazy vira superstição de performance.
O time olha para uma chave, sente que otimizou alguma coisa e segue em frente.
Mas o sistema só ficou menos explícito.
O ponto que vale fixar
Lazy Initialization no Spring não é uma promessa de aplicação mais rápida.
É uma escolha sobre quando o container cria certos Beans.
Essa escolha pode melhorar o startup.
Também pode esconder erro, deslocar latência para o primeiro usuário e tornar o comportamento operacional menos previsível.
Use lazy quando o adiamento faz parte do desenho.
Não use como tinta fresca sobre um contexto pesado demais.
Se um Bean é caro para nascer, entenda por quê.
Se um erro precisa aparecer cedo, não empurre esse erro para runtime.
No Spring, o momento em que um Bean nasce também é parte da arquitetura da aplicação.
