Meta-Annotations in Java (@Target, @Retention, @Documented, @Inherited) Explained with Examples

Illustration for Meta-Annotations in Java (@Target, @Retention, @Documented, @Inherited) Explained with Examples
By Last updated:

A frequent mistake developers make when defining custom annotations is forgetting to control how and where the annotation should apply. For example, creating an annotation without specifying its retention policy might make it disappear at runtime when you need it most, or forgetting to use @Target may allow annotations to be placed on elements where they make no sense.

This is where meta-annotations come into play. Meta-annotations are annotations applied to other annotations. They define critical rules such as:

  • Where an annotation can be used (@Target)
  • How long it should be retained (@Retention)
  • Whether it should appear in Javadoc (@Documented)
  • Whether it can be inherited by subclasses (@Inherited)

Without meta-annotations, annotations become vague and error-prone. They are the blueprints that give meaning and context to custom annotations, ensuring they work consistently in frameworks like Spring (@Service), Hibernate (@Entity), or JUnit (@Test).

Think of meta-annotations as traffic laws for annotations—they tell annotations where they can drive, how long they stay visible, and whether they pass on rules to descendants.


@Target

Purpose

Specifies where an annotation can be applied (class, method, field, parameter, etc.).

Example

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Audit {
}

Here, @Audit can only be applied to methods and constructors:

public class Service {
    @Audit
    public void processOrder() { }
}

Pitfall

If you don’t use @Target, the annotation can be applied everywhere, which may cause misuse.


@Retention

Purpose

Defines how long the annotation should be retained:

  • SOURCE – discarded at compile time.
  • CLASS – stored in class file but not available at runtime.
  • RUNTIME – available at runtime via reflection.

Example

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable { }
public class Example {
    @Loggable
    public void run() { }
}

Frameworks like Spring and Hibernate rely on RUNTIME annotations to configure behavior dynamically.

Pitfall

Using CLASS instead of RUNTIME means your framework cannot detect the annotation at runtime.


@Documented

Purpose

Ensures that custom annotations appear in generated Javadoc. By default, custom annotations are excluded from Javadoc.

Example

import java.lang.annotation.Documented;

@Documented
public @interface PublicApi { }

When generating documentation, methods annotated with @PublicApi will show up in the Javadoc.

Real-World Use

Frameworks expose annotations like @Deprecated in Javadoc to guide developers toward alternatives.


@Inherited

Purpose

Allows a custom annotation applied at the class level to be inherited by subclasses.

Example

import java.lang.annotation.Inherited;

@Inherited
public @interface Secured { }

@Secured
class BaseService { }

class UserService extends BaseService { }

Here, UserService automatically inherits the @Secured annotation from BaseService.

Pitfall

  • Only works on class-level annotations.
  • Does not apply to methods or fields.

📌 What's New in Java Versions?

  • Java 5 – Introduced meta-annotations (@Target, @Retention, @Documented, @Inherited).
  • Java 8 – Added support for type annotations, expanding @Target.
  • Java 9 – Allowed annotations in module descriptors.
  • Java 11 – No significant changes for meta-annotations.
  • Java 17 – No significant changes.
  • Java 21 – No significant changes.

Pitfalls and Best Practices

Pitfalls

  • Forgetting @Retention(RUNTIME) when reflection is needed.
  • Misusing @Target → annotation ends up in places it doesn’t belong.
  • Assuming @Inherited applies to methods and fields (it doesn’t).

Best Practices

  • Always define both @Target and @Retention in custom annotations.
  • Use @Documented for public-facing APIs.
  • Keep inheritance intentional—avoid over-reliance on @Inherited.

Summary + Key Takeaways

  • Meta-annotations define the rules of engagement for custom annotations.
  • @Target restricts where annotations can be applied.
  • @Retention decides how long annotations are kept.
  • @Documented makes annotations part of API documentation.
  • @Inherited passes class-level annotations to subclasses.
  • Using them properly avoids annotation misuse and strengthens framework-level design.

FAQ

  1. When should I use RetentionPolicy.RUNTIME instead of CLASS?
    Use RUNTIME when frameworks or reflection-based tools need to process annotations at runtime.

  2. Can I combine multiple ElementType values in @Target?
    Yes, you can specify multiple element types in an array.

  3. Does @Inherited apply to methods?
    No, it only applies to class-level annotations.

  4. What happens if I don’t specify a retention policy?
    The default is CLASS, meaning the annotation won’t be available at runtime.

  5. Can I make a meta-annotation itself documented?
    Yes, meta-annotations can be annotated with @Documented.

  6. Why is @Documented important in API design?
    It ensures that developers see important annotations in Javadoc, improving usability.

  7. What’s the risk of leaving @Target unspecified?
    The annotation can be applied anywhere, leading to misuse.

  8. How do frameworks like Spring rely on @Retention?
    Spring uses RUNTIME retention to detect annotations like @Component during classpath scanning.

  9. Can I define my own meta-annotations?
    No, only Java provides standard meta-annotations, but you can create utility annotations that act like meta-markers.

  10. Does @Inherited work with interfaces?
    No, it only works with class inheritance, not interfaces.