Sitemap
Dhruval Vaishnav

Dhruval Vaishnav

Backend Engineer | Java & Spring Boot writer ☕ | Making complex tech simple 🚀 | Follow for weekly software engineering content 👩🏻‍💻

How to Create Custom Annotations in Spring Boot (And Why You Should)

3 min readMar 25, 2026

--

You use @Service, @RestController, @Transactional every day.

But have you ever wondered how to create your own annotations that do real work?

Custom annotations can eliminate boilerplate, enforce consistency, and make your codebase dramatically cleaner. Here’s how to build them from scratch.

The Real-World Analogy 🏷️

Think of annotations like labels on packages in a warehouse.

A label saying FRAGILE doesn't do anything by itself — but the warehouse staff (Spring's processing engine) sees it and handles that package differently.

Your custom annotation is the label. Spring AOP or reflection is the warehouse staff acting on it.

Why Custom Annotations?

Without custom annotation — repeated boilerplate:

@GetMapping("/orders")
public List<Order> getOrders() {
long start = System.currentTimeMillis();
log.info("Method getOrders started");
try {
return orderService.findAll();
} finally {
log.info("Method getOrders took {}ms", System.currentTimeMillis() - start);
}
}

@GetMapping("/users")
public List<User> getUsers() {
long start = System.currentTimeMillis();
log.info("Method getUsers started");
try {
return userService.findAll();
} finally {
log.info("Method getUsers took {}ms", System.currentTimeMillis() - start);
}
}

With custom annotation — clean and reusable:

@GetMapping("/orders")
@LogExecutionTime
public List<Order> getOrders() {
return orderService.findAll();
}

@GetMapping("/users")
@LogExecutionTime
public List<User> getUsers() {
return userService.findAll();
}

Step 1 — Create the Annotation

@Target(ElementType.METHOD)          // Can be applied to methods
@Retention(RetentionPolicy.RUNTIME) // Available at runtime
public @interface LogExecutionTime {
// No elements needed for this annotation
}

Key meta-annotations:

Get Dhruval Vaishnav’s stories in your inbox

Join Medium for free to get updates from this writer.

Meta-annotation Options @Target METHOD, TYPE, FIELD, PARAMETER @Retention RUNTIME (most common), CLASS, SOURCE

Step 2 — Create the Aspect (The Logic)

@Aspect
@Component
public class LogExecutionTimeAspect {

private static final Logger log = LoggerFactory.getLogger(LogExecutionTimeAspect.class);

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();

log.info("▶ Starting: {}", methodName);

Object result = joinPoint.proceed(); // execute the actual method
long duration = System.currentTimeMillis() - start;
log.info("✅ {} completed in {}ms", methodName, duration);
return result;
}
}

Step 3 — Add AOP Dependency

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Real-World Example — Custom @RateLimit Annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int requestsPerMinute() default 60;
}
@Aspect
@Component
public class RateLimitAspect {

private Map<String, List<Long>> requestLog = new ConcurrentHashMap<>();

@Around("@annotation(rateLimit)")
public Object checkRateLimit(ProceedingJoinPoint joinPoint,
RateLimit rateLimit) throws Throwable {

String methodName = joinPoint.getSignature().toShortString();
int limit = rateLimit.requestsPerMinute();

List<Long> timestamps = requestLog.computeIfAbsent(methodName,
k -> new ArrayList<>());

long oneMinuteAgo = System.currentTimeMillis() - 60_000;
timestamps.removeIf(t -> t < oneMinuteAgo);

if (timestamps.size() >= limit) {
throw new RuntimeException("Rate limit exceeded for: " + methodName);
}

timestamps.add(System.currentTimeMillis());
return joinPoint.proceed();
}
}

Usage:

@GetMapping("/search")
@RateLimit(requestsPerMinute = 10)
public List<Product> search(@RequestParam String query) {
return productService.search(query);
}

Annotation With Parameters

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int attempts() default 3;
long delayMs() default 1000;
}

@Retry(attempts = 5, delayMs = 500)
public void callExternalApi() {
// If this throws, retry up to 5 times with 500ms delay
}

🎯 Interview Tips

Q: What is AOP and how does it relate to annotations?

AOP (Aspect-Oriented Programming) separates cross-cutting concerns (logging, security, transactions) from business logic.
Custom annotations mark where these concerns apply, and Aspects define what to do when they’re found.

Q: What is @Around vs @Before vs @After?

@Before runs before the method.
@After runs after.
@Around wraps the method — you control when (or whether) the original method executes using joinPoint.proceed().

Q: How does @Transactional work internally?

It’s a Spring annotation processed by an AOP Aspect. When Spring sees @Transactional, it wraps your method in a proxy that begins a transaction before and commits/rolls back after.

Key Takeaways

  • Custom annotations are created with @interface
  • @Target controls where the annotation can be applied
  • @Retention(RUNTIME) makes it available for Spring to process
  • AOP Aspects define the actual logic that runs when your annotation is encountered
  • This pattern powers @Transactional, @Cacheable, @Async — all Spring's magic annotations

Follow me for daily Java & Spring Boot deep dives. 🚀

#Java #SpringBoot #AOP #Annotations #Backend

--

--

Dhruval Vaishnav
Dhruval Vaishnav

Backend Engineer | Java & Spring Boot writer ☕ | Making complex tech simple 🚀 | Follow for weekly software engineering content 👩🏻‍💻