Introduction
Lombok is a Java library that automatically plugs into your editor and build tools, spicing up your Java code. It helps eliminate boilerplate code like getters, setters, constructors, equals, hashCode, and toString methods through annotations, making your code cleaner and more maintainable.
Maven Dependencies
<!-- Lombok dependency --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency> <!-- For Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Annotation processor for IDEs --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> <optional>true</optional> </dependency>
Core Annotations
@Data - The Complete Package
import lombok.Data;
// Without Lombok - 50+ lines of boilerplate
public class UserWithoutLombok {
private Long id;
private String username;
private String email;
private int age;
private boolean active;
// Constructors, getters, setters, equals, hashCode, toString...
}
// With Lombok - Just 7 lines!
@Data
public class User {
private Long id;
private String username;
private String email;
private int age;
private boolean active;
}
// Usage example
public class UserService {
public void processUser() {
User user = new User();
user.setId(1L);
user.setUsername("john_doe");
user.setEmail("[email protected]");
user.setAge(30);
user.setActive(true);
System.out.println(user); // Auto-generated toString()
User anotherUser = new User();
anotherUser.setId(1L);
System.out.println(user.equals(anotherUser)); // Auto-generated equals()
}
}
@Getter and @Setter
import lombok.Getter;
import lombok.Setter;
public class Product {
// Field-level annotations
@Getter @Setter
private Long id;
@Getter @Setter
private String name;
// Class-level annotations with access levels
@Getter
@Setter
public static class Category {
private Long id;
private String name;
private String description;
// Lombok generates:
// public Long getId() { return this.id; }
// public void setId(Long id) { this.id = id; }
// ... and so on for other fields
}
// Configuring access levels
@Getter(AccessLevel.PROTECTED)
@Setter(AccessLevel.PRIVATE)
private String internalCode;
@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.NONE) // No setter generated
private final String sku;
public Product(String sku) {
this.sku = sku;
}
}
// Usage
class ProductService {
public void demonstrateGettersSetters() {
Product product = new Product("SKU123");
Product.Category category = new Product.Category();
category.setId(1L);
category.setName("Electronics");
// product.setSku("NEW123"); // Compile error - no setter for final field
String sku = product.getSku(); // This works
// String code = product.getInternalCode(); // Compile error - protected getter
// product.setInternalCode("CODE"); // Compile error - private setter
}
}
Constructors Made Easy
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
private Long id;
private String name;
private String department;
private double salary;
// Lombok generates:
// public Employee() {}
// public Employee(Long id, String name, String department, double salary) {
// this.id = id;
// this.name = name;
// this.department = department;
// this.salary = salary;
// }
}
@RequiredArgsConstructor
public class Customer {
private final Long id;
private final String customerCode;
@NonNull
private String name;
private String email;
private String phone;
// Lombok generates:
// public Customer(Long id, String customerCode, String name) {
// if (name == null) throw new NullPointerException("name is marked non-null but is null");
// this.id = id;
// this.customerCode = customerCode;
// this.name = name;
// }
}
// Static factory methods
@AllArgsConstructor(staticName = "of")
public class Point {
private final int x;
private final int y;
// Usage: Point point = Point.of(10, 20);
}
// Builder pattern with constructor
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Address {
private String street;
private String city;
private String zipCode;
private String country;
}
class ConstructorExamples {
public void demonstrateConstructors() {
// No-args constructor
Employee emp1 = new Employee();
// All-args constructor
Employee emp2 = new Employee(1L, "John Doe", "IT", 75000.0);
// Required args constructor
Customer customer = new Customer(1L, "CUST001", "Jane Smith");
// Customer invalid = new Customer(1L, "CUST001", null); // Throws NPE
// Static factory
Point point = Point.of(10, 20);
// Builder
Address address = Address.builder()
.street("123 Main St")
.city("New York")
.zipCode("10001")
.country("USA")
.build();
}
}
Advanced Lombok Features
@Builder - Fluent Object Creation
import lombok.Builder;
import lombok.Singular;
import lombok.ToString;
import java.util.List;
import java.util.Map;
@Builder
@ToString
public class Order {
private Long id;
private String orderNumber;
private String customerEmail;
private double totalAmount;
@Singular
private List<OrderItem> items;
@Singular
private Map<String, String> metadata;
@Builder.Default
private OrderStatus status = OrderStatus.PENDING;
@Builder.Default
private int priority = 1;
public enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
}
@Builder
@ToString
class OrderItem {
private String productId;
private String productName;
private int quantity;
private double unitPrice;
}
class BuilderExamples {
public void demonstrateBuilder() {
// Basic builder
Order order = Order.builder()
.id(1L)
.orderNumber("ORD001")
.customerEmail("[email protected]")
.totalAmount(199.99)
.build();
// With singular collections
Order complexOrder = Order.builder()
.id(2L)
.orderNumber("ORD002")
.item(OrderItem.builder()
.productId("P001")
.productName("Laptop")
.quantity(1)
.unitPrice(999.99)
.build())
.item(OrderItem.builder()
.productId("P002")
.productName("Mouse")
.quantity(2)
.unitPrice(25.99)
.build())
.metadata("source", "web")
.metadata("campaign", "spring_sale")
.status(Order.OrderStatus.CONFIRMED)
.build();
System.out.println(complexOrder);
// Output includes all items and metadata
// ToBuilder for creating modified copies
Order modifiedOrder = complexOrder.toBuilder()
.totalAmount(1050.97)
.status(Order.OrderStatus.SHIPPED)
.build();
}
}
@Value - Immutable Classes
import lombok.Value;
import lombok.With;
import lombok.experimental.NonFinal;
@Value
public class ImmutableUser {
Long id;
String username;
String email;
@NonFinal
@With
String temporaryPassword;
// Lombok generates:
// - All fields are made private and final
// - Getter methods for all fields (no setters)
// - All-args constructor
// - equals(), hashCode(), toString()
// Custom constructor
public ImmutableUser(String username, String email) {
this.id = null;
this.username = username;
this.email = email;
this.temporaryPassword = null;
}
}
@Value
@Builder
public class ImmutableProduct {
String id;
String name;
String description;
double price;
@Builder.Default
boolean available = true;
}
class ValueExamples {
public void demonstrateImmutableObjects() {
// Using @Value
ImmutableUser user = new ImmutableUser(1L, "john", "[email protected]");
System.out.println(user.getUsername()); // john
// user.setUsername("new"); // Compile error - no setter
// Using @With for modification
ImmutableUser updatedUser = user.withTemporaryPassword("temp123");
System.out.println(updatedUser.getTemporaryPassword()); // temp123
// Builder with @Value
ImmutableProduct product = ImmutableProduct.builder()
.id("P001")
.name("Smartphone")
.description("Latest model")
.price(699.99)
.available(true)
.build();
System.out.println(product);
}
}
Logging Annotations
import lombok.extern.slf4j.Slf4j;
import lombok.extern.java.Log;
import lombok.extern.log4j.Log4j2;
import lombok.Cleanup;
import java.io.FileInputStream;
import java.util.logging.Logger;
@Slf4j
public class LoggingService {
public void processData(String data) {
log.trace("Entering processData with: {}", data);
if (data == null) {
log.warn("Null data received");
return;
}
try {
// Automatic resource management
@Cleanup FileInputStream input = new FileInputStream("config.properties");
// FileInputStream will be automatically closed at the end of the scope
log.info("Processing data: {}", data);
// Complex processing logic here
log.debug("Data processing completed successfully");
} catch (Exception e) {
log.error("Error processing data: {}", data, e);
}
}
}
@Log4j2
public class AdvancedLoggingService {
public void performOperation() {
log.info("Operation started");
try {
// Business logic
if (log.isDebugEnabled()) {
log.debug("Detailed debug information");
}
} catch (Exception e) {
log.error("Operation failed", e);
throw new RuntimeException("Operation failed", e);
}
log.info("Operation completed successfully");
}
}
// For java.util.logging
@Log
public class JavaUtilLoggingExample {
private static final Logger logger = Logger.getLogger(JavaUtilLoggingExample.class.getName());
public void demo() {
log.info("This uses java.util.logging");
}
}
Lombok in Spring Applications
Spring Boot Entities with Lombok
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
// JPA Entity example
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class UserEntity {
@Id
@EqualsAndHashCode.Include
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(min = 3, max = 50)
@Column(unique = true, nullable = false)
private String username;
@Email
@NotBlank
@Column(unique = true, nullable = false)
private String email;
@NotBlank
@Size(min = 6)
@Column(nullable = false)
private String password;
@Builder.Default
private boolean enabled = true;
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
// Custom getter
public String getDisplayName() {
return username + " (" + email + ")";
}
}
// MongoDB Document example
@Document(collection = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductDocument {
@Id
private String id;
private String name;
private String description;
private Double price;
@Builder.Default
private Boolean active = true;
@Singular
private List<String> tags;
}
// Spring Service with Lombok
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
public UserEntity createUser(CreateUserRequest request) {
log.info("Creating user with username: {}", request.getUsername());
UserEntity user = UserEntity.builder()
.username(request.getUsername())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.build();
UserEntity savedUser = userRepository.save(user);
log.info("User created successfully with ID: {}", savedUser.getId());
return savedUser;
}
@Transactional
public UserEntity updateUser(Long userId, UpdateUserRequest request) {
return userRepository.findById(userId)
.map(user -> {
user.setEmail(request.getEmail());
if (request.getPassword() != null) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
return userRepository.save(user);
})
.orElseThrow(() -> new UserNotFoundException(userId));
}
}
// DTOs with Lombok
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
@NotBlank
private String username;
@Email
@NotBlank
private String email;
@NotBlank
@Size(min = 6)
private String password;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateUserRequest {
@Email
private String email;
@Size(min = 6)
private String password;
}
Configuration and Customization
Lombok Configuration File
# lombok.config # Enable experimental features lombok.anyConstructor.suppressConstructorProperties = true # Add null checks in generated setters lombok.setter.flagUsage = WARNING # Make generated builders public lombok.accessors.chain = true # Configure logging lombok.log.fieldName = LOG # Exclude fields from toString lombok.toString.exclude = [password, ssn] # Customize field names lombok.accessors.prefix += m_
Customizing Lombok Behavior
import lombok.AccessLevel;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
// Field defaults for entire class
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@ToString
public class ConfigurationExample {
String name; // Becomes: private final String name;
int value; // Becomes: private final int value;
// No need to explicitly declare them as private final
}
// Customizing toString
@ToString(callSuper = true,
includeFieldNames = false,
exclude = {"password", "secretKey"})
public class SecureUser extends BaseEntity {
private String username;
private String email;
private String password;
private String secretKey;
// toString will include superclass and exclude password/secretKey
}
// Customizing equals and hashCode
@EqualsAndHashCode(callSuper = true,
onlyExplicitlyIncluded = true)
public class Employee extends Person {
@EqualsAndHashCode.Include
private String employeeId;
private String department;
private double salary;
// Only employeeId is included in equals/hashCode
}
Best Practices and Patterns
Effective Lombok Usage
import lombok.*;
import lombok.experimental.Accessors;
// Builder with accessors chain
@Getter
@Setter
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ChainableUser {
private Long id;
private String username;
private String email;
// Enables method chaining: user.setId(1L).setUsername("john").setEmail("[email protected]");
}
// Immutable configuration class
@Value
@Builder
public class AppConfig {
String databaseUrl;
String username;
String password;
int connectionTimeout;
@Builder.Default
int maxConnections = 10;
@Builder.Default
boolean cacheEnabled = true;
@Singular
List<String> allowedHosts;
}
// Service with constructor injection
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderProcessingService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
private final AppConfig appConfig;
@Transactional
public Order processOrder(OrderRequest request) {
log.info("Processing order for customer: {}", request.getCustomerEmail());
Order order = Order.builder()
.orderNumber(generateOrderNumber())
.customerEmail(request.getCustomerEmail())
.totalAmount(request.getTotalAmount())
.items(request.getItems())
.build();
Order savedOrder = orderRepository.save(order);
// Process payment
paymentService.processPayment(savedOrder);
// Send notification
notificationService.sendOrderConfirmation(savedOrder);
log.info("Order processed successfully: {}", savedOrder.getOrderNumber());
return savedOrder;
}
private String generateOrderNumber() {
return "ORD_" + System.currentTimeMillis();
}
}
// Exception classes with Lombok
@Getter
public class BusinessException extends RuntimeException {
private final String errorCode;
private final LocalDateTime timestamp;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
}
}
// Record-like classes with Lombok (pre-Java 16)
@Value
@Builder
public class Point {
int x;
int y;
public double distanceTo(Point other) {
return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2));
}
}
Common Pitfalls and Solutions
Avoiding Lombok Issues
// Problem: Circular dependencies in equals/hashCode
@Data
public class Department {
private Long id;
private String name;
private List<Employee> employees; // Circular reference!
}
@Data
public class Employee {
private Long id;
private String name;
private Department department; // Circular reference!
}
// Solution: Use @EqualsAndHashCode.Exclude
@Data
public class Department {
private Long id;
private String name;
@EqualsAndHashCode.Exclude
private List<Employee> employees;
}
@Data
public class Employee {
private Long id;
private String name;
@EqualsAndHashCode.Exclude
private Department department;
}
// Problem: JPA relationships with Lombok
@Entity
@Data
public class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private List<OrderItem> items = new ArrayList<>();
// Helper method for bidirectional relationship
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
}
@Entity
@Data
public class OrderItem {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "order_id")
@ToString.Exclude
private Order order;
private String productName;
private int quantity;
}
Testing with Lombok
Test Classes with Lombok
import lombok.val;
import lombok.var;
@Slf4j
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUserSuccessfully() {
// given
val request = CreateUserRequest.builder()
.username("testuser")
.email("[email protected]")
.password("password123")
.build();
val encodedPassword = "encodedPassword123";
val savedUser = UserEntity.builder()
.id(1L)
.username("testuser")
.email("[email protected]")
.password(encodedPassword)
.build();
when(passwordEncoder.encode("password123")).thenReturn(encodedPassword);
when(userRepository.save(any(UserEntity.class))).thenReturn(savedUser);
// when
val result = userService.createUser(request);
// then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getUsername()).isEqualTo("testuser");
verify(userRepository).save(any(UserEntity.class));
verify(passwordEncoder).encode("password123");
}
@Test
void shouldHandleUserNotFound() {
// given
val userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// when & then
assertThrows(UserNotFoundException.class, () ->
userService.updateUser(userId, UpdateUserRequest.builder().build()));
}
}
// Using val and var for local variables
class TypeInferenceExample {
public void demonstrateValVar() {
val immutableList = List.of("a", "b", "c"); // final List<String>
var mutableList = new ArrayList<String>(); // ArrayList<String>
// val is final, so this won't compile:
// immutableList = new ArrayList<>();
// var can be reassigned
mutableList = new ArrayList<>(List.of("x", "y", "z"));
for (val item : immutableList) {
System.out.println(item); // item is final String
}
}
}
Conclusion
Lombok significantly reduces Java boilerplate code and improves developer productivity. Key benefits include:
- Reduced Code Volume: Eliminates repetitive getters, setters, constructors
- Improved Readability: Focus on business logic rather than boilerplate
- Fewer Bugs: Auto-generated methods are consistent and well-tested
- Better Maintenance: Changes in fields automatically reflect in generated methods
When to Use Lombok:
- Entities and DTOs: Perfect for data classes
- Configuration Classes: Great for builder patterns
- Service Classes: Constructor injection with
@RequiredArgsConstructor - Testing: Use
valfor cleaner test code
When to Be Cautious:
- JPA Entities: Be careful with
@ToStringand@EqualsAndHashCode - Inheritance: Configure
callSuperproperly - Complex Business Logic: Sometimes explicit code is clearer
Lombok works best when used consistently across a project and when team members are familiar with its features and limitations.