No Spring, nem tudo precisa virar Bean

Quando um time começa a usar Spring com mais frequência, é comum surgir uma conclusão apressada:

se o container consegue criar e injetar objetos, talvez seja melhor deixar tudo nas mãos dele.

Daí começam a aparecer @Component, @Service e @Bean em classes que nunca precisaram participar do contexto da aplicação.

Na superfície, isso parece padronização.

Na prática, costuma misturar dois mundos que deveriam continuar diferentes.

Spring é muito bom em gerenciar colaboradores da aplicação.

Mas nem todo objeto é um colaborador gerenciado.

Muitos objetos deveriam continuar sendo apenas objetos Java comuns.


Ser gerenciado pelo Spring tem um significado

O container não é uma fábrica genérica para qualquer instância que apareça no sistema.

Quando uma classe vira Bean, ela passa a fazer parte do contexto da aplicação.

O Spring pode controlar seu ciclo de vida, entregar suas dependências, aplicar proxies e disponibilizar aquela instância para outros componentes.

Isso é útil para objetos como:

  • casos de uso;
  • gateways;
  • repositórios;
  • clientes HTTP;
  • configurações;
  • componentes de infraestrutura.

Esses objetos normalmente representam capacidades compartilhadas da aplicação.

Eles existem para realizar trabalho em nome de outras partes do sistema.

É diferente de um Pedido, um Endereco, um Dinheiro ou um PedidoResponse.

Esses objetos representam dados, estado ou conceitos que nascem durante uma operação específica.

Eles não precisam existir no container para cumprir seu papel.


O problema começa quando dado temporário vira estado compartilhado

Por padrão, um Bean do Spring tem escopo singleton.

Isso significa que o container cria uma instância e a compartilha entre as chamadas que usam aquele componente.

Para um serviço sem estado mutável, isso costuma funcionar bem:

@Service
public class CalculadoraFrete {

    public BigDecimal calcular(Pedido pedido) {
        return pedido.pesoTotal().multiply(new BigDecimal("0.15"));
    }
}

Mas o cenário muda quando alguém transforma um objeto temporário em componente:

@Component
public class CarrinhoCompra {

    private final List<Item> itens = new ArrayList<>();

    public void adicionar(Item item) {
        itens.add(item);
    }

