No Spring, @Qualifier não é um detalhe. É parte da modelagem

Existe um momento comum em aplicações Spring:

o projeto tem mais de uma implementação para a mesma interface, o container não sabe qual Bean injetar e alguém resolve com @Qualifier.

A aplicação volta a subir.

O erro desaparece.

O serviço recebe a implementação esperada.

Mas a pergunta importante continua ali:

por que aquele ponto de injeção precisava daquela implementação específica?

Essa pergunta não é detalhe técnico.

Ela é modelagem.

Quando existem várias formas válidas de cumprir um contrato, escolher uma delas faz parte do desenho da aplicação.

@Qualifier não deveria ser tratado como fita isolante para o container.

Ele deveria tornar explícita uma decisão que já existe no domínio, no caso de uso ou na infraestrutura.


O problema aparece quando o tipo não conta a história inteira

Imagine uma aplicação que precisa calcular frete.

Existe uma interface:

public interface CalculadoraFrete {
    BigDecimal calcular(Pedido pedido);
}

E duas implementações:

@Service
public class CalculadoraFreteCorreios implements CalculadoraFrete {

    @Override
    public BigDecimal calcular(Pedido pedido) {
        // calcula usando tabela dos Correios
    }
}
@Service
public class CalculadoraFreteTransportadora implements CalculadoraFrete {

    @Override
    public BigDecimal calcular(Pedido pedido) {
        // calcula usando contrato da transportadora
    }
}

Agora um serviço depende de CalculadoraFrete:

@Service
public class FecharPedidoService {

    private final CalculadoraFrete calculadoraFrete;

    public FecharPedidoService(CalculadoraFrete calculadoraFrete) {
        this.calculadoraFrete = calculadoraFrete;
    }
}

Para o Java, o tipo parece suficiente.

Para o Spring, não é.

Existem dois Beans compatíveis.

O ponto de injeção diz apenas:

"preciso de uma calculadora de frete".

Mas a aplicação tem duas.

O container não consegue adivinhar qual delas representa a decisão correta para aquele caso de uso.

E ele não deveria adivinhar mesmo.

Quando o tipo abstrato é amplo demais, a escolha da implementação passa a carregar significado.

Esse significado precisa aparecer em algum lugar.


@Qualifier dá nome para uma escolha

Uma forma direta de resolver a ambiguidade é nomear os Beans e declarar qual deles o serviço quer:

@Service("freteCorreios")
public class CalculadoraFreteCorreios implements CalculadoraFrete {

    @Override
    public BigDecimal calcular(Pedido pedido) {
        // calcula usando tabela dos Correios
    }
}
@Service("freteTransportadora")
public class CalculadoraFreteTransportadora implements CalculadoraFrete {

    @Override
    public BigDecimal calcular(Pedido pedido) {
        // calcula usando contrato da transportadora
    }
}

E então:

@Service
public class FecharPedidoService {

    private final CalculadoraFrete calculadoraFrete;

    public FecharPedidoService(
        @Qualifier("freteCorreios") CalculadoraFrete calculadoraFrete
    ) {
        this.calculadoraFrete = calculadoraFrete;
    }
}

Agora o Spring sabe qual Bean usar.

Mas a parte mais importante não é o Spring saber.

É o leitor saber.

O construtor deixou de dizer apenas "preciso de uma calculadora".

Ele passou a dizer:

"este serviço fecha pedido usando a calculadora dos Correios".

Isso muda a leitura do código.

A dependência deixa de ser uma abstração genérica e passa a revelar uma variante concreta do processo.

Se essa escolha é relevante para o comportamento do caso de uso, ela não é ruído.

Ela é parte do contrato daquele componente.


O erro é usar @Qualifier para esconder uma modelagem fraca

O problema começa quando @Qualifier vira uma correção automática para qualquer ambiguidade.

O time vê o erro:

required a single bean, but 2 were found

E responde assim:

public FecharPedidoService(
    @Qualifier("calculadoraFreteCorreios") CalculadoraFrete calculadoraFrete
) {
    this.calculadoraFrete = calculadoraFrete;
}

Sem discutir se o serviço deveria depender dessa implementação.

Sem discutir se existem dois casos de uso diferentes.

Sem discutir se a escolha deveria ser feita em tempo de execução.

Sem discutir se a interface está genérica demais.

O código fica tecnicamente correto, mas conceitualmente pobre.

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

É uma abstração que juntou coisas diferentes sob o mesmo nome.

Por exemplo:

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

Se existem NotificadorEmail, NotificadorSms, NotificadorPush e NotificadorWhatsApp, talvez o ponto principal não seja escolher um Bean.

