Java / Lombok Interview questions
1. What is Lombok?
Lombok is a Java library that removes boilerplate code — getters, setters, constructors,
equals/hashCode, toString, and more — by generating it
automatically at compile time from simple annotations, rather than you writing it by hand.
import lombok.Getter; import lombok.Setter; public class Person { @Getter @Setter private String name; @Getter @Setter private int age; }
Behind the scenes, Lombok plugs into the Java compiler as an annotation processor: it reads your source file, generates the missing methods directly into the compiled bytecode, and your IDE (with the Lombok plugin installed) shows those generated methods as if they were written normally. The source file itself never actually contains the generated code — it's added during compilation.
2. What is the purpose of Lombok in Java?
Lombok exists to cut down the sheer amount of repetitive, mechanical code that plain Java classes typically
require — particularly simple data-holding classes (often called POJOs) that need getters, setters,
constructors, and standard Object method overrides just to function correctly with frameworks,
collections, and equality checks.
Without Lombok, a class with five fields might need 40-plus lines of generated-looking code just for
getters, setters, and a proper equals/hashCode/toString. With Lombok,
a handful of annotations replace all of that, letting the class definition focus on what data it actually
holds rather than the mechanical methods every field needs.
@Getter @Setter @ToString @EqualsAndHashCode public class Product { private String id; private String name; private double price; }
3. How do you add Lombok to a Maven project?
Lombok is added as a regular dependency, typically scoped as provided since it's only needed
at compile time — the generated code becomes part of your compiled classes, so Lombok itself doesn't
need to be on the runtime classpath.
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency>
With that dependency present, Maven's compiler plugin automatically picks up Lombok as an annotation
processor during the compile phase — no separate build plugin configuration is normally
required, since annotation processors on the classpath are discovered automatically by
javac.
4. How do you add Lombok to a Gradle project?
Gradle uses the compileOnly and annotationProcessor configurations together for
Lombok — compileOnly makes the annotations available while writing code, and
annotationProcessor tells Gradle to actually run Lombok during compilation to generate the
boilerplate.
dependencies { compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' testCompileOnly 'org.projectlombok:lombok:1.18.30' testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' }
Both lines are necessary: omitting annotationProcessor means the annotations compile but no
code actually gets generated, resulting in "cannot find symbol" errors for methods like generated getters. The
test-prefixed variants are needed separately if you use Lombok annotations in test source code too.
5. What is @Getter used for?
@Getter generates a standard getter method for a field (or every field, when applied at the
class level), following the usual JavaBeans naming convention — getFieldName() for most
types, or isFieldName() for a boolean field.
public class Person { @Getter private String name; @Getter private boolean active; } // generates: // public String getName() { return name; } // public boolean isActive() { return active; }
Applying @Getter at the class level generates a getter for every field in that class in one
line, rather than annotating each field individually — a common shortcut once a class has several fields
that all need the same treatment.
6. What is @Setter used for?
@Setter generates a standard setter method for a field, following the JavaBeans convention of
setFieldName(Type value), and like @Getter, it can be applied per-field or at the
class level to cover every field at once.
public class Person { @Setter private String name; } // generates: // public void setName(String name) { this.name = name; }
A common pattern is applying @Getter at the class level but adding @Setter only
to specific fields that should actually be mutable, keeping other fields effectively read-only from outside
the class — useful for fields that should be set once (often via a constructor) and never changed
afterward.
7. What is @ToString used for?
@ToString generates a toString() override that prints the class name followed by
each field's name and value, giving a readable default representation instead of Java's default
ClassName@hashcode output.
@ToString public class Person { private String name; private int age; } // toString() produces: "Person(name=Ada, age=34)"
By default it includes every field, but individual fields can be excluded with
@ToString.Exclude — commonly used for sensitive fields (passwords, tokens) that shouldn't
end up in logs, or for fields that would make the output unreasonably large or cause infinite recursion (like
a reference back to a parent object).
8. What is @EqualsAndHashCode used for?
@EqualsAndHashCode generates proper equals() and hashCode()
overrides based on the class's fields, following the standard contract Java expects — two objects with
equal field values are considered equal and produce the same hash code, which is essential for correct
behavior in collections like HashSet or HashMap.
@EqualsAndHashCode public class Point { private int x; private int y; } // two Point(1, 2) instances are now equal, and share the same hashCode
By default it includes all non-static fields, but specific fields can be excluded with
@EqualsAndHashCode.Exclude — useful for fields that shouldn't factor into equality, such as
a timestamp or a cached, derived value that doesn't represent the object's actual identity.
9. What is @NoArgsConstructor used for?
@NoArgsConstructor generates a public, no-argument constructor for the class — useful
for frameworks that require a default constructor to instantiate objects via reflection, such as JPA entities
or JSON deserialization libraries like Jackson.
@NoArgsConstructor public class Person { private String name; private int age; } // generates: public Person() {}
If the class has final fields without default values, Lombok can't generate a truly empty
constructor that leaves them properly initialized, and will either fail or require the
force = true option, which initializes those fields to their default value (0,
false, or null) inside the generated constructor.
10. What is @AllArgsConstructor used for?
@AllArgsConstructor generates a constructor that takes one parameter for every field in the
class, in the order the fields are declared — a quick way to get a fully-populating constructor without
writing it by hand.
@AllArgsConstructor public class Person { private String name; private int age; } // generates: public Person(String name, int age) { this.name = name; this.age = age; }
It's often combined with @Builder so the builder has a full constructor to delegate to, or
with @NoArgsConstructor so the class supports both a default constructor (for frameworks) and a
fully-populating one (for application code), covering both use cases with two short annotations instead of two
hand-written constructors.
11. What is @RequiredArgsConstructor used for?
@RequiredArgsConstructor generates a constructor that includes only the fields Lombok
considers "required": final fields, and fields annotated @NonNull that don't already
have a default value assigned.
@RequiredArgsConstructor public class Person { private final String name; // required: included @NonNull private String email; // required: included private int age; // not required: excluded } // generates: public Person(String name, String email) { ... }
This is especially common in Spring applications for constructor-based dependency injection: marking
injected dependency fields as final and applying @RequiredArgsConstructor at the
class level generates exactly the constructor Spring needs to inject those dependencies, without listing every
other, non-dependency field.
12. What is @Data used for?
@Data is a convenience annotation that bundles several others together:
@Getter, @Setter, @ToString, @EqualsAndHashCode, and
@RequiredArgsConstructor, all applied at once with a single annotation on the class.
@Data public class Person { private final String id; private String name; private int age; } // equivalent to individually applying @Getter, @Setter (on non-final fields), // @ToString, @EqualsAndHashCode, and @RequiredArgsConstructor
It's a fast way to create a typical mutable data class, but it's an all-or-nothing bundle — if you
need to customize or exclude just one piece of that behavior (say, excluding a field from
equals), you generally either add the individual exclusion annotations alongside
@Data, or drop down to the individual annotations instead of the bundle.
13. What is @Value used for?
@Value is the immutable counterpart to @Data: it makes the class and all its
fields final by default, generates getters (but no setters), a constructor covering every field,
toString(), and equals()/hashCode() — producing a fully immutable
value object from a single annotation.
@Value public class Point { int x; int y; } // fields become private final; only getters are generated, no setters; // class itself becomes final
It's the right fit for genuinely immutable value types (coordinates, money amounts, identifiers) where you
want the compiler to enforce that instances can't change after construction, rather than relying on discipline
to avoid calling setters that technically exist, as would be the case with a mutable @Data class.
14. What is @Builder used for?
@Builder generates the builder pattern for a class — a fluent, chainable way to
construct an object field by field, particularly useful for classes with many fields (especially optional
ones) where a single large constructor call would be hard to read and easy to get wrong.
@Builder public class Person { private String name; private int age; private String email; } Person p = Person.builder() .name("Ada") .age(34) .email("ada@example.com") .build();
Unlike a plain constructor call where argument order and meaning can be easy to mix up (especially with several parameters of the same type), the builder's named methods make each value's purpose explicit at the call site, and fields left unset simply default to their type's default value rather than requiring every field to be specified.
15. What is @Slf4j used for?
@Slf4j generates a private, static logger field named log, pre-configured for the
SLF4J logging facade, saving you from writing the same one-line logger declaration in every class that needs
logging.
@Slf4j public class OrderService { public void placeOrder(Order o) { log.info("Placing order {}", o.getId()); } } // equivalent to manually writing: // private static final Logger log = LoggerFactory.getLogger(OrderService.class);
Lombok provides similar logger-generating annotations for other logging frameworks too —
@Log4j2, @CommonsLog, @Log (for java.util.logging) —
each generating the equivalent boilerplate declaration for that specific framework's logger type, bound to the
correct class automatically.
16. What is @NonNull used for?
Placing @NonNull on a field, constructor parameter, or method parameter tells Lombok to
generate a null check at the start of the relevant generated (or annotated) method, throwing a
NullPointerException immediately with a clear message if the value is null, rather
than letting a null slip through and fail confusingly somewhere else later.
public class Person { @NonNull private String name; public Person(@NonNull String name) { this.name = name; } } // generates: if (name == null) { // throw new NullPointerException("name is marked non-null but is null"); // }
It's a lightweight, compile-time-generated form of defensive null checking — not a substitute for a real nullability analysis tool, but a convenient way to fail fast with a clear error at the point a null was actually passed in, rather than at some later, harder-to-trace point in the code.
17. What is @Cleanup used for?
@Cleanup automatically calls a resource's close() method (or another method you
specify) at the end of the enclosing block, functioning as a Lombok-era predecessor to Java's built-in
try-with-resources for ensuring resources get released.
@Cleanup InputStream in = new FileInputStream("data.txt"); // Lombok inserts a finally block that calls in.close() at the end of this scope
Since Java 7 introduced try-with-resources as a language feature, @Cleanup is largely
historical for closing standard AutoCloseable resources — most modern code reaches for
try-with-resources directly instead. It remains occasionally useful for calling a cleanup method that isn't
named close(), via @Cleanup("customMethodName"), which try-with-resources can't
directly express.
18. What is @SneakyThrows used for?
@SneakyThrows lets a method throw a checked exception without declaring it in a
throws clause and without wrapping it in a try/catch — Lombok generates bytecode that throws
the checked exception directly, exploiting the fact that the JVM itself doesn't actually enforce checked
exceptions the way the Java compiler does.
@SneakyThrows public void readFile(String path) { Files.readAllBytes(Paths.get(path)); // IOException is checked, but no throws clause needed here }
It's mainly used to avoid boilerplate try/catch-and-rethrow-as-unchecked patterns, particularly in
functional interfaces (like a lambda passed to Stream.map) where checked exceptions are awkward to
propagate. It's controversial precisely because it bypasses the compiler's checked-exception enforcement,
which some teams consider valuable and don't want circumvented so easily.
19. What is @Synchronized used for?
@Synchronized is Lombok's safer alternative to Java's built-in synchronized
keyword on a method: instead of locking on this (or the class object for a static method), which
exposes the lock to any external code that also happens to synchronize on the same object, Lombok generates a
private, dedicated lock field that only this class's own generated code touches.
@Synchronized public void increment() { counter++; } // generates a private final Object lock = new Object(); // and wraps the method body in synchronized(lock) { ... }
This avoids a subtle but real risk with plain synchronized methods: since this is
a public, externally-visible reference, any other code that happens to synchronize on the same instance could
unintentionally interact with (or deadlock against) your class's own internal locking, something a private,
dedicated lock object entirely avoids.
20. What is @With used for?
@With generates a "wither" method — a method that returns a new instance of the class
with one field changed and every other field copied unchanged from the original, which is the standard pattern
for "updating" an immutable object without mutating it.
@Value @With public class Point { int x; int y; } Point p1 = new Point(1, 2); Point p2 = p1.withX(5); // new Point(5, 2); p1 itself is unchanged
It's the natural companion to @Value-style immutable classes: since you can't set a field
directly on an immutable object, @With gives you a clean, named way to derive a modified copy
instead, which is the idiomatic pattern for working with immutable data.
21. Describe how Lombok works under the hood (annotation processing)?
Lombok hooks into the Java compilation process as an annotation processor, a standard extension
point the javac compiler supports for running code during compilation that can inspect and modify
the program being compiled. Unlike most annotation processors, which are restricted to only generating new,
separate source files, Lombok uses (officially unsupported, internal) compiler APIs to directly modify the
in-memory abstract syntax tree (AST) of the class being compiled.
flowchart LR
A[Source .java file with annotations] --> B[javac parses to AST]
B --> C[Lombok annotation processor runs, modifies AST in place]
C --> D[javac continues compiling the modified AST]
D --> E[Generated .class bytecode includes Lombok's additions]
This is why Lombok can add whole new methods to an existing class (something standard annotation processors can't do) — it directly edits the parsed representation of your class before the compiler finishes turning it into bytecode, rather than generating a separate file alongside it.
22. How do you install the Lombok plugin in an IDE?
Because Lombok's generated methods don't exist in your actual source file, an IDE that only parses the source as written would show errors for calls to getters/setters that "don't exist" — the IDE plugin teaches the IDE's own code analysis to understand Lombok's annotations and treat the generated methods as if they were really there.
// IntelliJ IDEA: Settings/Preferences -> Plugins -> search "Lombok" -> Install -> restart IDE // Eclipse: download lombok.jar, run `java -jar lombok.jar`, point it at your Eclipse install
Most modern IDEs (IntelliJ IDEA, Eclipse, VS Code with the Java extension pack) have dedicated Lombok
support available as a plugin or built-in feature. Without it installed, the project can still compile
correctly from the command line (since javac itself runs the annotation processor regardless),
but the IDE's editor will show false "cannot find symbol" errors and won't offer code completion for the
generated methods.
23. What are the types of Lombok annotations?
Lombok's annotations fall into two broad groups, reflected in their package names.
| Core (lombok.*) | Experimental (lombok.experimental.*) |
| Stable, well-established annotations: @Getter, @Setter, @Data, @Builder, @ToString, @EqualsAndHashCode, and similar. | Newer or more situational features whose API may still change: @Accessors, @FieldDefaults, @Delegate, @UtilityClass, and others. |
| Safe to rely on for long-term stability. | Usable, but expect more possibility of behavior/API changes across Lombok versions. |
import lombok.Getter; // core import lombok.experimental.FieldDefaults; // experimental
The distinction is mainly a signal about stability and maturity rather than a hard functional boundary — experimental annotations work fine in practice, but Lombok's maintainers reserve more freedom to change their behavior across versions than they would for the long-stable core set.
24. How do you exclude a field from @ToString?
Annotating a specific field with @ToString.Exclude removes just that field from the generated
toString() output, while every other field is still included as usual.
@ToString public class User { private String username; @ToString.Exclude private String password; } // toString() produces "User(username=ada)" - password is omitted
This is the standard way to keep sensitive fields (passwords, tokens, secrets) out of log output, since
toString() is frequently what ends up in application logs, whether directly logged or captured
incidentally through an object being passed to a logging call. It's good practice to apply this proactively to
any field holding sensitive data, rather than discovering the leak after it's already appeared in production
logs.
25. How do you exclude a field from @EqualsAndHashCode?
@EqualsAndHashCode.Exclude on a field removes it from both the equality comparison and the
hash code calculation, while every other (non-excluded) field is still used as normal.
@EqualsAndHashCode public class Session { private String userId; @EqualsAndHashCode.Exclude private long lastAccessedAt; // shouldn't affect whether two Session objects are "equal" }
This matters for fields that are naturally variable or derived rather than part of the object's actual
identity — a "last accessed" timestamp, a cached computed value, or a mutable collection whose changing
contents shouldn't flip an object's equality (and, more subtly, whose contents changing after being placed in
a HashSet could break that set's internal invariants if it were included in the hash code).
26. What is @Builder.Default used for?
Normally, a field's default field initializer (like private int retries = 3;) is silently
ignored when using @Builder, because the builder constructs the object through its own
constructor logic rather than the field's normal initialization path — without
@Builder.Default, an unset field in the builder would end up as its type's zero value
(0, null, etc.), not the initializer you wrote.
@Builder public class RetryConfig { @Builder.Default private int maxRetries = 3; // without this annotation, unset -> 0, not 3 } RetryConfig cfg = RetryConfig.builder().build(); // cfg.getMaxRetries() == 3, thanks to @Builder.Default
This is a common gotcha for developers new to Lombok's builder: field initializers you'd expect to "just
work" are silently dropped unless explicitly marked with @Builder.Default, so any field with a
meaningful default value needs this annotation to actually preserve that default when built via the builder.
27. Why does IDE support matter for using Lombok effectively?
Since Lombok's generated methods only exist after annotation processing runs during compilation, an IDE without Lombok awareness sees only the source file as literally written — no getters, no setters, no generated constructor — and will flag every call to a Lombok-generated method as an error, even though the project compiles and runs perfectly fine from the command line.
Beyond avoiding false error highlighting, proper IDE support also enables:
- Code completion suggesting the generated methods as you type
- "Navigate to declaration" jumping to the field/annotation responsible, since there's no literal method body to jump to
- Refactoring tools (rename field, find usages) correctly tracking the generated methods too
Without this, working in a Lombok-heavy codebase in an unsupported editor is a genuinely frustrating experience — constant red squiggly lines for code that's actually correct — which is why the plugin is considered close to mandatory for serious day-to-day Lombok use, even though it isn't required for the build itself to succeed.
28. What is the difference between @Data and @Value?
Both are convenience bundles that save you from applying several annotations individually, but they target
opposite design intents: @Data produces a mutable class; @Value produces an
immutable one.
| @Data | @Value |
| Fields stay mutable by default (non-final unless you mark them so). | Fields become final automatically; the class itself becomes final too. |
| Generates getters and setters. | Generates only getters, no setters. |
| Good fit for typical mutable POJOs, DTOs, JPA entities (with caution). | Good fit for immutable value objects (coordinates, money, IDs). |
@Data public class MutablePoint { private int x, y; } @Value public class ImmutablePoint { int x, y; }
29. What is the difference between @NoArgsConstructor, @AllArgsConstructor, and @RequiredArgsConstructor?
All three generate a constructor, but they differ in which fields end up as parameters.
| @NoArgsConstructor | @RequiredArgsConstructor | @AllArgsConstructor |
| No parameters at all. | Only final fields and @NonNull fields without a default. | Every field, in declaration order. |
| Useful for frameworks needing a default constructor. | Useful for constructor-injection style, or enforcing required values at construction. | Useful for a fully-populating constructor, or as the delegate target for @Builder. |
public class Person { private final String id; // required private String name; // not required private int age; // not required } // @NoArgsConstructor -> Person() // @RequiredArgsConstructor -> Person(String id) // @AllArgsConstructor -> Person(String id, String name, int age)
It's common to combine two or three of these on the same class to support multiple valid ways of constructing it, depending on what a given caller (application code vs. a framework) actually needs.
30. When should you use @Builder instead of a constructor?
Reach for @Builder once a class has several fields, especially a mix of required and optional
ones, or multiple fields of the same type where positional constructor arguments become easy to mix up (two
adjacent String parameters, for instance, where swapping them compiles fine but is wrong).
// error-prone: which String is which? new Person("Ada", "ada@example.com", "Engineering"); // explicit and self-documenting Person.builder() .name("Ada") .email("ada@example.com") .department("Engineering") .build();
A plain constructor (often via @AllArgsConstructor) remains perfectly fine for small classes
with two or three unambiguous fields. The builder earns its keep specifically as field count and ambiguity
grow, or when you want optional fields to have sensible defaults without needing a combinatorial explosion of
overloaded constructors to cover every combination of "which fields are specified."
31. How does @EqualsAndHashCode handle inheritance by default?
By default, @EqualsAndHashCode does not call the superclass's equals/
hashCode — it only considers the fields declared in the annotated class itself, which can
silently produce incorrect equality if a subclass has meaningful fields inherited from a parent class that
should also factor into equality.
@EqualsAndHashCode(callSuper = true) public class Employee extends Person { private String department; // now includes Person's fields (via Person's equals/hashCode) AND department }
Setting callSuper = true makes the generated method also invoke the superclass's
equals/hashCode and factor that result in, which is usually what you want once
there's a real inheritance hierarchy with meaningful parent-class state. Lombok will actually emit a compiler
warning if you extend a class and don't specify callSuper explicitly, precisely because getting
this wrong is a common, easy-to-miss bug.
32. Why can @Data be risky on JPA entity classes?
@Data bundles @EqualsAndHashCode and @ToString across every field,
which interacts poorly with several JPA/Hibernate specifics: lazy-loaded associations can trigger unwanted
database queries the moment toString() or equals() touches them, and including a
mutable, database-generated id field in equals/hashCode can break
collection behavior for entities that are added to a Set before being persisted (when the id is
still null) and then persisted afterward (when it gets a real value).
@Entity @Data // risky: consider a more targeted set of annotations instead public class Order { @Id private Long id; @OneToMany private List<OrderItem> items; // lazy loading + toString/equals = trouble }
A more deliberate approach on entities is usually: exclude lazy associations from
@ToString/@EqualsAndHashCode, and base equality on a stable business key rather than
the mutable, sometimes-null database id, rather than reaching for the all-in-one @Data bundle by
default.
33. How do you combine Lombok's @Builder with inheritance?
Plain @Builder doesn't naturally handle a subclass adding its own fields on top of a parent
class's builder — by default, each class's @Builder only knows about that class's own
fields. Lombok's @SuperBuilder annotation exists specifically to solve this, generating a builder
that's aware of, and can be extended by, subclasses.
@SuperBuilder public class Vehicle { protected String make; } @SuperBuilder public class Car extends Vehicle { private int doors; } Car c = Car.builder() .make("Toyota") // inherited from Vehicle's builder .doors(4) .build();
Both the parent and every subclass in the hierarchy need @SuperBuilder (not the plain
@Builder) for this chaining to work correctly — mixing plain @Builder on a
parent with @SuperBuilder on a child (or vice versa) doesn't work, since they generate
incompatible builder implementations.
34. What is the difference between @Getter(lazy = true) and a normal @Getter?
A normal @Getter just returns the field's current value directly. @Getter(lazy = true)
is meant for a field whose value is expensive to compute: it generates a getter that computes the value only
the first time it's called, caches the result, and returns the cached value on every subsequent call.
public class Config { @Getter(lazy = true) private final List<String> expensiveData = computeExpensiveData(); } // computeExpensiveData() only actually runs the first time getExpensiveData() is called
This requires the field to be final and assigned via an initializer expression, since Lombok
implements the caching using a specific double-checked-locking pattern under the hood, generating thread-safe
lazy initialization without you having to hand-write that pattern yourself — a genuinely tricky pattern
to get right manually.
35. Why should you be careful using @EqualsAndHashCode on Hibernate entities with lazy-loaded proxies?
Hibernate sometimes returns a lazy-loading proxy object standing in for the real entity, rather than
the actual entity instance itself — and a proxy's runtime class differs from the real entity's class
(it's a dynamically generated subclass). A naive equals() comparing
this.getClass() == other.getClass(), which is what @EqualsAndHashCode can generate
by default in some configurations, can incorrectly report a real entity and its own proxy as unequal, purely
because their runtime classes differ.
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @Entity public class Product { @Id @EqualsAndHashCode.Include private Long id; // compare by id, ignoring runtime class differences from proxying }
The safer pattern is to base equality on the entity's stable identifier field alone, using
onlyExplicitlyIncluded = true combined with @EqualsAndHashCode.Include on just the
id, and to compare using instanceof rather than exact class equality, avoiding the proxy/class
mismatch entirely.
36. What is @Accessors used for and how does it change fluent-style access?
@Accessors customizes the naming and chaining style of Lombok's generated getters/setters,
moving away from strict JavaBeans conventions toward a more fluent, chainable style some codebases prefer.
@Accessors(fluent = true, chain = true) @Getter @Setter public class Person { private String name; private int age; } Person p = new Person().name("Ada").age(34); // no "get"/"set" prefix, and setters return `this`
fluent = true drops the get/set prefixes entirely (so
name() and age() serve as both getter-style and setter-style access), while
chain = true makes setters return the object itself so calls can be chained. This is a stylistic
choice — standard JavaBeans-style accessors remain the default and are what most frameworks (like
Jackson, by default) expect, so switching to fluent accessors can require extra configuration for such tools to
still work correctly.
37. How does Lombok handle @ToString with circular references?
Lombok's @ToString doesn't have built-in circular reference detection — if class A has
a @ToString-included field pointing to class B, and B has a field pointing back to A, calling
toString() on either one recurses infinitely (A's toString calls B's toString, which calls A's
toString, and so on) until it overflows the stack with a StackOverflowError.
@ToString public class Parent { private List<Child> children; // danger: each Child references parent back } @ToString public class Child { @ToString.Exclude // break the cycle here private Parent parent; private String name; }
The standard fix is exactly what's shown above: use @ToString.Exclude on whichever side of the
bidirectional relationship you don't need printed — typically the "back-reference" side (child pointing
to parent) — so the generated toString() never actually walks the cycle in the first place.
38. What is @FieldDefaults used for?
@FieldDefaults (an experimental annotation) lets you set a default access level and/or
final-ness for every field in a class in one line, rather than repeating
private final on each field individually.
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) public class Point { int x; // becomes: private final int x; int y; // becomes: private final int y; }
It's a small readability/consistency tool for classes with many fields that should uniformly be private and
immutable — commonly paired with @Value, which actually applies similar defaults for you
already, so @FieldDefaults is more useful in classes using individual annotations like
@Getter rather than the all-in-one @Value/@Data bundles that already
set field visibility and finality themselves.
39. What is @Delegate used for?
@Delegate (experimental) generates wrapper methods on a class that forward calls to a field's
methods, implementing the composition-based delegation pattern without hand-writing a pass-through method for
every method you want to expose.
public class SortedList { @Delegate private final List<String> items = new ArrayList<>(); } // SortedList now exposes add(), remove(), size(), etc. - all forwarded to `items` // without SortedList itself implementing List or writing any pass-through methods
This gives you composition's flexibility (you're not locked into extending a specific class, and you can
selectively expose only some of the delegate's interface) while avoiding the tedium of manually writing a
forwarding method for every single method on the delegated type — particularly valuable when delegating
to an interface with many methods, like List or Map.
40. Why is Lombok's approach considered a "compiler hack" and what are the implications?
Lombok works by using internal, officially unsupported compiler APIs (from javac's and
Eclipse's own compiler internals) to directly rewrite a class's abstract syntax tree during compilation —
something the standard, documented annotation processing API doesn't actually permit (it only supports
generating entirely new, separate source files, not modifying existing ones). This is why the community often
describes it as "hacking" the compiler rather than simply "using" it as intended.
The practical implications:
- Compiler version sensitivity — a new major JDK release can change internal APIs Lombok depends on, requiring Lombok itself to be updated before it works with that JDK version.
- Tooling friction — any tool that processes source or bytecode without knowing about Lombok (some static analyzers, certain IDE features) can behave unexpectedly around Lombok-generated code.
- A degree of "magic" — some teams deliberately avoid Lombok specifically because they're uncomfortable relying on unofficial compiler internals for something as central as their class definitions.
41. How do you debug generated Lombok code?
Since the generated methods don't appear in your source file, stepping through them in a debugger or
inspecting exactly what was generated requires a couple of specific techniques rather than just reading the
.java file.
# view the actual generated code as plain Java: delombok src/main/java -d src/delomboked # or inspect the compiled .class file's bytecode directly: javap -c -p target/classes/com/example/Person.class
delombok (Lombok's own tool) converts your annotated source into an equivalent, fully expanded
plain-Java source tree with no Lombok annotations at all — the clearest way to see exactly what code was
generated. Most modern IDEs with Lombok plugin support also let you step into generated methods directly in the
debugger, since the plugin makes the IDE aware of them, though the "source" it shows during debugging is
often a synthesized view rather than a literal file on disk.
42. What issues can Lombok cause with static code analysis tools?
Static analysis tools that operate on source code (rather than compiled bytecode) may not understand Lombok's annotations at all, and so analyze the class as if the generated methods simply don't exist — this can produce both false positives (flagging a field as "never read" when it actually is, via a Lombok-generated getter) and false negatives (missing a real bug inside logic that only exists after Lombok's generation, since the tool never sees it).
// a source-based analyzer might flag `email` as unused, // not realizing @Getter/@Setter generate real accessors for it @Getter @Setter private String email;
The fix generally depends on the specific tool: many mainstream static analysis tools (SonarQube, Checkstyle, PMD, certain IDE inspections) have added explicit Lombok awareness over time, either natively or via a plugin, so this has become less of a problem than it once was — but a lesser-known or in-house tool that only parses raw source may still need this pointed out or configured for, and coverage tools measuring line coverage can also misreport results for Lombok-generated code that technically has no corresponding source lines to mark as covered.
43. What are the risks of using @Data with mutable collections?
A field like private List<String> tags; under @Data gets a plain getter
that returns the actual internal list reference, not a defensive copy — meaning external code that calls
the getter can mutate the object's internal state directly, bypassing any invariant the class was supposed to
maintain.
@Data public class Team { private List<String> members; } Team t = new Team(List.of("Ada")); t.getMembers().add("Grace"); // mutates Team's internal state from outside, if the list itself is mutable
This also interacts badly with @EqualsAndHashCode and HashSet/HashMap
usage: if such an object is placed in a hash-based collection and then its internal collection field is
mutated externally (changing its hash code), the object can become "lost" in that collection, unable to be
found by a subsequent lookup even though it's still physically present. Defensive copying in the getter, or
exposing an unmodifiable view, is something you'd have to add manually — Lombok doesn't do it for you.
44. How does delombok work and when would you use it?
delombok runs Lombok's own transformation logic but, instead of feeding the result to the
compiler, writes out the fully expanded, plain-Java equivalent source code — every getter, setter,
constructor, and other generated method spelled out explicitly, with no Lombok annotations remaining.
java -jar lombok.jar delombok src/main/java -d src/main/delomboked
Common reasons to use it:
- Generating Javadoc — the standard Javadoc tool doesn't understand Lombok annotations, so running delombok first produces source Javadoc can actually process and document correctly.
- Debugging exactly what Lombok generated, without guessing from the annotation alone.
- Removing a Lombok dependency from a project — delomboking the whole codebase gives you a starting point of equivalent plain Java to work from, rather than hand-writing every generated method from scratch.
45. Why might Lombok cause issues in multi-module Maven builds with annotation processor ordering?
Annotation processors run in whatever order the build tool discovers and applies them, and if a project uses multiple processors together (Lombok alongside MapStruct, or a custom code-generating processor), the order they run in can matter — particularly since Lombok's generated methods need to already exist by the time another processor that depends on seeing them runs its own analysis.
<annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> </path> </annotationProcessorPaths>
Explicitly declaring annotationProcessorPaths (rather than relying on whatever processors
happen to be found on the classpath) lets you control this ordering deliberately, which becomes especially
relevant in multi-module builds where different modules might otherwise pick up processors in inconsistent
orders depending on classpath assembly quirks, leading to intermittent, hard-to-reproduce build failures where
a processor can't find methods Lombok hadn't generated yet.
46. What is @UtilityClass used for?
@UtilityClass (experimental) converts a class into a static utility class: it makes the class
final, generates a private constructor that throws if somehow invoked via reflection, and marks
every field and method in the class as static automatically — the standard boilerplate for
a class that's meant to be a bag of static helper methods, never instantiated.
@UtilityClass public class MathUtils { public int square(int x) { return x * x; } } // equivalent to a final class with a private constructor // and `square` implicitly treated as `public static int square(int x)`
Without it, writing a proper utility class by hand means remembering to mark the class final,
add a private no-op constructor, and mark every single method static individually — small
but easy-to-forget details that @UtilityClass handles for you in one line.
47. What is the difference between Lombok's @Builder and the traditional Gang-of-Four Builder pattern?
The classic Gang-of-Four Builder pattern is typically hand-written as a separate, standalone class (often
with its own interface) explicitly responsible for assembling a complex object step by step, sometimes
supporting different construction algorithms or representations via a "Director" that orchestrates the
builder. Lombok's @Builder automatically generates a much simpler, single-purpose builder scoped
to just the annotated class, focused purely on the "fluent field-by-field construction" convenience rather
than the full flexibility of the original pattern.
| GoF Builder pattern | Lombok @Builder |
| Hand-written, can support multiple representations/directors. | Auto-generated, tied directly to one class's fields. |
| More ceremony, more flexibility for complex construction logic. | Minimal ceremony, covers the common "set fields fluently" case well. |
In practice, most real-world uses of "the builder pattern" in application code are really just this simpler, fluent-construction convenience — which is exactly what Lombok automates — rather than the full GoF pattern with its director/strategy flexibility, which is comparatively rare to need in ordinary business code.
48. How do you use Lombok with Java records - is Lombok still needed?
Java's built-in record type (since Java 16) already generates a canonical constructor,
accessors, equals(), hashCode(), and toString() automatically —
covering a large chunk of what @Value or @Data used to provide for immutable classes,
without needing Lombok at all.
// Java record - no Lombok needed for this much: public record Point(int x, int y) {} // automatically gets: constructor, x(), y(), equals(), hashCode(), toString()
Lombok still adds value on top of records for things records don't provide natively — most notably
@Builder, since records don't have a built-in fluent builder, and @With-style
"derive a modified copy" methods, which records also lack built-in. So for simple immutable data carriers,
records alone are often enough; Lombok remains useful specifically when you want a builder or wither-style
methods on top of a record, or for classes that don't fit the record shape (mutable classes, classes needing
custom equals/hashCode logic beyond field-based defaults).