    public BigDecimal total() {
        return itens.stream()
            .map(Item::subtotal)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

Esse código parece simples.

Mas agora o carrinho pode ser compartilhado entre requisições diferentes.

O item adicionado por um usuário pode continuar na mesma instância usada por outro.

O problema não é falta de anotação.

É exatamente o contrário.

Uma anotação colocou no container um objeto que deveria nascer para representar uma operação, um usuário ou um fluxo específico.


Objeto de domínio não precisa pedir permissão ao container

Existe um hábito perigoso em alguns projetos: se a classe tem regra, ela recebe @Component.

Mas comportamento de domínio não transforma automaticamente uma classe em infraestrutura gerenciada.

Veja um exemplo:

public class Pedido {

    private final List<ItemPedido> itens = new ArrayList<>();
    private StatusPedido status = StatusPedido.ABERTO;

    public void adicionar(ItemPedido item) {
        if (status != StatusPedido.ABERTO) {
            throw new IllegalStateException("Pedido não aceita novos itens");
        }

        itens.add(item);
    }

    public void fechar() {
        if (itens.isEmpty()) {
            throw new IllegalStateException("Pedido vazio não pode ser fechado");
        }

        status = StatusPedido.FECHADO;
    }
}

Essa classe tem estado e regra de negócio.

E justamente por isso cada pedido precisa ser uma instância própria.

Um pedido pode vir do banco, ser criado por uma factory ou nascer dentro de um caso de uso.

Ele não precisa ser injetado.

Ele não precisa ser descoberto por component scan.

Ele não precisa virar singleton.

Spring pode participar da aplicação sem assumir a criação de cada objeto que existe dentro dela.


Injeção de dependência não substitui criação normal de objetos

O post sobre quem cria os objetos no Spring mostrou uma mudança importante de controle: componentes da aplicação deixam de criar manualmente seus colaboradores.

Mas isso não significa banir new do código.

new continua sendo a forma natural de criar valores e objetos que pertencem ao fluxo atual:

@Service
public class CriarPedidoService {

    private final PedidoRepository pedidoRepository;

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

    public PedidoId criar(CriarPedidoCommand command) {
        Pedido pedido = new Pedido(command.clienteId());

        for (ItemCommand item : command.itens()) {
            pedido.adicionar(new ItemPedido(
                item.produtoId(),
                item.quantidade(),
                item.precoUnitario()
            ));
        }

        pedidoRepository.salvar(pedido);
        return pedido.getId();
    }
}

CriarPedidoService é um bom candidato a Bean.

Ele representa um caso de uso e precisa receber um repositório.

Pedido e ItemPedido nascem conforme a entrada recebida.

Eles representam o estado daquela operação.

Usar new aqui não quebra inversão de controle.

Só deixa claro que o container não precisa decidir tudo.


DTO, record e value object também não são componentes

O mesmo raciocínio vale para objetos simples:

public record Dinheiro(BigDecimal valor, Currency moeda) {
}

public record CriarPedidoCommand(
    ClienteId clienteId,
    List<ItemCommand> itens
) {
}

public record PedidoResponse(
    Long id,
    String status,
    BigDecimal total
) {
}

Esses objetos carregam valores.

Alguns podem validar invariantes.

Outros podem facilitar transporte de dados entre fronteiras.

Nenhum deles precisa ser registrado como Bean.

Transformá-los em componentes só aproxima conceitos simples do runtime do framework sem entregar ganho real.

Esse cuidado conversa diretamente com o post sobre DTO não ser Entity: classes diferentes existem por motivos diferentes. Colocar todas sob o mesmo mecanismo de criação apaga essas diferenças.


E quando um objeto precisa de dependências para ser criado?

Às vezes a construção de um objeto exige mais do que chamar um construtor.

Talvez seja necessário consultar configuração, aplicar uma política ou combinar dados externos.

Ainda assim, o objeto resultante não precisa virar Bean.

Uma factory gerenciada pode cuidar dessa criação:

@Component
public class PedidoFactory {

    private final PoliticaDesconto politicaDesconto;

    public PedidoFactory(PoliticaDesconto politicaDesconto) {
        this.politicaDesconto = politicaDesconto;
    }

    public Pedido criar(CriarPedidoCommand command) {
        Desconto desconto = politicaDesconto.calcularPara(command.clienteId());

        return new Pedido(
            command.clienteId(),
            command.itens(),
            desconto
        );
    }
}

Aqui a separação fica clara.

PedidoFactory é um colaborador compartilhado e recebe dependências do container.

Cada Pedido produzido continua sendo um objeto independente.

O Spring gerencia quem sabe construir.

Ele não precisa gerenciar cada resultado da construção.


Trocar singleton por prototype raramente corrige o desenho

Quando o time percebe que uma instância compartilhada causa problema, pode surgir uma tentativa de remendo:

@Component
@Scope("prototype")
public class Pedido {
}

Agora o Spring consegue criar uma nova instância quando o Bean é solicitado.

Mas a pergunta principal continua sem resposta:

por que Pedido precisa estar no container?

Escopos como prototype, request e session têm usos legítimos.

Eles são úteis quando existe uma necessidade real de ciclo de vida gerenciado naquele contexto.

Mas não deveriam virar uma forma mais sofisticada de criar objetos comuns.

Se uma classe pode nascer com um construtor simples e viver naturalmente dentro do fluxo que a criou, envolver o container normalmente adiciona dependência do framework sem melhorar o desenho.


O ponto que vale fixar

Spring só gerencia o que é Bean.

Mas isso não significa que tudo deveria virar Bean.

Coloque no container os colaboradores que representam capacidades da aplicação e realmente precisam de injeção, configuração, ciclo de vida ou comportamento aplicado pelo framework.

Deixe como objetos Java comuns os dados, valores, entidades e instâncias temporárias que pertencem a uma operação específica.

O container é uma parte importante da arquitetura.

Ele não precisa ser o dono de todos os objetos do sistema.