Talvez a pergunta real seja:

qual canal este caso de uso representa?

Ou:

o canal é uma regra fixa do fluxo ou uma decisão calculada a partir do cliente, do pedido ou da configuração?

Essas duas respostas levam a desenhos diferentes.

@Qualifier resolve apenas uma delas.


Quando a escolha é fixa, o Qualifier pode deixar o contrato mais honesto

Se um caso de uso sempre precisa de uma implementação específica, @Qualifier pode ser uma boa forma de explicitar isso.

Imagine um serviço que envia um código de recuperação de senha apenas por e-mail:

public interface EnviadorMensagem {
    void enviar(Destinatario destinatario, String texto);
}
@Service("email")
public class EnviadorEmail implements EnviadorMensagem {

    @Override
    public void enviar(Destinatario destinatario, String texto) {
        // envia e-mail
    }
}
@Service("sms")
public class EnviadorSms implements EnviadorMensagem {

    @Override
    public void enviar(Destinatario destinatario, String texto) {
        // envia SMS
    }
}

O caso de uso pode ser explícito:

@Service
public class RecuperarSenhaService {

    private final EnviadorMensagem enviadorEmail;

    public RecuperarSenhaService(
        @Qualifier("email") EnviadorMensagem enviadorEmail
    ) {
        this.enviadorEmail = enviadorEmail;
    }
}

Aqui o @Qualifier não está escondendo uma escolha.

Ele está documentando uma escolha fixa.

O serviço não quer "qualquer enviador".

Ele quer o enviador de e-mail.

Nomear o atributo como enviadorEmail reforça a mesma ideia.

Isso parece pequeno, mas evita uma leitura falsa do código.

Se o atributo se chama enviadorMensagem, parece que qualquer canal serve.

Se o atributo se chama enviadorEmail, o próprio objeto declara sua expectativa.

Esse cuidado é importante porque injeção de dependência também comunica desenho.


Quando a escolha é dinâmica, Qualifier no construtor pode estar no lugar errado

Agora imagine outro caso:

o usuário escolhe se quer receber notificação por e-mail, SMS ou WhatsApp.

Nesse cenário, o serviço não deveria injetar uma implementação fixa com @Qualifier.

Porque a escolha não pertence ao bootstrap da aplicação.

Ela pertence ao fluxo de execução.

Um desenho melhor pode usar um resolvedor:

public enum CanalNotificacao {
    EMAIL,
    SMS,
    WHATSAPP
}
public interface EnviadorMensagem {
    CanalNotificacao canal();

    void enviar(Destinatario destinatario, String texto);
}
@Component
public class EnviadorMensagemResolver {

    private final Map<CanalNotificacao, EnviadorMensagem> enviadores;

    public EnviadorMensagemResolver(List<EnviadorMensagem> enviadores) {
        this.enviadores = enviadores.stream()
            .collect(Collectors.toMap(EnviadorMensagem::canal, Function.identity()));
    }

    public EnviadorMensagem resolver(CanalNotificacao canal) {
        EnviadorMensagem enviador = enviadores.get(canal);

        if (enviador == null) {
            throw new IllegalArgumentException("Canal não suportado: " + canal);
        }

        return enviador;
    }
}

E o caso de uso decide com base na entrada:

@Service
public class EnviarNotificacaoService {

    private final EnviadorMensagemResolver resolver;

    public EnviarNotificacaoService(EnviadorMensagemResolver resolver) {
        this.resolver = resolver;
    }

    public void enviar(SolicitacaoNotificacao solicitacao) {
        EnviadorMensagem enviador = resolver.resolver(solicitacao.canal());
        enviador.enviar(solicitacao.destinatario(), solicitacao.texto());
    }
}

Perceba a diferença.

Aqui não existe uma implementação certa para o serviço inteiro.

Existe uma implementação certa para cada solicitação.

Colocar @Qualifier("email") no construtor seria uma mentira de modelagem.

O serviço não depende de e-mail.

Ele depende de uma regra de seleção de canal.

Quando a escolha é dinâmica, o código precisa modelar a escolha.

Não apenas escolher um Bean na largada.


@Qualifier também pode revelar nomes ruins

Um sinal de alerta aparece quando o nome do qualifier precisa explicar demais:

@Qualifier("clienteHttpPedidoComTimeoutMaiorParaIntegracaoLegada")

Esse tipo de nome costuma indicar que a variação é mais rica do que uma simples etiqueta.

Talvez exista uma política de timeout.

Talvez exista um gateway para sistema legado.

Talvez exista um cliente específico para um bounded context.

