skip to content

How would you create a custom stereotype annotation via meta-annotation composition, and why?

level: seniorimportance: should knowfreq 40%

answer

  1. custom annotation + @Component meta-annotation = new stereotype
  2. MergedAnnotations / AnnotatedElementUtils resolves meta-presence
  3. @AliasFor to alias value/attributes
  4. bundle @Transactional/@Scope/@Validated
  5. RUNTIME retention + TYPE target required

basics

~10 s

Define your own annotation and meta-annotate it with @Component (and any others you want to bundle, like @Scope or @Transactional). Component scanning treats classes with your annotation as beans because @Component is present transitively.

solid answer

~40 s

Spring stereotypes are just annotations meta-annotated with @Component, and Spring's scanner detects any annotation carrying @Component transitively. So you can compose a custom stereotype: create @interface MyUseCase annotated with @Component, and optionally bundle other cross-cutting annotations — @Scope, @Transactional, @Validated, or a qualifier. Classes annotated @MyUseCase are then registered as beans and inherit the bundled semantics. You can expose attributes and alias them to the composed annotations' attributes using @AliasFor (Spring's annotation model, AnnotatedElementUtils, resolves these). This is useful to enforce team conventions (every use-case is transactional and validated), reduce annotation clutter, and give layers a domain-specific vocabulary. The default bean-name generator still derives the name from the class unless the stereotype declares a String value aliased to @Component's value.

code

java · 19 lines
java
import java.lang.annotation.*;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component        // scannable stereotype
@Transactional    // every use case is transactional
public @interface UseCase {
    @AliasFor(annotation = Component.class, attribute = "value")
    String value() default ""; // custom bean name -> delegates to @Component
}

@UseCase("placeOrder") // registered as bean named "placeOrder", transactional
public class PlaceOrderUseCase {
    public void execute() { /* ... */ }
}

go deeper

for a junior

Likely just knows the four built-ins; recognizing that custom ones are possible is a plus.

for a middle

Can create a @Component-meta-annotated custom stereotype and explain it's scanned.

for a senior

Adds @AliasFor attribute aliasing, bundling @Transactional/@Scope, and the merged-annotation model.

for a principal

Frames it as convention enforcement / architecture fitness, notes tooling caveats (reflection vs AnnotatedElementUtils) and cites @SpringBootApplication as a composed annotation.

**Foundation:** Spring's component scanning uses `AnnotatedBeanDefinitionReader`/`ClassPathScanningCandidateComponentProvider`, which look for `@Component` **as a meta-annotation** using Spring's merged-annotation model (`AnnotatedElementUtils` / `MergedAnnotations`). This model treats an annotation A as 'present' on an element if A is directly there **or** meta-present through another annotation. That is exactly why `@Service` (annotated `@Component`) is scanned. You can exploit the same rule. **Creating a custom stereotype:** ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Component // makes it a scannable stereotype @Scope("singleton") // optional bundled semantics public @interface UseCase { @AliasFor(annotation = Component.class, attribute = "value") String value() default ""; } ``` Any `@UseCase`-annotated class is now a bean. `@AliasFor(annotation = Component.class, attribute = "value")` means `@UseCase("foo")` sets the bean name to `foo`, delegating to `@Component`'s `value`. Without that alias, the class-derived default name applies. **Bundling behavior (composed annotations):** you can stack multiple meta-annotations to package a convention: ```java @Component @Transactional @Validated public @interface TxUseCase {} ``` Now every `@TxUseCase` bean is transactional and bean-validated — enforcing an architectural rule in one place. (For `@Transactional` attributes to be overridable from the custom annotation you again use `@AliasFor`.) **Why do this:** - **Convention enforcement** — guarantee every persistence class or use case has the same cross-cutting config. - **Domain vocabulary** — `@DomainService`, `@UseCase`, `@RestApi` read better than generic `@Component`. - **Refactor leverage** — change the convention (add caching, change scope) in the meta-annotation, not at every site. - **Custom scan filters / AOP** — you can write pointcuts or `@ComponentScan` include-filters that target your annotation. **Gotchas:** - Set `@Retention(RUNTIME)` and an appropriate `@Target` (usually `TYPE`), or scanning/AOP won't see it. - Meta-annotation detection is **transitive**, so `@Component` need not be direct; but the class still must be under a scanned base package. - `@AliasFor` requires exact attribute types and default values; misuse throws `AnnotationConfigurationException` at startup. - Attribute **override** only merges through Spring's annotation model (`AnnotatedElementUtils`), not plain Java reflection (`getAnnotation`), so third-party tools reading annotations reflectively won't see merged values. - `@Configuration`, `@RestController`, `@ControllerAdvice`, and `@SpringBootApplication` are themselves composed annotations — real proof the pattern is idiomatic.

  • Why does a class annotated only with your custom @UseCase get picked up by component scanning?
    Because @UseCase is meta-annotated with @Component; Spring's merged-annotation model treats @Component as meta-present, and the scanner's AnnotationTypeFilter for @Component matches transitively.
  • What does @AliasFor(annotation = Component.class, attribute = "value") accomplish?
    It aliases the custom annotation's value to @Component's value, so passing a name in @UseCase("x") sets the bean name x instead of the class-derived default.

saying these in an interview costs you the question

  • Thinking you must re-implement the scanner to support custom stereotypes
  • Forgetting RUNTIME retention so the annotation is invisible at runtime
  • Assuming plain reflection getAnnotation() merges meta-annotation attributes (it doesn't; needs AnnotatedElementUtils)
  • Believing custom stereotypes bypass base-package scanning rules

context