No Spring, @Bean existe para resolver um problema específico

Existe uma confusão comum quando alguém começa a usar Spring:

se @Component, @Service e @Repository registram objetos no container, então @Bean seria só outra forma de fazer a mesma coisa.

Uma alternativa de estilo.

Uma preferência pessoal.

Um jeito mais "manual" de criar classes.

Não é bem assim.

@Bean existe para resolver um problema específico:

registrar no contexto do Spring um objeto cuja criação precisa ser descrita por código.

Isso muda a leitura da anotação.

@Bean não é só "mais uma anotação para virar Bean".

É uma forma explícita de dizer:

"Spring, este objeto também faz parte da aplicação, mas eu preciso controlar como ele nasce."


O problema que @Component resolve

Antes de olhar para @Bean, vale voltar um passo.

Quando você tem uma classe sua, com construção simples, descoberta pelo próprio projeto, o caminho natural costuma ser uma anotação de estereótipo:

@Service
public class FecharPedidoService {

    private final PedidoRepository pedidoRepository;

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

Nesse caso, o Spring consegue encontrar a classe no component scan.

Ele consegue chamar o construtor.

Ele consegue resolver as dependências.

Ele consegue registrar a instância como Bean.

O modelo é direto:

a classe é sua, a criação é simples e o próprio tipo já comunica bem o papel daquele objeto.

Esse é o cenário em que @Component, @Service, @Repository e @Controller funcionam muito bem.


O problema que @Bean resolve é outro

Agora imagine que a aplicação precisa de um ObjectMapper configurado.

Você não vai abrir a classe ObjectMapper e colocar @Component nela.

Ela não é sua.

Ela vem de uma biblioteca.

E mesmo que fosse possível anotá-la, ainda faltaria uma parte importante: a configuração.

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
                .findAndRegisterModules()
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

Aqui o problema não é "como faço uma classe virar Bean?".

O problema é:

"como registro no Spring uma instância criada do jeito certo para esta aplicação?"

Essa diferença é central.

@Component marca uma classe para o Spring descobrir.

@Bean marca um método que produz um objeto para o Spring gerenciar.


@Bean fica no método porque o ponto importante é a criação

Com @Component, o foco está na classe:

@Component
public class CalculadoraDescontoPadrao implements CalculadoraDesconto {
}

Com @Bean, o foco está no método:

@Bean
public CalculadoraDesconto calculadoraDesconto(RegraComercialProperties properties) {
    if (properties.usarNovaRegra()) {
        return new CalculadoraDescontoNova();
    }

    return new CalculadoraDescontoLegada();
}

O método conta uma história que uma anotação na classe não contaria bem.

Ele mostra que existe uma decisão de criação.

Ele mostra que talvez exista configuração.

Ele mostra que o objeto resultante é o que importa para o container, não necessariamente a classe concreta usada para construí-lo.

Por isso @Bean não deveria ser lido como "atalho para dar new dentro do Spring".

Ele é uma declaração explícita de fábrica.


A classe de configuração organiza decisões de montagem

Normalmente, métodos @Bean vivem dentro de uma classe anotada com @Configuration:

@Configuration
public class PagamentoConfig {

    @Bean
    public GatewayPagamento gatewayPagamento(PagamentoProperties properties) {
        return new GatewayPagamentoHttp(
                properties.baseUrl(),
                properties.token()
        );
    }
}

Essa classe não deveria virar uma gaveta genérica para qualquer coisa.

Ela organiza decisões de montagem da aplicação.

O que entra ali costuma ter uma dessas características:

  • a classe vem de uma biblioteca externa;
  • a criação depende de propriedades;
  • a criação exige factory, builder ou adaptação;
  • a implementação concreta precisa ficar escondida atrás de uma interface;
  • o objeto representa infraestrutura compartilhada da aplicação.

Quando nenhuma dessas coisas existe, talvez @Bean só esteja deixando o código mais distante do lugar onde deveria estar.


@Bean não torna o objeto menos gerenciado

Um objeto criado por um método @Bean continua sendo um Bean do Spring.

Isso significa que ele participa do contexto.

Ele pode ser injetado em outros objetos.

Ele pode receber dependências pelo parâmetro do método.

Ele pode ter ciclo de vida gerenciado.

Ele pode ser usado em composição com outros Beans.

@Configuration
public class RelatorioConfig {

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

Os parâmetros do método também são resolvidos pelo container.

Ou seja, o método @Bean não está fora do Spring.

Ele está dentro do modelo do Spring.

A diferença é que a criação foi declarada de forma explícita.


O erro é usar @Bean para esconder desenho ruim

@Bean começa a virar problema quando é usado para empurrar decisões para uma configuração distante sem necessidade.

Imagine algo assim:

@Configuration
public class ServicesConfig {

    @Bean
    public FecharPedidoService fecharPedidoService(
            PedidoRepository pedidoRepository,
            EstoqueService estoqueService,
            EmailService emailService
    ) {
        return new FecharPedidoService(
                pedidoRepository,
                estoqueService,
                emailService
        );
    }
}

Esse código pode funcionar.

Mas, se FecharPedidoService é uma classe da própria aplicação, com dependências claras, normalmente não havia um problema específico para @Bean resolver.

O Spring já conseguiria fazer isso:

@Service
public class FecharPedidoService {

    private final PedidoRepository pedidoRepository;
    private final EstoqueService estoqueService;
    private final EmailService emailService;