Talvez o problema não seja "qual cliente HTTP injetar", mas "qual colaboração este caso de uso realmente tem".

Compare:

public ProcessarPedidoService(
    @Qualifier("clienteHttpPedidoComTimeoutMaiorParaIntegracaoLegada")
    PedidoClient pedidoClient
) {
    this.pedidoClient = pedidoClient;
}

Com:

public ProcessarPedidoService(PedidoLegadoGateway pedidoLegadoGateway) {
    this.pedidoLegadoGateway = pedidoLegadoGateway;
}

No segundo caso, o tipo já carrega intenção.

O Spring não precisa de uma etiqueta para diferenciar duas coisas que o modelo tratou como iguais.

Isso não significa criar interfaces para tudo.

Significa perceber quando duas dependências têm responsabilidades diferentes o suficiente para merecer nomes diferentes.

Às vezes o melhor @Qualifier é um tipo mais específico.


Annotation customizada pode ser melhor que string solta

Outro problema comum é espalhar strings pelo código:

@Qualifier("email")
@Qualifier("sms")
@Qualifier("whatsapp")

Funciona.

Mas strings são frágeis.

Um typo vira erro de configuração.

Uma renomeação pode passar batida.

Em cenários importantes, uma annotation customizada deixa a intenção mais forte:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
public @interface CanalEmail {
}

Uso na implementação:

@Service
@CanalEmail
public class EnviadorEmail implements EnviadorMensagem {
}

Uso no ponto de injeção:

public RecuperarSenhaService(@CanalEmail EnviadorMensagem enviadorEmail) {
    this.enviadorEmail = enviadorEmail;
}

Agora a escolha virou um conceito nomeado no código.

Não é só uma string que precisa coincidir.

É uma marca explícita da aplicação.

Esse recurso deve ser usado com critério.

Se cada bean ganhar uma annotation própria sem motivo, o projeto só troca confusão por cerimônia.

Mas quando a variação é central e aparece em vários pontos, uma annotation semântica pode ser mais clara do que repetir nomes soltos.


@Primary e @Qualifier contam histórias diferentes

No post sobre @Primary, a ideia principal era que @Primary escolhe um vencedor padrão.

@Qualifier faz outra coisa.

Ele escolhe explicitamente um candidato.

Com @Primary, o ponto de injeção continua dizendo:

"me dê o padrão".

Com @Qualifier, o ponto de injeção diz:

"me dê este".

Essa diferença é grande.

Use @Primary quando existe um padrão real para a aplicação.

Use @Qualifier quando aquele componente precisa de uma variante específica.

Use outro desenho quando a escolha depende do fluxo, do usuário, do pedido, do tenant, do país ou de qualquer dado que só aparece em tempo de execução.

O erro é tratar tudo como se fosse a mesma decisão.

Não é.

Ambiguidade de bean pode ser sintoma de várias coisas:

  • existe um padrão técnico;
  • existe uma dependência específica;
  • existe uma decisão dinâmica;
  • existe uma abstração genérica demais;
  • existe uma responsabilidade mal nomeada.

Cada caso pede uma resposta diferente.


O que observar antes de adicionar um Qualifier

Antes de colocar @Qualifier, vale fazer algumas perguntas:

  • este serviço precisa sempre desta implementação?
  • o nome do atributo deixa essa escolha clara?
  • a escolha pertence ao bootstrap ou ao fluxo de execução?
  • existe uma regra de negócio escolhendo entre variantes?
  • um tipo mais específico comunicaria melhor a intenção?
  • estou usando @Qualifier para revelar uma decisão ou para esconder uma ambiguidade?

Essas perguntas evitam um erro comum:

resolver o container antes de entender o modelo.

Spring consegue injetar quase qualquer coisa quando você dá nomes suficientes.

Mas fazer o container encontrar um Bean não significa que o desenho ficou claro.

Clareza vem de dependências que dizem o que são, por que existem e qual papel cumprem no caso de uso.

@Qualifier pode ajudar nisso.

Mas só quando ele é consequência de uma decisão bem entendida.


O ponto principal

@Qualifier não é apenas um detalhe para satisfazer o Spring.

Ele é uma declaração de intenção.

Quando um componente pede uma implementação específica, essa escolha passa a fazer parte do contrato daquele componente.

Se a escolha é fixa, deixe isso explícito.

Se a escolha é dinâmica, modele a seleção.

Se o qualifier parece carregar uma frase inteira, talvez falte um tipo ou conceito melhor.

O Spring não está só reclamando de dois Beans.

Ele está mostrando que o seu modelo tem mais de uma resposta possível.

E quando existem várias respostas possíveis, a pior decisão é fingir que isso é só configuração.