No Spring, @Primary não resolve ambiguidade. Ele escolhe um vencedor

Existe uma confusão comum em aplicações Spring:

quando o container reclama que encontrou mais de um Bean para injetar, alguém adiciona @Primary em uma das implementações e considera o problema resolvido.

A aplicação volta a subir.

O erro desaparece.

O deploy segue.

Mas a ambiguidade conceitual continua lá.

O que mudou foi apenas isto: o Spring agora sabe qual candidato deve vencer quando ninguém for mais específico.

@Primary não explica intenção de negócio.

@Primary não escolhe com base no caso de uso.

@Primary não transforma duas implementações equivalentes em um desenho claro.

Ele só define um padrão.

E padrão global é uma decisão forte demais para ser usada como curativo em qualquer ambiguidade.


O problema aparece quando existem vários beans do mesmo tipo

Imagine uma aplicação com duas formas de enviar notificação:

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

Uma implementação envia e-mail:

@Service
public class NotificadorEmail implements Notificador {

    @Override
    public void enviar(Mensagem mensagem) {
        // envia e-mail
    }
}

Outra envia SMS:

@Service
public class NotificadorSms implements Notificador {

    @Override
    public void enviar(Mensagem mensagem) {
        // envia SMS
    }
}

Agora um serviço depende de Notificador:

@Service
public class PedidoService {

    private final Notificador notificador;

    public PedidoService(Notificador notificador) {
        this.notificador = notificador;
    }
}

Para quem lê o código, a pergunta é inevitável:

qual notificador esse serviço quer?

Para o Spring, a pergunta é ainda mais concreta:

qual Bean do tipo Notificador deve ser injetado?

Existem dois candidatos.

Nenhum deles é mais específico pelo tipo.

O ponto de injeção não deu nenhuma pista.

Então o Spring falha.

E ele está certo em falhar.

O erro não é burocracia do framework. É o container avisando que o desenho deixou uma decisão em aberto.


@Primary define o candidato padrão

Uma forma de fazer a aplicação voltar a subir é marcar uma implementação como primária:

@Service
@Primary
public class NotificadorEmail implements Notificador {

    @Override
    public void enviar(Mensagem mensagem) {
        // envia e-mail
    }
}

Agora, quando o Spring encontrar um ponto de injeção por tipo e houver mais de um candidato compatível, ele preferirá NotificadorEmail.

O serviço abaixo receberá e-mail:

@Service
public class PedidoService {

    private final Notificador notificador;

    public PedidoService(Notificador notificador) {
        this.notificador = notificador;
    }
}

Isso não significa que o Spring "entendeu" que pedido deve usar e-mail.

Significa apenas que, diante de múltiplos candidatos, existe um vencedor padrão.

Essa diferença importa.

@Primary é uma regra de desempate do container.

Não é uma regra de domínio.

Quando o time esquece essa fronteira, @Primary começa a ser usado para esconder decisões que deveriam aparecer no código.


O código passa a depender de uma escolha distante

O problema de usar @Primary sem critério é que o ponto de injeção continua genérico demais.

Veja novamente:

public PedidoService(Notificador notificador) {
    this.notificador = notificador;
}

Esse construtor não diz se precisa de e-mail, SMS, push, WhatsApp, fake de teste, fallback ou qualquer outra variante.

Ele só diz:

"me dê algum Notificador".

A decisão real ficou em outro lugar, na anotação de uma das implementações.

Isso cria um acoplamento estranho.

Quem lê PedidoService precisa saber que, em algum canto da aplicação, existe um @Primary influenciando o que entra ali.

Pior: se amanhã alguém trocar o @Primary para NotificadorSms, PedidoService muda de comportamento sem nenhuma alteração no próprio serviço.

O ponto de injeção parecia estável.

Mas a escolha estava externa, global e silenciosa.

É o mesmo tipo de risco discutido no post sobre configuração importar mais do que parece: no Spring, configuração muda comportamento. Por isso ela não deveria ser usada para esconder uma decisão importante sem necessidade.


@Primary é aceitável quando existe um padrão técnico real

Isso não quer dizer que @Primary seja ruim.

Ele faz sentido quando existe uma implementação padrão de verdade.

Por exemplo, imagine um cliente HTTP usado pela maior parte da aplicação:

