Chapter 4: Lombok - Reduce Boilerplate Code
Introduction
Question: How many lines of code for a simple Product class?
Without Lombok: ~100 lines (getters, setters, constructors, toString, equals, hashCode)
With Lombok: ~10 lines
Lombok = Productivity 🚀
What is Lombok?
Definition
Lombok is a Java library that automatically generates repetitive code (boilerplate) using annotations.
Boilerplate = Repetitive and boring code 😴
Examples:
- Getters and setters
- Constructors
toString()equals()andhashCode()
Analogy
Without Lombok:
You are a chef who must:
- Wash the vegetables
- Cut the vegetables
- Cook the dish
- Clean the kitchen
With Lombok:
You say "Prepare a dish for me" and an assistant does 1, 2, and 4 automatically. You focus on 3 (the cooking).
Installing Lombok
Step 1: Add the Maven Dependency
File: pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Note: <optional>true</optional> indicates that Lombok is only used at compile time.
Step 2: Install the IDE Plugin
VS Code
- Open Extensions (
Ctrl+Shift+XorCmd+Shift+X) - Search for: "Lombok Annotations Support for VS Code"
- Install the extension by Gabriel Basilio Brito
- Restart VS Code
IntelliJ IDEA
- Go to File → Settings → Plugins
- Search for: "Lombok"
- Install the plugin
- Restart IntelliJ
Eclipse
- Download
lombok.jarfrom projectlombok.org - Run:
java -jar lombok.jar - Select your Eclipse installation
- Restart Eclipse
Step 3: Verification
Create a test class:
package com.example.demo.model;
import lombok.Data;
@Data
public class TestLombok {
private String name;
private Integer age;
}
Compile:
mvn clean compile
Check in the console:
[INFO] BUILD SUCCESS
✅ Lombok works!
Main Annotations
1. @Data
Automatically generates:
- All getters
- All setters
toString()equals()andhashCode()- Constructor for
finalfields
Before Lombok
File: model/Product.java
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
// Empty constructor
public Product() {}
// Constructor with parameters
public Product(String name, Double price) {
this.name = name;
this.price = price;
}
// Getters
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Double getPrice() {
return price;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setPrice(Double price) {
this.price = price;
}
// toString()
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
'}';
}
// equals() and hashCode() (50+ more lines)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return Objects.equals(id, product.id) &&
Objects.equals(name, product.name) &&
Objects.equals(price, product.price);
}
@Override
public int hashCode() {
return Objects.hash(id, name, price);
}
}
Total: ~100 lines
After Lombok
File: model/Product.java
package com.example.demo.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
}
Total: ~15 lines
Reduction: From 100 lines to 15 lines! (85% reduction) 🎉
2. @Getter / @Setter
Generates getters/setters individually.
Example:
package com.example.demo.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
@Getter @Setter
private String name;
@Getter @Setter
private Double price;
}
Equivalent to:
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
Use at class level:
@Getter
@Setter
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Generates all getters and setters for all fields.
3. @NoArgsConstructor / @AllArgsConstructor
@NoArgsConstructor
Generates a no-arguments constructor.
@NoArgsConstructor
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Equivalent to:
public Product() {}
@AllArgsConstructor
Generates a constructor with all arguments.
@AllArgsConstructor
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Equivalent to:
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
Common combination:
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Generates:
Product()- Empty constructorProduct(Long id, String name, Double price)- Full constructor
4. @RequiredArgsConstructor
Generates a constructor with final or @NonNull fields.
Example:
@RequiredArgsConstructor
@Entity
public class Product {
private Long id;
@NonNull
private String name;
@NonNull
private Double price;
}
Generates:
public Product(String name, Double price) {
this.name = name;
this.price = price;
}
Note: id is excluded because it is not final and not @NonNull.
5. @Builder
Builder pattern for object creation.
Example:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private Double price;
}
Usage:
Product product = Product.builder()
.name("Laptop")
.price(999.99)
.build();
Advantages:
- Readable code
- Named parameters
- Flexible order
6. @ToString
Automatically generates toString().
Example:
@ToString
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Equivalent to:
@Override
public String toString() {
return "Product(id=" + id + ", name=" + name + ", price=" + price + ")";
}
Exclude fields:
@ToString(exclude = {"price"})
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Result:
Product(id=1, name=Laptop)
7. @EqualsAndHashCode
Generates equals() and hashCode().
Example:
@EqualsAndHashCode
@Entity
public class Product {
private Long id;
private String name;
private Double price;
}
Equivalent to:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return Objects.equals(id, product.id) &&
Objects.equals(name, product.name) &&
Objects.equals(price, product.price);
}
@Override
public int hashCode() {
return Objects.hash(id, name, price);
}
Use only ID:
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class Product {
@EqualsAndHashCode.Include
@Id
private Long id;
private String name;
private Double price;
}
✅ Best practice for JPA entities!
Common Combinations
For JPA Entities
Recommended configuration:
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@Min(0)
private Double price;
}
Generates:
- Getters and setters
- Empty constructor (required by JPA)
- Full constructor
toString(),equals(),hashCode()
For DTOs
Recommended configuration:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductDTO {
private Long id;
private String name;
private Double price;
}
Usage:
ProductDTO dto = ProductDTO.builder()
.id(1L)
.name("Laptop")
.price(999.99)
.build();
For Services (Dependency Injection)
Recommended configuration:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
// No need for a constructor!
public List<Product> findAll() {
return productRepository.findAll();
}
}
Lombok automatically generates:
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
Advantages and Disadvantages
Advantages
✅ Less boilerplate code (85% reduction)
✅ More readable code (focus on business logic)
✅ Easier maintenance (no getters/setters to update)
✅ Increased productivity (faster development)
✅ Fewer errors (automatically generated code)
Disadvantages
❌ External dependency (adding a library)
❌ Requires IDE plugin (for code completion)
❌ Can hide complexity (invisible code)
❌ Learning curve (for beginners)
❌ More complex debugging (generated code not visible)
Best Practices
1. Avoid @Data for Entities with Relationships
Problem:
@Entity
@Data // WARNING!
public class Order {
@Id
private Long id;
@OneToMany
private List<OrderItem> items;
}
Risk: toString() can cause infinite loops with bidirectional relationships.
Solution:
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"items"})
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Order {
@EqualsAndHashCode.Include
@Id
private Long id;
@OneToMany
private List<OrderItem> items;
}
2. Use @RequiredArgsConstructor for Services
Good:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
}
Bad:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
}
Why? Constructor injection is preferred (testability, immutability).
3. Always Combine with @NoArgsConstructor for JPA
JPA requires an empty constructor:
@Entity
@Data
@NoArgsConstructor // REQUIRED for JPA
@AllArgsConstructor
public class Product {
@Id
private Long id;
private String name;
}
Summary
Essential Annotations
| Annotation | Generates |
|---|---|
@Data | Getters, Setters, toString, equals, hashCode |
@Getter / @Setter | Getters or Setters |
@NoArgsConstructor | Empty constructor |
@AllArgsConstructor | Full constructor |
@RequiredArgsConstructor | Constructor with final/@NonNull |
@Builder | Builder pattern |
@ToString | toString() |
@EqualsAndHashCode | equals() and hashCode() |
Standard JPA Configuration
@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
}
✅ Minimal code, maximum functionality!
Next Step
You now have all the theoretical tools from Module 4! Move on to TP4.1 to migrate your application to PostgreSQL with Lombok.
➡️ TP4.1: PostgreSQL Migration