Compile-Time Annotation Processing (APT) — How Lombok and AutoValue Work
Fast recall
Quick revision
Core answer
@SupportedAnnotationTypes and @SupportedSourceVersion, and register it via `META-INF/services/javax.annotation.processing.Processor`. The processor runs in rounds — each round may generate new source that triggers another round. Tools like Lombok (@Getter, @Setter, @Builder) use APT with AST modification (a slightly hacky approach that reaches into javac's internal Trees API). AutoValue and MapStruct use APT the 'clean' way — generating sibling source files like AutoValue_User.java. The advantage over runtime reflection: zero runtime cost — the generated code is compiled normally, executes at full JIT speed, and doesn't require setAccessible(true) or break the module system. The downside: it's compile-time only, so it can't adapt to runtime configuration.Whether you know APT runs during compilation (not runtime), can name tools that use it, and understand it generates code rather than reflecting on types at runtime
Confusing APT with runtime reflection; thinking Lombok uses reflection (it doesn't — it modifies the AST during compilation)
Distinguish clean APT (AutoValue generating sibling files) from Lombok's hack (AST modification via internal javac API); mention the incremental-compile limitations of APT
Interview-ready guide
Interview answer
APT — Annotation Processing Tool — runs during javac compilation. You implement an AbstractProcessor, declare which annotations you handle via @SupportedAnnotationTypes, and register via META-INF/services/javax.annotation.processing.Processor. The compiler runs your processor in rounds — each round can generate new source files, which triggers more rounds until no more generation happens.
The advantage over runtime reflection is zero runtime cost. The generated code compiles into normal bytecode — no reflection, no setAccessible, no module-opens, full JIT optimization. For GraalVM native image and serverless, this is critical — runtime reflection is expensive or impossible, but APT-generated code is just code.
Tools that use APT: Lombok for @Getter/@Setter/@Builder; AutoValue for immutable value classes; MapStruct for bean mappers; Dagger for compile-time DI; Micronaut and Quarkus for full application frameworks; Spring Boot's configuration processor for IDE autocomplete metadata.
Lombok is a special case worth calling out. AutoValue and MapStruct generate clean sibling source files — you see AutoValue_User.java appear in your generated sources. Lombok modifies the existing class's AST — it reaches into javac's internal com.sun.tools.javac.tree.JCTree API and injects getName, setName, etc. directly into your User class. This is why IDE support needs special plugins and why Lombok occasionally breaks on new Java releases — it uses unofficial APIs.
Modern trend: frameworks like Micronaut and Quarkus replaced Spring's runtime-reflection DI with compile-time APT DI. Startup drops from 5-10 seconds to under 100 milliseconds. Memory drops from 200MB to 50MB. For containerized and serverless workloads, that's a huge operational win.
When to write your own processor in application code: when the pattern is repetitive and mechanical — builders, mappers, typesafe configuration — and you care about zero runtime cost. For dynamic, config-driven behavior, runtime reflection via Spring AOP is simpler.
Compile-Time Annotation Processing (APT) — How Lombok and AutoValue Work
Direct Answer
Annotation Processing Tool (APT) runs during javac compilation — it reads annotations on Java source and generates new source files (or modifies the AST) before compilation completes. You implement an AbstractProcessor declaring @SupportedAnnotationTypes and @SupportedSourceVersion, and register it via META-INF/services/javax.annotation.processing.Processor. The processor runs in rounds — each round may generate new source that triggers another round. Tools like Lombok (@Getter, @Setter, @Builder) use APT with AST modification (a slightly hacky approach that reaches into javac's internal Trees API). AutoValue and MapStruct use APT the 'clean' way — generating sibling source files like AutoValue_User.java. The advantage over runtime reflection: zero runtime cost — the generated code is compiled normally, executes at full JIT speed, and doesn't require setAccessible(true) or break the module system. The downside: it's compile-time only, so it can't adapt to runtime configuration.
Common Mistakes & Pitfalls
Confusing APT with runtime reflection; thinking Lombok uses reflection (it doesn't — it modifies the AST during compilation)
Check your understanding
Answer these without looking back at the article.
- Why does Lombok use internal javac APIs while AutoValue doesn't?
- How does compile-time DI (Dagger, Micronaut) improve GraalVM native image performance?
- What's the difference between aggregating and isolating annotation processors in Gradle?
- How would you use JavaPoet to generate a builder class at compile time?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.