skip to content

Compare compile-time annotation processing with bytecode manipulation (e.g. Lombok) and runtime reflection. When would you choose each, and what are the tradeoffs?

level: principalimportance: nice to knowfreq 30%

answer

  1. APT = compile-time, generate new files, type-safe, AOT-friendly
  2. Lombok = mutates your class via unsupported javac internals (fragile, 'magic')
  3. Reflection/ByteBuddy = runtime, dynamic but slower + errors deferred + AOT-hostile
  4. Choose generation for static boilerplate; reflection only when truly dynamic
  5. Micronaut/Quarkus/Spring-AOT shift work to compile time for native image

basics

~20 s

Annotation processing generates new code at compile time, so it's type-safe and adds no runtime cost. Lombok rewrites your existing classes by hacking the compiler's internals. Reflection inspects code at runtime, which is flexible but slower and less safe. Pick generation for boilerplate, reflection only when behavior must be dynamic.

solid answer

~50 s

There are three strategies for metadata-driven code. Standard annotation processing (APT) runs during compilation and can only emit new files; Dagger, MapStruct, and AutoValue use it to produce reflection-free, fully type-checked code with zero runtime startup penalty, at the cost of generated artifacts and a build step. Bytecode/AST manipulation — Lombok's approach — actually mutates your existing classes (adding getters, etc.) by reaching into javac's internal, unsupported AST APIs; it's ergonomically magical but fragile across JDK versions, invisible to plain readers, and dependent on IDE plugins. Runtime reflection (or runtime bytecode via ASM/ByteBuddy, as in Spring/Hibernate proxies) defers everything to runtime: maximally dynamic and decoupled from the build, but slower, error-deferred-to-runtime, and a poor fit for ahead-of-time/GraalVM native images. Rule of thumb: prefer APT generation for static boilerplate (safe, fast, AOT-friendly); use reflection/runtime bytecode only when the set of types or behavior genuinely isn't known until runtime; treat Lombok-style AST hacking as a convenience tradeoff you accept with eyes open.

go deeper

for a junior

Knows generation happens at compile time and reflection at runtime, and that one is faster.

for a middle

Contrasts the three approaches and can name representative tools (Dagger/MapStruct vs Lombok vs Spring reflection).

for a senior

Articulates concrete tradeoffs (safety, performance, fragility, inspectability) and picks appropriately per use case.

for a principal

Reasons about AOT/native-image strategy, the industry shift from runtime reflection to build-time processing, toolchain-stability risk of internal-API hacks, and long-term maintainability at scale.

## The three approaches, defined All three react to annotations/metadata, but at different times and with different powers. ### 1. Compile-time annotation processing (APT) Runs inside javac. **Can only generate new files**; cannot edit your source. Output is ordinary `.java` you (and tools) can read, compiled and type-checked like anything else. Examples: **Dagger** (DI graph), **MapStruct** (bean mappers), **AutoValue/Immutables** (value types). - **Pros**: zero runtime reflection cost; errors caught **at compile time**; generated code is inspectable; works with **ahead-of-time compilation / GraalVM native image** (no runtime metadata needed); no runtime dependency on the tool. - **Cons**: adds a build step and generated artifacts; can slow incremental builds if processors aren't well-behaved; only additive (you write `Foo` and use `Foo_Generated`); learning curve. ### 2. Bytecode / AST manipulation (Lombok-style, compile-time) Lombok registers as a processor but then **mutates the AST of your existing classes** through javac's *internal* (`com.sun.tools.javac.*`) APIs — so `@Getter` really adds `getX()` to *your* class, something standard APT forbids. - **Pros**: extremely terse source; no separate generated class to reference; feels seamless. - **Cons**: relies on **unsupported, version-specific** compiler internals (each JDK bump risks breakage); the methods exist in bytecode but **not in the source you read**, so the code is 'magic'; needs **IDE plugins** to not show errors; harder to debug; some consider it a maintainability liability. There is also runtime-side bytecode manipulation (next) — Lombok specifically is compile-time AST mutation. ### 3. Runtime reflection (and runtime bytecode generation) **Reflection** (`java.lang.reflect`) inspects and invokes code **at runtime** using loaded `Class` objects. Frameworks like Spring and Hibernate read annotations and wire/proxy at startup, often combined with **runtime bytecode generation** (ASM, ByteBuddy, CGLIB) to synthesize proxy classes on the fly. - **Pros**: maximally **dynamic** — can handle types/plugins discovered at runtime; decoupled from the build; no generated source to manage. - **Cons**: **runtime performance** cost (reflection is slower; startup scanning adds latency); errors appear **at runtime**, not compile time; type safety is weaker (lots of casting); **hostile to AOT/GraalVM native image**, which can't see runtime-reflective behavior without extra configuration; security-manager/module-system access concerns. ## Decision framework - **Is the work pure, known-at-compile-time boilerplate** (value classes, mappers, DI wiring of a fixed graph)? → **APT generation**. Best safety/performance, AOT-friendly. - **Do you value extreme source brevity over compiler-API stability and explicitness, on a controlled toolchain?** → Lombok is an option, accepting fragility and the 'invisible code' tradeoff. - **Is the set of types or the behavior genuinely unknown until runtime** (plugin systems, generic serialization of arbitrary objects, dynamic proxies)? → **reflection / runtime bytecode**, accepting the perf and AOT costs (or pairing with build-time hints for native image). ## The modern AOT angle The industry trend (GraalVM native image, Spring AOT, Micronaut, Quarkus) is shifting work **from runtime reflection to compile time** precisely to get fast startup and small native images. Micronaut and Quarkus lean heavily on annotation processing / build-time analysis instead of runtime reflection for exactly this reason. So a principal-level answer notes that APT/build-time generation is increasingly favored where reflection used to dominate. ## Summary table (mental) - **When it runs**: APT/Lombok = compile time; reflection = runtime. - **What it touches**: APT = new files; Lombok = your existing class (via internals); reflection = loaded classes at runtime. - **Safety**: APT highest (compile-checked); reflection lowest (runtime errors). - **Performance**: APT/Lombok best at runtime; reflection costs at runtime. - **AOT/native**: APT great; reflection problematic; Lombok fine at runtime but compiler-fragile.

  • Why is annotation processing friendlier to GraalVM native image than reflection?
    Native image performs closed-world AOT compilation and can't see runtime-reflective behavior without explicit hints. APT generates concrete code at build time, so there's nothing to discover at runtime — it 'just works' in the native image.
  • What's the core risk of Lombok's approach?
    It depends on internal, unsupported javac AST APIs that can change between JDK versions, so a JDK upgrade can break it; it also makes methods that exist in bytecode but not in readable source, requiring IDE plugin support.
  • When is runtime reflection still the right choice over compile-time generation?
    When the relevant types or behavior aren't known until runtime — generic serializers over arbitrary objects, plugin/extension systems, dynamic proxies — where you genuinely cannot generate the code ahead of time.

saying these in an interview costs you the question

  • Calling Lombok 'just a normal annotation processor' (it mutates existing source via internals)
  • Claiming reflection is free or as fast as generated code
  • Saying APT works at runtime
  • Ignoring GraalVM/AOT implications when comparing approaches
  • Treating the three as interchangeable rather than time-of-execution tradeoffs

context