Annotation processing is most useful when it moves mechanical, rule-based work to compile time without hiding the important parts of the system.
That is the line to keep in mind. Good code generation removes repetition, enforces conventions, and improves runtime behavior. Bad code generation creates a second codebase that nobody understands when something breaks.
The Best Use Cases Are Boring on Purpose
Annotation processing works well for patterns that are:
- repetitive
- deterministic
- low-ambiguity
- easier to validate at compile time than at runtime
Good examples:
- mappers and adapters
- generated registries or metadata indexes
- client stubs
- compile-time validation of framework annotations
Weak examples:
- policy-heavy business rules
- code whose behavior changes based on lots of implicit context
- generation that is hard to reason about without reading the processor internals
If the generated code becomes more mysterious than the handwritten code it replaced, the trade-off is usually bad.
A Minimal Processor Shape
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AutoFactory {}
@SupportedSourceVersion(SourceVersion.RELEASE_21)
@SupportedAnnotationTypes("com.example.AutoFactory")
public final class AutoFactoryProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(AutoFactory.class)) {
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"@AutoFactory can only target classes",
element
);
continue;
}
generateFactory((TypeElement) element);
}
return true;
}
private void generateFactory(TypeElement type) {
// Use Filer to create deterministic generated source.
}
}
This is enough to show the two qualities that matter most:
- actionable diagnostics
- deterministic output
Determinism Is Not a Nice-to-Have
Generated code should be stable across machines, clean builds, and CI runs.
That means:
- sort members and inputs before generation
- avoid timestamps and random IDs in output
- keep formatting stable
- make naming conventions predictable
If generated files churn for no semantic reason, code review quality drops quickly and teams stop trusting the tool.
Compiler Errors Are Part of the Product
One of the clearest signs of processor quality is what happens when someone uses the annotation incorrectly.
Bad processor behavior:
- vague messages
- stack traces with no source location
- silently skipping invalid inputs
Good processor behavior:
- precise, human-readable error
- attached to the exact source element
- explains how to fix the mistake
In practice, this matters as much as the generated code itself.
Tip
If a processor saves boilerplate but produces confusing compile failures, it is still hurting developer velocity.
Keep the Processor’s Job Small
A strong processor usually does one of two things:
- validate a compile-time contract
- generate straightforward code from explicit annotated input
It should not become a hidden framework runtime living inside the compiler.
That is why the best generated code often looks almost boring:
- predictable package
- predictable class name
- readable methods
- little or no hidden policy
Generated code should be debuggable by ordinary engineers, not just by the processor author.
Incremental Build Behavior Is a Real Architectural Concern
Processors that scan too broadly or depend on unrelated files can damage build performance and confuse IDE behavior.
Try to keep processing close to annotated elements. If the processor must aggregate global state, document that cost and test it intentionally.
This matters more in large codebases than in toy examples because build friction compounds across teams.
A Sensible Rollout Pattern
If a codebase is adopting annotation processing for the first time:
- put the processor in its own module
- choose one narrow target, such as a mapper or factory
- compare generated output against a handwritten baseline
- add tests for invalid annotation usage
- add snapshot or golden-file tests where output shape matters
- expand only after the processor proves stable and understandable
That keeps adoption reviewable and makes rollback easy.
Treat Generated Code as Production Code
That means:
- generated sources participate in tests
- generated APIs are versioned carefully if other modules depend on them
- clean CI builds are part of validation
- IDE and CI generation should match
The processor is infrastructure. It deserves the same discipline as any other build-critical component.
Key Takeaways
- Annotation processing is strongest for deterministic, low-ambiguity code generation and compile-time validation.
- Diagnostics and output stability matter as much as the generation logic.
- Keep generated code readable and keep processor responsibilities narrow.
- Treat the processor and its output like production infrastructure, not clever build magic.
Categories
Tags