Spring Boot with Lombok
Java classes often contain more repetitive code than real logic: getters, setters, constructors, toString(), equals() and hashCode(). Project Lombok is an open-source library that writes this code for you, based on a few annotations, so your classes stay short and focused.
This tutorial shows how to set up Lombok in a Spring Boot project, explains each important annotation with examples, shows how to use Lombok safely in each layer, especially with JPA entities, and compares it with Java records.
1. What Is Lombok?
Here is a simple class written in plain Java:
public class Product {
private Long id;
private String name;
private BigDecimal price;
public Product() {
}
public Product(Long id, String name, BigDecimal price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
@Override
public String toString() {
return "Product(id=" + id + ", name=" + name + ", price=" + price + ")";
}
// ... plus equals() and hashCode(), another 20 lines
}
And here is the same class with Lombok:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
private Long id;
private String name;
private BigDecimal price;
}
Both versions behave the same. The Lombok version is easier to read, and when you add a field, the getter, setter, toString() and the other methods update automatically, so they can never be forgotten or fall out of date.
2. How Lombok Works
Lombok is an annotation processor. It runs inside the Java compiler:
- The compiler reads your source code and finds Lombok annotations such as
@Getter. - Lombok adds the matching methods to the class while it is being compiled.
- The compiler writes a normal
.classfile that contains those methods.
The result is ordinary Java bytecode, exactly as if you had written the methods by hand. That means:
- No runtime cost: nothing extra runs while your application runs.
- No runtime dependency: Lombok is only needed for compiling, not inside the final JAR.
- Normal for other tools: Spring, Jackson and Hibernate simply see regular getters, setters and constructors.
3. Adding Lombok to a Spring Boot Project
The easiest way is to tick Lombok when creating the project on Spring Initializr. To add it to an existing Maven project, use three pieces. Spring Boot already manages the Lombok version, so you do not need to choose one:
<dependencies>
<!-- 1. The library itself, only needed while compiling -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 2. Register Lombok as an annotation processor for the compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- 3. Keep Lombok out of the packaged JAR -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
| Part | Why it is needed |
|---|---|
<optional>true</optional> | Stops Lombok from being passed on to other projects that depend on yours |
annotationProcessorPaths | Tells the compiler to run Lombok; newer JDKs no longer find annotation processors automatically |
Spring Boot plugin excludes | Leaves Lombok out of the runnable JAR, because it is not needed at runtime |
For Gradle, two lines are enough:
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
4. IDE Setup
Your build tool compiles Lombok code correctly on its own, but the IDE also needs to understand Lombok, or it shows false errors such as “method getName() not found”.
| IDE | What to do |
|---|---|
| IntelliJ IDEA | Lombok support is built in. If prompted, enable Settings → Build, Execution, Deployment → Compiler → Annotation Processors → Enable annotation processing. |
| Eclipse / Spring Tools | Run the Lombok JAR once (java -jar lombok.jar), point it at your Eclipse installation, and restart Eclipse. |
| VS Code | The Java extension pack includes Lombok support; no extra steps. |
5. @Getter and @Setter
@Getter and @Setter generate accessor methods. Put them on the class for all fields, or on single fields for finer control:
@Getter
@Setter
public class Customer {
private Long id;
private String name;
private boolean active; // generates isActive() and setActive(...)
@Setter(AccessLevel.NONE) // no setter for this field
private LocalDateTime createdAt = LocalDateTime.now();
@Getter(AccessLevel.PROTECTED) // getter only visible to subclasses
private String internalNote;
}
Use AccessLevel to generate methods with other visibility, or AccessLevel.NONE to skip one. Only add setters where values really should change.
6. Constructor Annotations
| Annotation | Generates a constructor with... | Typical use |
|---|---|---|
@NoArgsConstructor | No parameters | JPA entities and frameworks that create objects by reflection |
@AllArgsConstructor | One parameter per field | DTOs and test data |
@RequiredArgsConstructor | Parameters for final and @NonNull fields only | Spring beans with constructor injection |
@RequiredArgsConstructor is especially useful in Spring, because it gives you clean constructor injection:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository; // injected
private final PaymentClient paymentClient; // injected
private int retryCount = 3; // not final: not in the constructor
public Order place(OrderRequest request) {
Order order = orderRepository.save(Order.from(request));
paymentClient.charge(order);
return order;
}
}
Lombok generates a constructor taking OrderRepository and PaymentClient. Because it is the only constructor, Spring uses it automatically, with no @Autowired needed. Add a new final dependency, and the constructor updates itself.
7. @ToString and @EqualsAndHashCode
@ToString generates a readable toString(), which is handy in logs. Exclude fields that are sensitive or large:
@Getter
@ToString
public class Account {
private Long id;
private String email;
@ToString.Exclude
private String passwordHash; // never printed in logs
}
// account.toString() -> "Account(id=7, email=asha@example.com)"
@EqualsAndHashCode generates equals() and hashCode(). By default it uses all fields; it is usually better to choose them explicitly:
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Product {
@EqualsAndHashCode.Include
private String sku; // two products are equal if their SKUs match
private String name;
private BigDecimal price;
}
8. @Data and @Value
Two shortcut annotations bundle several others:
@Data | @Value | |
|---|---|---|
| Getters | Yes | Yes |
| Setters | Yes, for non-final fields | No |
| Fields | As you declare them | All made private final |
| Constructor | For required (final) fields | For all fields |
toString, equals, hashCode | Yes | Yes |
| Class | Can be changed after creation (mutable) | Cannot be changed (immutable), and is final |
@Data
public class CartItemForm { // mutable: a web form fills it field by field
private String sku;
private int quantity;
}
@Value
public class Money { // immutable: safe to share between threads
BigDecimal amount;
String currency;
}
Money price = new Money(new BigDecimal("19.99"), "INR");
Prefer @Value for small value objects that should never change. Use @Data for simple data holders such as form objects, but never for JPA entities (see section 13).
9. @Builder
@Builder creates a builder, a readable way to construct objects with many fields, where each value is named:
@Getter
@Builder(toBuilder = true)
public class NotificationRequest {
private String recipient;
private String subject;
private String body;
@Builder.Default
private Channel channel = Channel.EMAIL; // used when channel() is not called
@Builder.Default
private int priority = 5;
}
NotificationRequest welcome = NotificationRequest.builder()
.recipient("asha@example.com")
.subject("Welcome!")
.body("Thanks for signing up.")
.build(); // channel = EMAIL, priority = 5
// toBuilder() copies an existing object and changes only some fields
NotificationRequest urgent = welcome.toBuilder()
.channel(Channel.SMS)
.priority(1)
.build();
| Feature | What it does |
|---|---|
@Builder.Default | Keeps a field’s default value when the builder does not set it; without it, the field ends up null or 0 |
toBuilder = true | Adds toBuilder(), which copies an object into a new builder |
@Singular | On a collection field, adds a method that adds one item at a time, such as .tag("new") |
10. @Slf4j and @NonNull
@Slf4j adds a ready-to-use logger field named log:
@Slf4j
@Service
@RequiredArgsConstructor
public class InvoiceService {
private final InvoiceRepository invoiceRepository;
public Invoice create(Order order) {
log.info("Creating invoice for order {}", order.getId());
return invoiceRepository.save(Invoice.from(order));
}
}
It replaces the usual line private static final Logger log = LoggerFactory.getLogger(InvoiceService.class);, and it can never point at the wrong class by mistake after copying code.
@NonNull adds a null check at the start of a method or constructor:
public Label createLabel(@NonNull Address address) {
// Lombok inserts: if (address == null) throw new NullPointerException("address is marked non-null but is null");
return labelPrinter.print(address);
}
Use it for internal safety checks. For validating user input in requests, use Bean Validation annotations such as @NotNull instead, which return friendly error responses.
11. Annotation Cheat Sheet
| Annotation | Generates |
|---|---|
@Getter / @Setter | Getter and setter methods |
@NoArgsConstructor | A constructor with no parameters |
@AllArgsConstructor | A constructor with all fields |
@RequiredArgsConstructor | A constructor with final and @NonNull fields |
@ToString | A toString() method |
@EqualsAndHashCode | equals() and hashCode() |
@Data | Getters, setters, required-args constructor, toString, equals, hashCode |
@Value | An immutable class: private final fields, getters, all-args constructor and more |
@Builder | A builder for readable object creation |
@With | Methods like withPrice(...) that return a copy with one field changed |
@Slf4j | A log field using SLF4J |
@NonNull | A null check on a parameter or field |
12. Lombok in Each Application Layer
| Layer | Recommended Lombok annotations | Avoid |
|---|---|---|
| Controller | @RequiredArgsConstructor, @Slf4j | Setters on controllers |
| Service | @RequiredArgsConstructor, @Slf4j | @Data (services are not data) |
| Request/response DTO | @Value or @Builder, or a Java record | Mutable setters you do not need |
| Form or mutable DTO | @Data, @NoArgsConstructor | — |
| JPA entity | @Getter, @Setter, @NoArgsConstructor | @Data, default @ToString and @EqualsAndHashCode |
@Slf4j
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/{id}")
public ProductResponse get(@PathVariable Long id) {
log.debug("Fetching product {}", id);
return productService.findById(id);
}
}
13. Lombok and JPA Entities
JPA entities need special care, because Hibernate uses proxies and lazy loading. @Data on an entity causes three problems:
| Problem | Cause | Result |
|---|---|---|
| Unexpected database queries | toString() touches lazy relationships | Slow logging, or a LazyInitializationException |
| Endless loops | toString() or hashCode() follows a two-way relationship back and forth | StackOverflowError |
| Entities lost from sets | hashCode() uses fields that change, including the id assigned on save | An entity added to a HashSet cannot be found again |
A safe pattern uses only @Getter and @Setter, and writes equals() and hashCode() by hand:
@Entity
@Table(name = "customers")
@Getter
@Setter
@NoArgsConstructor
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "customer")
private List<Order> orders = new ArrayList<>();
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Customer that)) return false;
return id != null && id.equals(that.getId());
}
@Override
public int hashCode() {
return getClass().hashCode(); // stays the same before and after saving
}
@Override
public String toString() {
return "Customer(id=" + id + ", name=" + name + ")"; // no relationships
}
}
Entities are compared by their database id, the hash code never changes, and toString() never touches the lazy orders list. For more on entities, see the Spring Data JPA Overview.
14. Lombok vs Java Records
Since Java 16, records give you immutable data classes without any library:
public record ProductResponse(Long id, String name, BigDecimal price) {
}
| Java record | Lombok | |
|---|---|---|
| Needs a library | No, built into Java | Yes |
| Immutable data carriers (DTOs) | Excellent | Good (@Value) |
| Mutable classes with setters | Not possible | Yes |
| JPA entities | Not suitable | Yes, with care |
| Builders | Not built in | @Builder |
| Spring beans and logging | Not applicable | @RequiredArgsConstructor, @Slf4j |
A practical rule: use records for request and response DTOs, and Lombok for entities, Spring beans and classes that need builders or setters. Both work well side by side in the same project.
15. Configuring Lombok with lombok.config
A lombok.config file in the project root sets rules for every class below it. A useful starting point:
# This is the root configuration; do not look in parent folders
config.stopBubbling = true
# Mark generated code so test coverage tools such as JaCoCo ignore it
lombok.addLombokGeneratedAnnotation = true
# Copy @Qualifier and @Value from fields to the generated constructor
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
# Prevent the risky @Data on any class, if your team agrees
# lombok.data.flagUsage = error
| Setting | Benefit |
|---|---|
config.stopBubbling | Keeps settings predictable when projects are nested |
addLombokGeneratedAnnotation | Coverage reports measure only the code you actually wrote |
copyableAnnotations | Makes @Qualifier and @Value work with @RequiredArgsConstructor |
*.flagUsage | Warns about, or forbids, annotations your team has decided not to use |
16. Seeing the Generated Code
Sometimes you want to see exactly what Lombok generated. Delombok converts Lombok annotations into the plain Java code they produce:
- In IntelliJ IDEA: right-click the class and choose Refactor → Delombok. Undo it afterwards if you only wanted a look.
- In the IDE outline: the Structure view lists all generated methods, without changing the file.
- From the command line: run the Lombok JAR’s delombok tool, which writes plain Java versions of your sources to another folder.
java -jar lombok.jar delombok src/main/java -d target/delombok
This is also the escape route if a team ever decides to stop using Lombok: delombok the code once, and continue with plain Java.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
Using @Data on JPA entities | Lazy-loading errors, endless loops, broken sets | Use @Getter/@Setter and write equals/hashCode yourself |
Forgetting @Builder.Default | Default field values are ignored by the builder | Add @Builder.Default to fields with defaults |
Using @Builder alone on a class that Jackson or JPA must create | No no-argument constructor, so creating the object fails | Add @NoArgsConstructor and @AllArgsConstructor, or use @Jacksonized |
| Generating setters everywhere | Objects can be changed from anywhere | Add setters only where needed, or use @Value |
Printing passwords or tokens in toString() | Secrets appear in logs | @ToString.Exclude on sensitive fields |
| Missing annotation processor setup | “cannot find symbol” errors in the build | Configure annotationProcessorPaths (section 3) |
18. Pros and Cons
| Pros | Cons |
|---|---|
| Far less repetitive code | A build-time dependency every developer must set up |
| Classes stay short and readable | Generated code is not visible in the source file |
| Methods update automatically when fields change | Overuse, such as @Data everywhere, can cause subtle bugs |
Clean constructor injection with @RequiredArgsConstructor | Must be updated when you move to a new Java version |
| No runtime cost; not in the final JAR | Some teams prefer explicit code and avoid it |
19. Summary
Lombok removes repetitive Java code by generating it at compile time, with no runtime cost. Set it up with the dependency, the annotation processor configuration and the JAR exclusion, and your IDE will understand it too. Use @RequiredArgsConstructor and @Slf4j for Spring beans, @Value, @Builder or records for DTOs, and only @Getter and @Setter, with hand-written equals and hashCode, for JPA entities. Chosen with care, Lombok keeps a Spring Boot codebase short, clear and easy to change.
Frequently Asked Questions (FAQ)
Q1. Is Lombok safe to use in production applications?
Yes. Lombok only runs while your code is compiled. The compiled classes contain ordinary Java methods, and Lombok itself is not part of the running application or the packaged JAR.
It is widely used in production systems of all sizes. The real risks are in how it is used, such as @Data on JPA entities, not in Lombok itself.
Q2. Why do I get “cannot find symbol” errors for Lombok methods?
The compiler did not run Lombok, so methods such as getName() were never generated. Check:
- Maven build fails: add Lombok to
annotationProcessorPathsin the compiler plugin. Newer JDK versions no longer run annotation processors found on the classpath automatically. - Only the IDE shows errors: enable annotation processing in the IDE, or install Lombok support for Eclipse.
- After a Java upgrade: the Lombok version may be too old for the new JDK. Upgrade Spring Boot or Lombok.
Q3. Does Lombok work with the newest Java version?
Lombok works closely with the Java compiler’s internals, so each new Java release needs a Lombok release that supports it. Support usually arrives around the time the new Java version is released.
When upgrading Java:
- Upgrade Spring Boot first; it manages a matching Lombok version for you.
- If you need a newer Lombok, set the
lombok.versionproperty inpom.xml. - Watch for build errors that mention Lombok or annotation processing, which signal an outdated version.
Q4. How do I use @Qualifier with @RequiredArgsConstructor?
By default, Lombok does not copy @Qualifier from a field to the generated constructor parameter, so Spring ignores it. Tell Lombok to copy it, in lombok.config:
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
@Service
@RequiredArgsConstructor
public class AlertService {
@Qualifier("smsNotifier")
private final Notifier notifier; // now injected with the "smsNotifier" bean
}
Without this setting, Spring may fail with an error about several candidate beans of the same type.
Q5. Why can’t Jackson read JSON into my @Builder class?
A class with only @Builder has no public no-argument constructor and no setters, so Jackson does not know how to create it. Two fixes:
// Option 1: let Jackson use the builder (recommended)
@Value
@Builder
@Jacksonized
public class CreateOrderRequest {
String customerEmail;
List<String> items;
}
// Option 2: add the constructors Jackson and other tools expect
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateOrderRequest { ... }
@Jacksonized configures Jackson to deserialize through the builder, so the class can stay immutable.
Q6. Why are my field default values ignored when I use @Builder?
The builder sets every field from its own internal values. A field the builder did not set is given null, 0 or false, which overwrites your default:
@Builder
public class Settings {
private int pageSize = 20; // ignored by the builder: becomes 0
@Builder.Default
private String theme = "light"; // kept when theme() is not called
}
Add @Builder.Default to every field with a default value. Lombok warns about this at compile time, so watch for that warning.
Q7. What getter name does Lombok generate for boolean fields?
| Field | Getter | Setter |
|---|---|---|
boolean active | isActive() | setActive(...) |
Boolean active (wrapper) | getActive() | setActive(...) |
boolean isActive | isActive() | setActive(...) |
The last row surprises people: Lombok removes the is prefix, and Jackson then writes the JSON property as active, not isActive. Name boolean fields without the is prefix to avoid confusion.
Q8. Should I use @SneakyThrows?
Use it rarely. @SneakyThrows lets a method throw a checked exception without declaring it. That hides important information from callers, who no longer see that the method can fail with, for example, an IOException.
// Prefer: wrap the checked exception in a meaningful unchecked one
public String readTemplate(Path path) {
try {
return Files.readString(path);
} catch (IOException ex) {
throw new TemplateLoadException("Could not read " + path, ex);
}
}
Acceptable uses are narrow, such as inside lambdas where a checked exception truly cannot occur.
Q9. How do I use Lombok together with MapStruct?
Both are annotation processors, and MapStruct must see the methods Lombok generates. Register both, plus a small binding library that makes them cooperate:
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
Without the binding, MapStruct may report that it cannot find getters or setters that Lombok was supposed to generate.
Q10. Can I generate getters for only some fields?
Yes. Put @Getter or @Setter on individual fields instead of the class, or put them on the class and switch them off for particular fields:
@Getter
public class Employee {
private String name;
private String department;
@Getter(AccessLevel.NONE)
private BigDecimal salary; // no public getter: exposed only through business methods
public boolean earnsMoreThan(BigDecimal amount) {
return salary.compareTo(amount) > 0;
}
}
Hiding fields this way keeps sensitive data inside the class and encourages meaningful methods instead of raw getters.
Q11. Can I use Lombok annotations on Java records?
Some of them, yes. Records already have constructors, accessors, equals, hashCode and toString, but Lombok can still add a builder or with methods:
@Builder
@With
public record ShippingAddress(String street, String city, String postcode) {
}
ShippingAddress home = ShippingAddress.builder()
.street("12 MG Road").city("Bengaluru").postcode("560001")
.build();
ShippingAddress office = home.withStreet("45 Brigade Road");
Annotations such as @Getter, @Setter or @Data make no sense on records, because records are immutable and already have accessors.
Q12. How do I debug code that Lombok generated?
Generated methods have no source lines, so you cannot place a breakpoint inside, say, a generated getter. Practical options:
- Put a field watchpoint on the field itself. The debugger stops whenever it is read or changed, including through generated methods.
- Write that one method by hand. Lombok skips generating any method you write yourself, so you can debug it normally.
- Delombok the class temporarily to see and step through the full code.
Q13. What does callSuper mean in @EqualsAndHashCode?
When a class extends another, Lombok by default compares only the subclass’s own fields, and warns about it. callSuper = true includes the parent’s equals() and hashCode() too:
@EqualsAndHashCode
public class Vehicle {
private String registration;
}
@EqualsAndHashCode(callSuper = true)
public class Truck extends Vehicle {
private int maxLoadKg;
}
Without it, two trucks with different registrations but the same load would be considered equal. If the parent class is Object, leave callSuper off.
Q14. What does @Accessors(chain = true) do, and is it safe?
It makes setters return this, so you can chain calls:
@Getter
@Setter
@Accessors(chain = true)
public class SearchFilter {
private String keyword;
private int page;
}
SearchFilter filter = new SearchFilter().setKeyword("lamp").setPage(2);
It is convenient, but some libraries expect setters that return void, following the JavaBeans convention, and may not recognise chained setters as properties. For readable object creation, @Builder is usually the safer choice.
Q15. How hard is it to stop using Lombok later?
Not hard. Lombok has a delombok tool that rewrites your source code into plain Java, with all the generated methods written out. Run it once, review the result, remove the Lombok dependency, and the project continues as normal Java code.
This low exit cost is one reason many teams are comfortable adopting Lombok: it is a convenience, not a lock-in.
Q16. Why do some teams choose not to use Lombok?
Common reasons:
- Hidden code: some developers prefer seeing every method in the source file.
- Build coupling: Lombok depends on compiler internals, so Java upgrades may have to wait for a Lombok update.
- Modern Java covers much of it: records replace many data classes, and IDEs can generate the remaining methods.
- Easy to misuse:
@Dataon entities or setters everywhere can cause real bugs.
Neither choice is wrong. What matters is that a team agrees on one approach and applies it consistently.
Q17. What is the difference between Lombok’s @NonNull and @NotNull?
Lombok @NonNull | Bean Validation @NotNull | |
|---|---|---|
| When it checks | Every time the method or constructor is called | Only when validation runs, e.g. with @Valid |
| On failure | Throws NullPointerException | Collects a validation error with a message |
| Best for | Internal programming mistakes | User input in requests and forms |
Use @NotNull on request DTOs so clients get a clear 400 response, and @NonNull to catch mistakes inside your own code early.
Q18. Should I use Lombok’s @Cleanup to close resources?
Usually not. @Cleanup closes a resource at the end of its scope, but Java’s built-in try-with-resources does the same job, is understood by every developer and tool, and handles exceptions during closing more carefully:
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.lines().count();
} // reader is closed automatically, even if an exception occurs
Prefer standard Java features when they are just as short. Use Lombok for things Java does not offer as conveniently, such as builders and constructor injection.
Q19. Does @Slf4j work if my project uses Log4j2 instead of Logback?
Yes. @Slf4j creates an SLF4J logger, and SLF4J is only an interface. It sends log messages to whichever logging library is on the classpath: Logback, which is Spring Boot’s default, or Log4j2 if you switch to spring-boot-starter-log4j2.
Lombok also offers annotations for using a specific library directly:
| Annotation | Creates a logger from |
|---|---|
@Slf4j | SLF4J (recommended for Spring Boot) |
@Log4j2 | Log4j2 directly |
@CommonsLog | Apache Commons Logging |
@Log | java.util.logging |
Stick with @Slf4j, so you can change the logging library later without touching any class.
Q20. Does Lombok slow down compilation?
Slightly. Lombok runs as part of every compilation, which adds a small amount of time. For typical Spring Boot projects the difference is hardly noticeable.
In very large codebases, keep Lombok up to date, because newer versions include performance improvements. The running application is not affected at all, because Lombok’s work is finished once the classes are compiled.