public interface CatalogoClient {
    Produto buscarPorSku(String sku);
}

A implementação real chama o serviço externo:

@Service
@Primary
public class CatalogoHttpClient implements CatalogoClient {

    @Override
    public Produto buscarPorSku(String sku) {
        // chamada HTTP real
    }
}

E uma implementação alternativa existe para um uso muito específico:

@Service
public class CatalogoCacheWarmupClient implements CatalogoClient {

    @Override
    public Produto buscarPorSku(String sku) {
        // leitura otimizada para rotina interna de aquecimento de cache
    }
}

Nesse cenário, pode existir uma escolha padrão coerente.

Na maior parte da aplicação, CatalogoHttpClient é o cliente esperado.

Nos pontos excepcionais, o código deve ser explícito.

O ponto importante é este:

@Primary funciona melhor quando ele expressa um padrão técnico estável, não quando tenta resolver uma dúvida funcional.

Se a pergunta é "qual implementação representa o caminho normal da infraestrutura?", @Primary pode ser adequado.

Se a pergunta é "qual comportamento de negócio este caso de uso deve executar?", provavelmente não.


Quando a escolha importa, use qualificação explícita

Se um serviço precisa especificamente de e-mail, o código deveria dizer isso.

Uma forma simples é usar @Qualifier:

@Service
public class PedidoService {

    private final Notificador notificador;

    public PedidoService(@Qualifier("notificadorEmail") Notificador notificador) {
        this.notificador = notificador;
    }
}

Agora a decisão aparece no ponto em que ela importa.

O serviço não está pedindo qualquer notificador.

Ele está pedindo o notificador de e-mail.

Isso torna a leitura mais honesta.

Também reduz o risco de uma alteração global em @Primary mudar comportamento onde não deveria.

Mas @Qualifier também não deve virar muleta automática.

Se vários serviços começam a depender de nomes específicos de beans, talvez o problema seja outro: a abstração Notificador está genérica demais para representar decisões diferentes.

Nesse caso, pode ser melhor separar interfaces.


Às vezes a abstração está ampla demais

Nem toda interface precisa representar todas as variações possíveis de uma família.

Se uma parte do sistema só envia e-mail, talvez ela não precise depender de Notificador.

Ela pode depender de algo mais específico:

public interface EnviadorEmailPedido {
    void enviarConfirmacao(Pedido pedido);
}

Com uma implementação clara:

@Service
public class EnviadorEmailPedidoSmtp implements EnviadorEmailPedido {

    @Override
    public void enviarConfirmacao(Pedido pedido) {
        // monta e envia o e-mail de confirmação
    }
}

Agora a dependência conversa com o caso de uso.

O serviço não precisa saber que existe SMS.

Não precisa depender de uma interface genérica.

Não precisa disputar @Primary.

Isso costuma melhorar o desenho porque reduz uma abstração artificial.

Uma interface ampla demais pode parecer elegante, mas às vezes só empurra uma escolha para o container.

E container nenhum deveria ser obrigado a adivinhar intenção de negócio.


Quando a escolha é dinâmica, @Primary é o mecanismo errado

Há outro cenário comum: a implementação depende dos dados da requisição, do cliente, do canal ou do tipo de operação.

Por exemplo:

public enum CanalNotificacao {
    EMAIL,
    SMS
}

Nesse caso, não existe um vencedor global.

O vencedor depende do contexto.

Marcar e-mail como @Primary não resolve isso.

Só cria uma escolha padrão que pode ser errada para parte dos casos.

Uma alternativa mais explícita é carregar todas as estratégias e selecionar em tempo de execução:

public interface Notificador {
    CanalNotificacao canal();
    void enviar(Mensagem mensagem);
}
@Service
public class NotificadorEmail implements Notificador {

    @Override
    public CanalNotificacao canal() {
        return CanalNotificacao.EMAIL;
    }

    @Override
    public void enviar(Mensagem mensagem) {
        // envia e-mail
    }
}
@Service
public class NotificadorSms implements Notificador {

    @Override
    public CanalNotificacao canal() {
        return CanalNotificacao.SMS;
    }

    @Override
    public void enviar(Mensagem mensagem) {
        // envia SMS
    }
}

E uma fábrica ou registry faz a seleção:

