Annotations are a powerful feature in Java and Spring Boot that allow you to add metadata to code, enabling dynamic behavior at runtime. While Spring provides a rich set of annotations like @Transactional, @RestController, and @Autowired, you can go a step further by creating custom annotations tailored to your application’s needs.

In this post, you’ll learn how to:

  • Create custom annotations
  • Combine annotations with Spring AOP
  • Apply them for logging, validation, security, and more
  • Handle annotation metadata using reflection
  • Keep code modular, clean, and DRY

Why Use Custom Annotations?

Custom annotations help:

  • Encapsulate cross-cutting logic (e.g., logging, auditing, timing)
  • Make code declarative and expressive
  • Reduce duplication
  • Improve readability and maintainability
  • Separate concerns using Spring AOP

Instead of repeating code, annotate it and let the framework do the work.


Step 1: Define a Custom Annotation

Let’s create an annotation called @LogExecutionTime to time method execution.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
  • @Target defines applicable locations (method, class, etc.)
  • @Retention(RUNTIME) ensures it’s available at runtime

Step 2: Handle Annotation with AOP

Spring AOP allows intercepting annotated methods using @Aspect.

@Aspect
@Component
public class LogExecutionTimeAspect {

    @Around("@annotation(com.example.annotations.LogExecutionTime)")
    public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long elapsed = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + " executed in " + elapsed + "ms");
        return result;
    }
}

Enable AOP in your main class:

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {}

Now, you can simply annotate methods:

@LogExecutionTime
public void processOrder() {
// logic
}

Step 3: Add Annotation Parameters

Add flexibility to your annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditAction {
String value();
}

Now extract the parameter in your aspect:

@Around("@annotation(auditAction)")
public Object audit(ProceedingJoinPoint pjp, AuditAction auditAction) throws Throwable {
System.out.println("Auditing action: " + auditAction.value());
return pjp.proceed();
}

Usage:

@AuditAction("DELETE_USER")
public void deleteUser(Long id) {
// deletion logic
}

Step 4: Using Annotations with Reflection

Sometimes, you might want to read annotations dynamically:

Method method = MyService.class.getMethod("deleteUser");
AuditAction annotation = method.getAnnotation(AuditAction.class);
System.out.println(annotation.value());

Reflection is useful for frameworks, validators, or dynamic behavior at runtime.


Example Use Cases for Custom Annotations

  • Security: @RequireAdmin to restrict controller methods
  • Caching: @AutoCache to cache method outputs
  • Rate Limiting: @Throttle(limit = 10)
  • Feature Flags: @FeatureToggle("beta-feature")
  • Validation: @ValidOrderStatus

Each annotation abstracts reusable logic into a declarative form, keeping the business logic clean.


Best Practices

  • Keep annotation behavior predictable and testable
  • Use @Retention(RUNTIME) if needed at runtime (Spring AOP, reflection)
  • Annotate on appropriate @Target types (method, field, class)
  • Combine with AOP or Spring Events for cross-cutting logic
  • Document your custom annotations well

Conclusion

Custom annotations in Spring Boot unlock a powerful pattern for modular, declarative, and reusable logic. Whether you’re logging, auditing, validating, or guarding endpoints, annotations offer a clean way to apply behavior without tangling your code.

Mastering custom annotations helps you design scalable, maintainable Spring applications — and even build your own mini-frameworks within your project.