    public FecharPedidoService(
            PedidoRepository pedidoRepository,
            EstoqueService estoqueService,
            EmailService emailService
    ) {
        this.pedidoRepository = pedidoRepository;
        this.estoqueService = estoqueService;
        this.emailService = emailService;
    }
}

Nesse cenário, @Bean não trouxe clareza.

Ele só moveu a montagem para outro lugar.

E quando várias classes de aplicação são registradas desse jeito sem motivo, a configuração vira um mapa paralelo da aplicação.

Você precisa olhar para a classe e para a configuração para entender algo que poderia estar claro no próprio tipo.


@Bean é muito útil para adaptar bibliotecas

Um dos usos mais naturais de @Bean é integrar objetos que não foram desenhados para o Spring.

Por exemplo, um client criado por builder:

@Configuration
public class IntegracaoConfig {

    @Bean
    public ClienteConsultaCpf clienteConsultaCpf(IntegracaoProperties properties) {
        return ClienteConsultaCpf.builder()
                .baseUrl(properties.cpf().baseUrl())
                .timeout(properties.cpf().timeout())
                .apiKey(properties.cpf().apiKey())
                .build();
    }
}

Esse tipo de código pertence bem a uma configuração.

A aplicação precisa daquele client como colaborador.

Mas a forma de criar o objeto envolve detalhes de infraestrutura.

Colocar isso espalhado dentro dos serviços seria pior:

@Service
public class AprovarCadastroService {

    public void aprovar(Cadastro cadastro) {
        ClienteConsultaCpf cliente = ClienteConsultaCpf.builder()
                .baseUrl("https://api.exemplo.com")
                .timeout(Duration.ofSeconds(2))
                .apiKey("...")
                .build();

        // usa o cliente
    }
}

Aqui o serviço passou a conhecer detalhes de montagem.

Ele cria infraestrutura no meio do caso de uso.

Ele dificulta teste.

Ele duplica configuração.

Nesse caso, @Bean resolve um problema real: centralizar a criação de um colaborador de infraestrutura e entregá-lo pronto para quem precisa.


@Bean também explicita decisões de implementação

Às vezes a aplicação quer depender de uma interface, mas a implementação concreta é uma decisão de configuração.

public interface Notificador {
    void notificar(Mensagem mensagem);
}

A aplicação pode escolher uma implementação HTTP:

@Bean
public Notificador notificadorHttp(HttpClient httpClient) {
    return new NotificadorHttp(httpClient);
}

Ou uma implementação fake para um ambiente local:

@Bean
public Notificador notificadorFake() {
    return new NotificadorFake();
}

O ponto importante é que a escolha não deveria ficar escondida.

Se existem várias implementações ao mesmo tempo, voltam as discussões dos posts sobre @Primary, @Qualifier e injeção de lista de Beans.

@Bean registra objetos.

Ele não elimina a necessidade de modelar bem a escolha entre eles.


O nome do método também importa

Quando você declara um método @Bean, o nome do método normalmente vira o nome do Bean:

@Bean
public Clock clock() {
    return Clock.systemUTC();
}

Esse Bean passa a se chamar clock.

Isso parece detalhe até a aplicação ter mais de um Bean do mesmo tipo.

@Bean
public Clock relogioSistema() {
    return Clock.systemDefaultZone();
}

@Bean
public Clock relogioUtc() {
    return Clock.systemUTC();
}

Agora existem dois Clock.

O tipo sozinho deixou de ser suficiente.

O nome comunica intenção.

E, se alguém precisar escolher um deles, essa escolha precisa aparecer no ponto de injeção.

public AgendadorService(@Qualifier("relogioUtc") Clock clock) {
    this.clock = clock;
}

De novo, @Bean não é só mecanismo.

Ele também participa da linguagem da aplicação.


Cuidado com estado compartilhado

Por padrão, um Bean no Spring costuma ser singleton.

Isso vale para Beans criados por @Component.

E também vale para Beans criados por @Bean.

Então este código merece atenção:

@Bean
public CarrinhoTemporario carrinhoTemporario() {
    return new CarrinhoTemporario();
}

Se CarrinhoTemporario guarda itens em memória, você provavelmente acabou de criar estado compartilhado na aplicação inteira.

O problema não é a anotação.

O problema é registrar como Bean algo que talvez devesse ser um objeto de curta duração.

Esse ponto conversa diretamente com o post sobre nem tudo precisar virar Bean.

O container é ótimo para colaboradores estáveis.

Ele não deveria virar depósito de qualquer objeto que aparece no fluxo.


Uma pergunta simples antes de usar @Bean

Antes de criar um método @Bean, faça uma pergunta:

qual problema específico este método resolve?

Boas respostas costumam ser:

  • preciso registrar uma classe de biblioteca externa;
  • preciso construir o objeto com builder ou factory;
  • preciso aplicar configuração centralizada;
  • preciso expor uma implementação concreta atrás de uma interface;
  • preciso criar um colaborador de infraestrutura reutilizável.

Respostas fracas costumam ser:

  • porque prefiro não usar @Service;
  • porque quero controlar tudo manualmente;
  • porque vi em outro projeto;
  • porque assim fica tudo em uma classe de configuração;
  • porque todo objeto importante deveria ser Bean.

@Bean é poderoso justamente porque deixa a criação explícita.

Mas explicitar sem motivo também cobra preço.

Você ganha mais uma camada para manter, nomear, testar e entender.


O ponto que muda sua leitura do Spring

@Bean não existe para competir com @Component.

Ele existe para um tipo diferente de situação.

Quando a classe é sua, tem papel claro e pode ser descoberta diretamente, uma anotação de estereótipo costuma deixar o modelo mais simples.

Quando o objeto precisa nascer por meio de uma decisão explícita de montagem, @Bean entra muito bem.

Essa é a diferença prática:

@Component diz:

"Spring, descubra esta classe."

@Bean diz:

"Spring, use este método para criar este objeto."

Os dois caminhos chegam ao container.

Mas eles não contam a mesma história.

E, em aplicações grandes, essa história importa.