@Component
public class Notificadores {

    private final Map<CanalNotificacao, Notificador> notificadores;

    public Notificadores(List<Notificador> notificadores) {
        this.notificadores = notificadores.stream()
            .collect(Collectors.toMap(Notificador::canal, Function.identity()));
    }

    public Notificador porCanal(CanalNotificacao canal) {
        Notificador notificador = notificadores.get(canal);

        if (notificador == null) {
            throw new IllegalArgumentException("Canal sem notificador: " + canal);
        }

        return notificador;
    }
}

Agora a regra fica explícita:

@Service
public class PedidoService {

    private final Notificadores notificadores;

    public PedidoService(Notificadores notificadores) {
        this.notificadores = notificadores;
    }

    public void confirmar(Pedido pedido) {
        CanalNotificacao canal = pedido.canalPreferido();
        notificadores.porCanal(canal).enviar(Mensagem.confirmacao(pedido));
    }
}

Esse desenho não pergunta ao Spring "qual é o notificador vencedor da aplicação inteira?".

Ele pergunta ao domínio "qual canal este pedido deve usar?".

Essa é uma diferença grande.

Quando a escolha é dinâmica, ela precisa morar no fluxo da aplicação.


@Primary pode esconder testes frágeis

O uso descuidado de @Primary também aparece em testes.

Um teste sobe contexto Spring e precisa substituir uma dependência real por uma fake.

Então alguém registra um bean fake como primário:

@TestConfiguration
public class TestConfig {

    @Bean
    @Primary
    public GatewayPagamento gatewayPagamentoFake() {
        return new GatewayPagamentoFake();
    }
}

Isso pode ser útil em testes de integração.

Mas também pode esconder dependências demais.

Se muitos testes precisam de beans primários alternativos para conseguir subir, talvez a aplicação esteja difícil de montar em cenários controlados.

Às vezes o problema não é falta de @Primary.

É excesso de contexto para testar uma regra pequena.

Para teste unitário, constructor injection normalmente resolve melhor:

GatewayPagamento gateway = new GatewayPagamentoFake();
PedidoService service = new PedidoService(gateway);

Sem container.

Sem disputa de beans.

Sem primário.

O teste monta explicitamente o objeto com a dependência que precisa.

Isso conversa diretamente com o ponto do post sobre @Autowired não deveria ser seu padrão: dependências claras tornam o código mais simples de montar e entender.


O erro do Spring é um sinal útil

Quando o Spring diz que encontrou múltiplos beans para uma dependência, a primeira reação não deveria ser procurar onde colocar @Primary.

A primeira reação deveria ser investigar a natureza da ambiguidade.

Pergunte:

  • existe mesmo uma implementação padrão para a aplicação inteira?
  • este ponto de injeção precisa de uma implementação específica?
  • a escolha depende de dados de runtime?
  • a interface está genérica demais?
  • a variação é técnica, de ambiente ou de domínio?
  • esse bean deveria existir neste contexto?

Dependendo da resposta, a solução muda.

Se existe um padrão técnico real, @Primary pode fazer sentido.

Se o ponto de injeção precisa de uma implementação específica, @Qualifier pode ser melhor.

Se a escolha é dinâmica, use uma estratégia, factory ou registry.

Se a interface está ampla demais, reduza a abstração.

Se o bean só deveria existir em determinado ambiente, talvez @Profile ou configuração condicional seja o mecanismo correto.

O erro do container não é só uma falha de configuração.

Muitas vezes ele é uma oportunidade de melhorar o desenho.


O ponto principal

@Primary não resolve ambiguidade no sentido arquitetural.

Ele apenas diz ao Spring qual Bean deve vencer quando houver mais de um candidato e o ponto de injeção não for específico.

Isso pode ser exatamente o que você quer.

Mas também pode ser um jeito elegante de esconder uma decisão mal posicionada.

Use @Primary quando houver um padrão claro, estável e coerente para o contexto da aplicação.

Não use @Primary para evitar pensar por que existem múltiplas implementações e qual delas cada caso de uso realmente precisa.

No Spring, ambiguidade não some porque uma classe ganhou uma anotação.

Ela só ficou menos barulhenta.

E problema menos barulhento nem sempre é problema resolvido.