Back to Annotations
Q2Question 2 of 3

How do you create a custom annotation?

Key Concepts
  • Define with @interface keyword: public @interface Audited { String action(); }
  • Elements look like methods but act as attributes: String action() default "";
  • Apply meta-annotations: @Target (where), @Retention (when), @Documented
  • RUNTIME retention needed for reflection-based processing (Spring, custom interceptors)
  • Use value() as the element name for single-element annotations: @Audited("TRANSFER")

Definition

Define with @interface, add elements as method-like declarations, apply @Target and @Retention meta-annotations, and process at runtime via reflection or at compile time via annotation processors.

Creating a custom annotation:

  • @interface keyword declares it.
  • Elements are declared like methods: String action() default "";
  • @Target restricts where it can be used (METHOD, FIELD, TYPE).
  • @Retention(RUNTIME) makes it available for reflection.
  • Default values make elements optional.

Creating a custom stamp for bank audit forms. You define what information the stamp carries (action, timestamp), where it can be applied (on methods or classes), and whether it survives into the final document (retention).

Understanding the Concepts

How do you define a custom annotation?

You define a custom annotation using the @interface keyword. Each element inside the annotation looks like a method declaration but acts as an attribute. You control where the annotation can be used with @Target and how long it survives with @Retention.

JAVA
package com.bank.audit;

import java.lang.annotation.*;

@Target(ElementType.METHOD)            // can only be on methods
@Retention(RetentionPolicy.RUNTIME)    // available at runtime via reflection
@Documented                            // included in javadoc
public @interface Audited {
    String action() default "";         // optional element with default
    String description() default "";
    boolean sensitive() default false;  // marks PII-handling operations
}

The default keyword makes an element optional. If you omit it, the caller must always supply a value.

How do you use it?

Apply the annotation on any method and supply values for the elements. Elements with defaults can be omitted.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import com.bank.audit.Audited;

public class TransferService {

    @Audited(action = "TRANSFER", description = "Debit and credit between accounts")
    public void transfer(Account from, Account to, BigDecimal amount) {
        from.debit(amount);
        to.credit(amount);
    }

    @Audited(action = "CLOSE_ACCOUNT", sensitive = true)
    public void closeAccount(Account account) {
        account.setStatus("CLOSED");
    }
}

How do you read it at runtime?

Use the Java Reflection API to discover annotations on classes, methods, or fields. This is how Spring, JPA, and custom interceptors process annotations at runtime.

JAVA
package com.bank.audit;

import java.lang.reflect.Method;
import java.math.BigDecimal;

public class AuditLogger {
    public static void logAuditedMethods(Class<?> clazz) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Audited.class)) {
                Audited audit = method.getAnnotation(Audited.class);
                System.out.println("Method: " + method.getName());
                System.out.println("  Action: " + audit.action());
                System.out.println("  Desc: " + audit.description());
                System.out.println("  Sensitive: " + audit.sensitive());
            }
        }
    }

    public static void main(String[] args) {
        logAuditedMethods(TransferService.class);
    }
}
TEXT
Method: transfer
  Action: TRANSFER
  Desc: Debit and credit between accounts
  Sensitive: false
Method: closeAccount
  Action: CLOSE_ACCOUNT
  Desc:
  Sensitive: true

What is the value() shortcut for single-element annotations?

If you name the element value, callers can omit the element name when only that one element is supplied.

JAVA
public @interface RoleRequired {
    String value();  // named "value" for the shortcut
}

// Usage: no need to write value = "ADMIN"
@RoleRequired("ADMIN")
public void approveLoan(Loan loan) { }

This convention is used throughout Java: @SuppressWarnings("unchecked") works because the element is named value.

What element types are allowed?

Annotation elements can only return: primitives, String, Class, enums, other annotations, or arrays of these. You cannot use arbitrary objects like BigDecimal or List.

JAVA
public @interface Config {
    int maxRetries() default 3;                 // primitive
    String environment() default "PRODUCTION";  // String
    Class<?> handler() default Object.class;    // Class
    AccountType type() default AccountType.SAVINGS;  // enum
    Audited[] audits() default {};              // array of annotations
}

What are common mistakes when creating custom annotations?

The most common mistake is using the wrong retention. If you set SOURCE retention but try to read the annotation via reflection at runtime, it will not be there.

JAVA
// WRONG: SOURCE retention – annotation stripped before runtime
@Retention(RetentionPolicy.SOURCE)
public @interface TransactionLog { String action(); }

// At runtime: method.getAnnotation(TransactionLog.class) returns null!

// CORRECT: RUNTIME retention – annotation survives to runtime
@Retention(RetentionPolicy.RUNTIME)
public @interface TransactionLog { String action(); }

Another mistake is forgetting @Target, which allows the annotation on any element. This compiles but leads to confusing usage when developers place the annotation on fields, parameters, or classes where it makes no sense.

What should a senior engineer know?

  • Spring AOP uses custom annotations for cross-cutting concerns: @Cacheable, @Retry, @RateLimited. Spring intercepts calls to annotated methods via CGLIB proxies.
  • Lombok processes annotations at compile time to generate code (getters, builders) without reflection overhead at runtime.
  • AnnotationProcessor (JSR 269) lets you generate source files during compilation. Used by MapStruct, Dagger, and AutoValue.
  • @Repeatable (Java 8+) allows multiple of the same annotation: @Role("ADMIN") @Role("AUDITOR"). It requires a container annotation that holds an array of the repeated type.
  • @Inherited on a type annotation means subclasses inherit it. Without @Inherited, only the class that directly bears the annotation has it.

Visual Overview

TEXT
  1. Define:   @interface Audited { String action(); }
  2. Meta:     @Target(METHOD) @Retention(RUNTIME)
  3. Apply:    @Audited(action = "TRANSFER")
  4. Read:     method.getAnnotation(Audited.class).action()

  Element types: primitive, String, Class, enum, annotation, arrays of these
  Processing: runtime (reflection) or compile-time (annotation processor)

Key Takeaway

Create a custom annotation with @interface, add elements as method-like declarations with optional defaults, and apply @Target and @Retention meta-annotations. Use RUNTIME retention when frameworks or your own code need to read the annotation via reflection.

One sentence to remember: @interface defines it, @Target restricts where, @Retention controls when it survives, and reflection reads it at runtime.

---

Share
Use ← → arrow keys to navigate