@Developer("Admin")class UserService {
}Creating Custom Annotations in Java: Meta-Annotations and Reflection Processing
Direct Answer & Core Concept
Custom annotations allow Java developers to create their own metadata that can be attached to classes, methods, fields, constructors, and other program elements. They are useful when an application needs to describe code in a structured way without changing the actual business logic.
Java frameworks such as Spring, JUnit, Jakarta EE, and many testing or dependency-injection tools use annotations heavily. Understanding how to create custom annotations is therefore an important step toward advanced Java development.
In this tutorial, you will learn how to create custom annotations, control where they can be used with meta-annotations, retain them at runtime, and read them using Java Reflection.
Detailed Explanation & Mechanics
What Is a Custom Annotation in Java?
A custom annotation is an annotation defined by the developer for a specific application or framework requirement.
Instead of repeatedly storing configuration in comments or separate files, annotations can place metadata directly next to the code they describe.
For example:
Here, @Developer can be a custom annotation created by the application.
The annotation itself does not automatically execute code. It simply provides metadata that another part of the program can read and process.
Why Use Custom Annotations?
Custom annotations are useful when you want to attach structured information to Java code.
Common use cases include:
Marking classes for framework processing
Identifying service or controller classes
Defining validation rules
Creating test metadata
Configuring logging behavior
Describing permissions
Registering application components
Controlling reflection-based processing
Building lightweight frameworks
For example:
@Role("ADMIN")class AdminService {
}A security component could inspect this annotation and determine that the class is associated with the ADMIN role.
Basic Syntax of a Custom Annotation
A custom annotation is declared using the @interface keyword.
@interface Developer {
String value();
}You can then use it like this:
@Developer("Admin")class UserService {
}The syntax may look similar to an interface, but an annotation declaration has a different purpose.
The @interface keyword tells Java that you are defining an annotation type.
Creating Your First Custom Annotation
Let's create a simple annotation called @Info.
@interface Info {
String author();
String version();
}We can use it like this:
@Info(author = "Admin", version = "1.0")class PaymentService {
}The annotation now stores two pieces of metadata:
authorversion
However, we have not yet specified where this annotation can be used or how long it should remain available.
That is where meta-annotations become important.
What Are Meta-Annotations?
Meta-annotations are annotations that provide information about another annotation.
Java provides several important meta-annotations:
| Meta-Annotation | Purpose |
|---|---|
@Target | Specifies where an annotation can be used |
@Retention | Specifies how long an annotation is retained |
@Documented | Includes the annotation in generated documentation |
@Inherited | Allows certain class annotations to be inherited |
@Repeatable | Allows an annotation to be used multiple times |
These annotations are commonly used when designing custom annotations.
The @Target Meta-Annotation
@Target controls which Java elements can use your custom annotation.
For example:
@Target(ElementType.TYPE)@interface ServiceInfo {
String name();
}This annotation can be applied to classes, interfaces, enums, and other type declarations.
Example:
@ServiceInfo(name = "Payment")class PaymentService {
}But applying it to a method would not be allowed:
class PaymentService {
@ServiceInfo(name = "process") void processPayment() {
}
}The compiler will reject this because the annotation was restricted to TYPE.
Common ElementType Values
Java provides several ElementType values.
ElementType.TYPEElementType.METHODElementType.FIELDElementType.CONSTRUCTORElementType.PARAMETERElementType.LOCAL_VARIABLEElementType.PACKAGEElementType.MODULEElementType.ANNOTATION_TYPEElementType.TYPE_USEFor example, a method-only annotation can be defined as:
@Target(ElementType.METHOD)@interface LogExecution {
}Then:
class OrderService {
@LogExecution void createOrder() {
System.out.println("Order created");
}
}Allowing an Annotation on Multiple Elements
You can specify multiple targets using an array.
@Target( {
ElementType.TYPE, ElementType.METHOD, ElementType.FIELD
})@interface Auditable {
}Now the annotation can be used on classes, methods, and fields.
Example:
@Auditableclass User {
@Auditable private String name;
@Auditable void save() {
}
}The @Retention Meta-Annotation
@Retention determines how long an annotation is retained.
The three retention policies are:
RetentionPolicy.SOURCERetentionPolicy.CLASSRetentionPolicy.RUNTIMEThey have different purposes.
RetentionPolicy.SOURCE
SOURCE means the annotation is available only in source code.
@Retention(RetentionPolicy.SOURCE)@interface DeveloperNote {
}The annotation is discarded by the compiler and is not available in the compiled class at runtime.
This type of annotation is useful for tools that process source code.
RetentionPolicy.CLASS
CLASS means the annotation is stored in the compiled .class file but is generally not available through normal runtime reflection.
Example:
@Retention(RetentionPolicy.CLASS)@interface BuildInfo {
}CLASS is the default retention policy if @Retention is not specified.
RetentionPolicy.RUNTIME
RUNTIME means the annotation remains available at runtime.
@Retention(RetentionPolicy.RUNTIME)@interface Author {
String name();
}This is especially important when you want to process the annotation using Reflection.
For example:
@Author(name = "Admin")class ReportService {
}A reflection API can inspect ReportService and read the @Author annotation.
@Retention(RUNTIME) and Reflection
If your application needs to inspect a custom annotation while the program is running, use:
@Retention(RetentionPolicy.RUNTIME)For example:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface ComponentInfo {
String name();
}Now:
@ComponentInfo(name = "PaymentService")class PaymentService {
}The annotation can be discovered using Reflection.
Reading a Custom Annotation Using Reflection
Java Reflection allows a program to inspect classes, methods, fields, constructors, and annotations at runtime.
Consider this annotation:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface ComponentInfo {
String name();
}Apply it to a class:
@ComponentInfo(name = "PaymentService")class PaymentService {
}Now inspect it:
public class Main {
public static void main(String[] args) {
Class<PaymentService> clazz = PaymentService.class;
if (clazz.isAnnotationPresent(ComponentInfo.class)) {
ComponentInfo info = clazz.getAnnotation(ComponentInfo.class);
System.out.println(info.name());
}
}
}Output:
PaymentServiceThe basic process is:
Class↓Check annotation↓Get annotation↓Read annotation values↓Process metadataUsing getAnnotation()
The getAnnotation() method retrieves a specific annotation.
Example:
ComponentInfo info = PaymentService.class.getAnnotation(ComponentInfo.class);You can then read its values:
System.out.println(info.name());This works because the annotation uses:
@Retention(RetentionPolicy.RUNTIME)Without runtime retention, normal reflection cannot retrieve the annotation at runtime.
Using isAnnotationPresent()
Before retrieving an annotation, you can check whether it exists.
Class<?> clazz = PaymentService.class;
if (clazz.isAnnotationPresent(ComponentInfo.class)) {
System.out.println("Annotation found");
}This is useful when the annotation is optional.
Custom Annotation with a Default Value
Annotation elements can have default values.
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface ComponentInfo {
String name();
String version() default "1.0";
}Now you can write:
@ComponentInfo(name = "PaymentService")class PaymentService {
}The version automatically becomes:
1.0You can also override it:
@ComponentInfo( name = "PaymentService", version = "2.0")class PaymentService {
}The Special value() Element
An annotation can have an element named value.
@interface Developer {
String value();
}This allows a shorter syntax:
@Developer("Admin")class UserService {
}Instead of:
@Developer(value = "Admin")class UserService {
}The shorter form works when value is the element being supplied and the annotation syntax permits that shorthand.
Creating a Method Annotation
Custom annotations can also describe methods.
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface LogExecution {
}Use it:
class UserService {
@LogExecution public void createUser() {
System.out.println("Creating user");
} public void deleteUser() {
System.out.println("Deleting user");
}
}Now a reflection-based processor can find methods marked with @LogExecution.
Processing Method Annotations with Reflection
Example:
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
Method[] methods = UserService.class.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(LogExecution.class)) {
System.out.println( "Logging method: " + method.getName());
}
}
}
}Possible output:
Logging method: createUserThis is a simple example of annotation processing at runtime.
Creating an Annotation with Multiple Properties
A custom annotation can contain several elements.
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface ApiEndpoint {
String path();
String method() default "GET";
boolean authenticated() default true;
}Use it:
class UserController {
@ApiEndpoint( path = "/users", method = "GET", authenticated = true) public void getUsers() {
}
}Reflection can then read all these properties.
ApiEndpoint endpoint = method.getAnnotation(ApiEndpoint.class);
System.out.println(endpoint.path());
System.out.println(endpoint.method());
System.out.println(endpoint.authenticated());This pattern is similar to how frameworks store configuration metadata.
@Documented Meta-Annotation
@Documented indicates that an annotation should be included in generated Java documentation when applicable.
Example:
import java.lang.annotation.Documented;
@Documented@interface ApiInfo {
String value();
}This is useful for annotations that are part of a public API and should appear in generated documentation.
@Inherited Meta-Annotation
@Inherited affects class-level annotations.
Example:
@Inherited@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface Feature {
String value();
}Now:
@Feature("Payment")class PaymentService {
}class OnlinePaymentService extends PaymentService {
}When checking the subclass for the inherited class annotation:
Feature feature = OnlinePaymentService.class.getAnnotation(Feature.class);the annotation can be inherited from the superclass.
Important: @Inherited applies to class inheritance. It does not make annotations on methods or fields automatically inherited.
@Repeatable Meta-Annotation
Sometimes the same annotation needs to appear multiple times on one element.
For example:
@Role("ADMIN")@Role("MANAGER")class UserService {
}To support this, the annotation can be declared as repeatable.
First create a container annotation:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface Roles {
Role[] value();
}Then define the repeatable annotation:
import java.lang.annotation.Repeatable;
@Repeatable(Roles.class)@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface Role {
String value();
}Now it can be used multiple times:
@Role("ADMIN")@Role("MANAGER")class UserService {
}Reading Repeatable Annotations
You can retrieve all repeated annotations using:
Role[] roles = UserService.class.getAnnotationsByType(Role.class);Then:
for (Role role : roles) {
System.out.println(role.value());
}Output:
ADMINMANAGERThis is useful when an element can have multiple independent metadata entries.
Complete Custom Annotation Example
Let's combine the important concepts into one practical example.
Step 1: Create the Annotation
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@interface ServiceInfo {
String name();
String version() default "1.0";
}Step 2: Apply the Annotation
@ServiceInfo( name = "PaymentService", version = "2.1")class PaymentService {
public void processPayment() {
System.out.println("Payment processed");
}
}Step 3: Read the Annotation
public class Main {
public static void main(String[] args) {
Class<PaymentService> clazz = PaymentService.class;
if (clazz.isAnnotationPresent(ServiceInfo.class)) {
ServiceInfo info = clazz.getAnnotation(ServiceInfo.class);
System.out.println("Service: " + info.name());
System.out.println("Version: " + info.version());
}
}
}Output:
Service: PaymentServiceVersion: 2.1This demonstrates the complete annotation lifecycle:
Define↓Apply↓Compile↓Retain↓Discover with Reflection↓Read values↓Process metadataBuilding a Simple Annotation Processor
A practical application can scan a class and automatically perform an action when it finds a specific annotation.
Consider:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface Execute {
}Create a service:
class TaskService {
@Execute public void taskOne() {
System.out.println("Task One");
} public void taskTwo() {
System.out.println("Task Two");
}
}Now create a processor:
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void process(Object object) throws Exception {
Method[] methods = object.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Execute.class)) {
System.out.println( "Executing: " + method.getName());
method.invoke(object);
}
}
}
}Run it:
public class Main {
public static void main(String[] args) throws Exception {
TaskService service = new TaskService();
AnnotationProcessor.process(service);
}
}Output:
Executing: taskOneTask OneThis shows how annotations can act as instructions for a reflection-based framework.
Reflection Methods Commonly Used with Annotations
Several Reflection methods are useful when processing annotations.
| Method | Purpose |
|---|---|
isAnnotationPresent() | Checks whether an annotation exists |
getAnnotation() | Gets one specific annotation |
getAnnotations() | Gets annotations visible through the relevant API |
getDeclaredAnnotations() | Gets annotations directly declared on the element |
getAnnotationsByType() | Gets repeated annotations of a type |
getDeclaredAnnotationsByType() | Gets directly declared repeated annotations |
For example:
if (method.isAnnotationPresent(LogExecution.class)) {
LogExecution annotation = method.getAnnotation(LogExecution.class);
}getAnnotations() vs getDeclaredAnnotations()
These methods can behave differently when inheritance is involved.
getAnnotations()returns annotations that are visible according to the Reflection API's inheritance rules.
getDeclaredAnnotations()returns annotations directly declared on the element.
This distinction becomes particularly important when working with class-level annotations and @Inherited.
Annotation Processing vs Reflection
These two concepts are related but not identical.
Reflection
Reflection inspects classes and annotations at runtime.
Example:
method.getAnnotation(LogExecution.class);Annotation Processing
Annotation processing generally refers to processing annotations during compilation using Java's annotation-processing APIs.
A compile-time processor can inspect source-level or compiled program elements and generate code, validate declarations, or produce other build-time output.
For example, a custom processor can generate classes based on annotations.
So:
Reflection → RuntimeAnnotation Processing → Commonly Compile TimeDo not assume every annotation processor uses Reflection. Compile-time annotation processing operates through Java's language/model APIs rather than ordinary runtime reflection.
When Should You Use Custom Annotations?
Custom annotations are especially useful when metadata needs to stay close to the code it describes.
Good use cases include:
Framework configuration
API metadata
Validation
Logging markers
Permission declarations
Test metadata
Serialization configuration
Dependency injection
Code generation
Plugin registration
For simple configuration, however, an annotation is not always necessary. A configuration file, normal Java object, or explicit method call may sometimes be easier to understand.
Common Mistakes with Custom Annotations
1. Forgetting Runtime Retention
This is one of the most common mistakes.
@interface MyAnnotation {
}If you expect Reflection to find it at runtime, define:
@Retention(RetentionPolicy.RUNTIME)2. Using the Wrong @Target
If an annotation should only be applied to methods:
@Target(ElementType.METHOD)Do not leave the target unrestricted unless that behavior is actually desired.
3. Assuming Annotations Execute Code
This does not automatically execute anything:
@Executevoid process() {
}The annotation is metadata.
Some other component must read the annotation and decide what action to perform.
4. Confusing @Inherited with Method Inheritance
@Inherited applies to class annotations.
It does not automatically copy annotations from overridden methods into subclasses.
5. Using Reflection Everywhere
Reflection is powerful, but it can add complexity and runtime overhead.
For large systems, consider whether compile-time annotation processing or framework-supported mechanisms would be more appropriate.
Best Practices for Custom Annotations
Choose Clear Names
Prefer:
@Auditable@Cacheable@ApiEndpointover vague names such as:
@Info@Data@Configwhen the purpose can be made more specific.
Define a Narrow Target
Use @Target to prevent accidental usage.
Choose Retention Carefully
Use RUNTIME only when runtime inspection is required.
Provide Sensible Defaults
Defaults can make annotations easier to use:
String version() default "1.0";Keep Annotations Focused
An annotation should describe metadata rather than becoming a replacement for normal application logic.
Custom Annotations in Real Java Frameworks
Custom annotations are a foundation for many Java frameworks.
For example, frameworks may use annotations to identify:
Classes↓Methods↓Fields↓Configuration↓Framework processingA framework can scan application classes, detect annotations, read their attributes, and register or configure components automatically.
This is one reason annotations are so important in modern Java development.
Custom Annotation Example for a REST API
Consider:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface GetRoute {
String path();
}Use it:
class UserController {
@GetRoute(path = "/users") public void users() {
System.out.println("Users endpoint");
}
}A framework could scan controller methods and build a route table:
/users → UserController.users()The annotation itself does not create the route. The framework's processing logic does that.
Custom Annotation Example for Authorization
You can define:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface RequiresRole {
String value();
}Use it:
class AdminController {
@RequiresRole("ADMIN") public void deleteUser() {
System.out.println("User deleted");
}
}A security component could inspect the annotation before invoking the method.
Conceptually:
Request↓Find method↓Read @RequiresRole↓Check current user's role
↓
Allow or reject
↓
Execute methodAgain, the annotation describes the requirement; the security mechanism enforces it.
Custom Annotations and Java Reflection: Key Relationship
The most important relationship to remember is:
Custom Annotation↓@Retention(RUNTIME)↓Compiled Class↓Reflection↓Annotation discovered↓Values read↓Application logicIf runtime reflection is part of your design, RetentionPolicy.RUNTIME is usually essential.
Interview Questions
What is a custom annotation in Java?
A custom annotation is a developer-defined annotation used to attach structured metadata to Java program elements.
How do you create a custom annotation?
Use the @interface syntax:
@interface MyAnnotation {
}What does @Target do?
@Target specifies which Java program elements can use an annotation.
What does @Retention do?
@Retention specifies how long the annotation is retained: source code, class files, or runtime.
Which retention policy is needed for runtime reflection?
RetentionPolicy.RUNTIMEWhat does @Inherited do?
@Inherited allows certain class-level annotations to be inherited by subclasses when accessed through the relevant reflection APIs.
Can annotations contain methods?
Annotation elements look like method declarations:
String name();They define metadata values rather than normal executable methods.
Can an annotation have default values?
Yes.
String version() default "1.0";What is @Repeatable?
@Repeatable allows the same annotation type to be applied multiple times to the same declaration.
Do annotations execute code automatically?
No. Annotations provide metadata. Another mechanism, such as reflection-based code, a framework, or an annotation processor, must interpret that metadata.
Practice Example
Try creating your own @DeveloperInfo annotation.
Requirements:
Annotation name: DeveloperInfoTarget: TYPERetention: RUNTIMEProperties: name experience projectThen apply it:
@DeveloperInfo( name = "Admin", experience = 12, project = "Payment System")class Developer {
}Finally, use Reflection to print the annotation values.
This small exercise combines the three most important concepts:
Custom Annotation+Meta-Annotations+ReflectionSuggested Image / Graphic Ideas
Graphic 1 — Custom Annotation Lifecycle
Define Annotation↓Apply Annotation↓@Target / @Retention↓Compile↓Reflection↓Read Metadata↓Application LogicGraphic 2 — Java Meta-Annotations
Create a visual card showing:
@Target → Where?@Retention → How long?@Documented → Documentation?@Inherited → Class inheritance?@Repeatable → Multiple uses?Graphic 3 — Reflection Processing
@ServiceInfo↓PaymentService.class↓getAnnotation()↓ServiceInfo object↓name() / version()↓Framework LogicThese graphics can make the tutorial easier for beginners to understand and provide useful visual content for the page.
Runnable Code Example: Creating Custom Annotations in Java
Java 17+import java.lang.annotation.*;
import java.lang.reflect.Method;
//
1. Define Custom Annotation@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface AuditLog {
String action() default "EXECUTE";
}//
2. Apply Custom Annotationclass PaymentService {
@AuditLog(action = "CHARGE_CREDIT_CARD") public void charge(double amount) {
System.out.println("Processing payment: $" + amount);
}
}//
3. Process via Reflectionpublic class CustomAnnotationDemo {
public static void main(String[] args) throws Exception {
Method method = PaymentService.class.getMethod("charge", double.class);
if (method.isAnnotationPresent(AuditLog.class)) {
AuditLog audit = method.getAnnotation(AuditLog.class);
System.out.println("Audit Action Intercepted: " + audit.action());
}
}
}