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.
