Runtime vs Compile-time Annotations
TL;DR
- Annotations can be processed at two different phases of the software lifecycle: Compile-Time and Runtime.
- Compile-Time Annotations require
@Retention(RetentionPolicy.SOURCEorCLASS)and are processed by the compiler to generate code or emit warnings (e.g., Lombok,@Override). - Runtime Annotations require
@Retention(RetentionPolicy.RUNTIME)and are processed by the JVM using Reflection to alter program behavior dynamically (e.g., Spring@Autowired).
Concept
Compile-Time Annotations
These are evaluated by javac. They are incredibly fast because they have zero impact on the running application.
- Use Case: Generating boilerplate code (Lombok’s
@Data), performing strict type-checking (MapStruct’s@Mapper), or enforcing compiler rules (@Override,@Deprecated). - Pros: Zero runtime performance penalty. Catches errors before the app even starts.
- Cons: Very difficult to write (requires learning the complex
javax.annotation.processingAPI).
Runtime Annotations
These are evaluated while the application is actively running using Reflection.
- Use Case: Dependency Injection (Spring’s
@Service), Object-Relational Mapping (Hibernate’s@Entity), JSON serialization rules (Jackson’s@JsonProperty). - Pros: Extremely flexible. Very easy to write and process using basic Reflection APIs.
- Cons: Slows down application startup (scanning the classpath for annotations takes time). Errors are only discovered when the code actually executes.
Examples
// --- COMPILE-TIME ANNOTATION ---
// Processed by the compiler to verify the method actually overrides a parent.
// Does not exist at runtime.
@Override
public String toString() {
return "Example";
}
// Processed by Lombok's compiler plugin to generate getters and setters.
@Data
public class User {
private String name;
}
// --- RUNTIME ANNOTATION ---
// The compiler ignores this. At runtime, the Spring Framework scans
// the loaded classes, finds this annotation, and uses Reflection to instantiate it.
@RestController
public class ApiController {
// Spring uses Reflection to inject a database connection here
@Autowired
private Database db;
}
Interview Questions
Q: If you forget to add @Retention(RetentionPolicy.RUNTIME) to a custom runtime annotation, what happens?
A: The default retention policy in Java is CLASS. This means the compiler will successfully write the annotation into the .class file. However, when the JVM loads the class into memory at runtime, it will discard the annotation. When your Reflection code calls class.isAnnotationPresent(MyAnnotation.class), it will return false, and your logic will silently fail to execute.
Q: Why are modern microservice frameworks (like Quarkus and Micronaut) moving from Runtime to Compile-Time annotations?
A: Traditional frameworks like Spring use Runtime Reflection extensively. When a Spring app starts, it spends several seconds scanning the classpath and building reflection metadata. This slow startup time and high memory usage is terrible for Cloud-Native Serverless environments (like AWS Lambda).
Newer frameworks like Micronaut process all dependency injection annotations at Compile-Time. They generate raw, hardcoded Java wiring classes. As a result, the application starts in milliseconds and uses a fraction of the memory, because there is no Reflection happening at runtime!