skip to content

How do you create and use a custom qualifier annotation, and why prefer it over @Qualifier("string")?

level: seniorimportance: should knowfreq 46%

answer

  1. meta-annotate with @Qualifier
  2. RUNTIME retention required
  3. type-safe vs magic strings
  4. attributes matched value-by-value
  5. QualifierAnnotationAutowireCandidateResolver

basics

~20 s

Define your own annotation meta-annotated with @Qualifier, e.g. @Fast. Put it on the bean and on the injection point. Spring matches them like a typed qualifier. It's refactor-safe and typo-proof, unlike a plain @Qualifier("fast") string.

solid answer

~40 s

A custom qualifier is an annotation you declare and meta-annotate with @Qualifier (plus @Retention(RUNTIME) and appropriate @Target). Placing it on a bean definition and on the matching injection point makes Spring select that bean — same mechanism as @Qualifier("value"), but type-checked by the compiler. Benefits: no stringly-typed values to mistype, IDE find-usages/refactor works, and you can add attributes (e.g. @Datastore(kind=SQL)) that Spring matches value-by-value, giving multi-dimensional selection. Under the hood, QualifierAnnotationAutowireCandidateResolver reads these annotations and compares attributes between the injection point and candidate beans; all specified attributes must match. Custom qualifiers shine in larger codebases and libraries where a stable, semantic contract ("the fast cache", "the primary shard") is clearer and safer than scattering magic strings.

code

java · 20 lines
java
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

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

@Service @Replica
class ReadReplicaRepo implements OrderRepo { }

@Service
class PrimaryRepo implements OrderRepo { }

@Service
class ReportingService {
    private final OrderRepo repo;
    // Compile-checked selection of the replica-tagged bean
    ReportingService(@Replica OrderRepo repo) { this.repo = repo; }
}

go deeper

for a junior

Aware that custom qualifiers exist as an alternative to strings.

for a middle

Can define one with @Qualifier meta-annotation and RUNTIME retention and use it on both sides.

for a senior

Explains attribute matching and refactor-safety trade-offs vs string qualifiers.

for a principal

Decides when a qualifier belongs in a library's public API and how it interacts with @Primary/@Fallback.

## What a custom qualifier is Instead of `@Qualifier("fast")` (a string), you can define a **dedicated annotation** that *is* a qualifier by being meta-annotated with `@Qualifier`: ```java import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Fast { } ``` Then tag the bean and the injection point: ```java @Service @Fast class RedisCache implements Cache { } @Service class Reader { Reader(@Fast Cache cache) { /* gets RedisCache */ } } ``` Spring's `QualifierAnnotationAutowireCandidateResolver` recognizes any annotation meta-annotated with `@Qualifier` and uses it to filter autowire candidates — exactly like a string qualifier, but now it's a **compile-time symbol**. ## Why prefer it over `@Qualifier("string")` - **Typo-proof / refactor-safe:** renaming the annotation is a real refactor; a mistyped string qualifier only fails at runtime. - **Discoverable:** IDE find-usages shows every producer and consumer. - **Self-documenting:** `@Fast`, `@Replica`, `@PrimaryShard` carry domain meaning. - **Attribute matching:** you can give the annotation attributes and Spring matches them. ## Qualifiers with attributes ```java @Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Datastore { Kind kind(); boolean replica() default false; enum Kind { SQL, MONGO } } @Service @Datastore(kind = Kind.SQL, replica = true) class SqlReplicaRepo implements Repo { } class Consumer { Consumer(@Datastore(kind = Kind.SQL, replica = true) Repo repo) { } } ``` **All attributes specified at the injection point must match** the candidate's annotation for it to be selected. This lets you select along multiple dimensions without inventing composite strings. ## How matching actually works During candidate resolution, `QualifierAnnotationAutowireCandidateResolver.isAutowireCandidate(...)` checks each qualifier annotation on the injection point against the candidate bean definition's qualifiers (and the bean's own annotations). It compares the annotation type and every attribute value. Attributes with no value at the injection point aren't constrained; specified ones must equal the candidate's. ## Edge cases and notes - **You can register qualifier types explicitly** via `CustomAutowireConfigurer` when the annotation isn't picked up automatically (rare — meta-annotation is usually enough). - **Spring's `@Qualifier` and JSR-330's `jakarta.inject.Qualifier`** both work; a custom annotation can be meta-annotated with either. Spring understands both. - **A custom qualifier still coexists with @Primary:** the qualifier filters first; @Primary only tie-breaks what's left. - **Don't overuse:** for one-off disambiguation a string qualifier or @Primary is fine. Custom qualifiers pay off when a selection concept recurs across the codebase or is part of a library's public contract. - **Retention must be RUNTIME**, or Spring can't read it reflectively.

  • What retention policy must a custom qualifier annotation have and why?
    RUNTIME retention — Spring reads qualifier annotations reflectively at container startup. SOURCE or CLASS retention would strip them before Spring could see them.
  • If a custom qualifier has attributes, how strict is the match?
    Every attribute you specify at the injection point must equal the candidate bean's attribute value. Unspecified attributes are unconstrained. All specified ones must match for the bean to qualify.

saying these in an interview costs you the question

  • Forgetting @Retention(RUNTIME), so Spring can't see the qualifier
  • Thinking a custom qualifier needs manual CustomAutowireConfigurer in the common case (meta-annotation suffices)
  • Claiming attribute matching is 'any attribute' when it is 'all specified attributes must match'

context