Java 5 min read

Lombok: Reduce Java Boilerplate Code

Complete guide to Project Lombok for Java. Learn how to eliminate boilerplate code with annotations like @Data, @Builder, and @RequiredArgsConstructor.

MR

Moshiour Rahman

Advertisement

Introduction

Java is powerful, but it can be verbose. Writing getters, setters, constructors, and equals/hashCode methods for every class is tedious. Enter Lombok - a library that generates this boilerplate code at compile time using annotations.

Project Setup

Using Spring Initializr

spring init --dependencies=lombok --package=com.techyowls lombok-demo

Gradle Configuration

Add to your build.gradle:

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

Maven Configuration

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

Note: Install the Lombok plugin in your IDE (IntelliJ IDEA, Eclipse, VS Code) for proper code completion and navigation.

@Data Annotation

The @Data annotation is a convenient shortcut that bundles several annotations together:

@Data
public class Person {
    private String name;
    private Integer age;
}

This single annotation generates:

  • @Getter for all fields
  • @Setter for all fields
  • @ToString
  • @EqualsAndHashCode
  • @RequiredArgsConstructor

Generated Code

public class Person {
    private String name;
    private Integer age;

    public Person() {}

    public String getName() { return this.name; }
    public Integer getAge() { return this.age; }

    public void setName(String name) { this.name = name; }
    public void setAge(Integer age) { this.age = age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person other = (Person) o;
        return Objects.equals(name, other.name)
            && Objects.equals(age, other.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person(name=" + name + ", age=" + age + ")";
    }
}

Individual Annotations

You can use these annotations separately for more control:

AnnotationPurpose
@GetterGenerates getter methods
@SetterGenerates setter methods
@ToStringGenerates toString() method
@EqualsAndHashCodeGenerates equals() and hashCode()
@NoArgsConstructorGenerates no-argument constructor
@AllArgsConstructorGenerates all-args constructor

Field-Level Usage

public class User {
    @Getter @Setter
    private String username;

    @Getter // Read-only field
    private final LocalDateTime createdAt = LocalDateTime.now();
}

@Builder Annotation

The @Builder annotation implements the Builder pattern - extremely useful for creating objects with many fields:

@Data
@Builder
public class Person {
    private String name;
    private Integer age;
    private String email;
    private String address;
}

Usage

Person person = Person.builder()
    .name("TechyOwls")
    .age(25)
    .email("hello@techyowls.io")
    .address("San Francisco")
    .build();

Generated Builder Class

public class Person {
    // fields...

    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    public static class PersonBuilder {
        private String name;
        private Integer age;
        private String email;
        private String address;

        public PersonBuilder name(String name) {
            this.name = name;
            return this;
        }

        public PersonBuilder age(Integer age) {
            this.age = age;
            return this;
        }

        // ... other builder methods

        public Person build() {
            return new Person(name, age, email, address);
        }
    }
}

Builder with Default Values

@Builder
public class HttpRequest {
    @Builder.Default
    private String method = "GET";

    @Builder.Default
    private int timeout = 5000;

    private String url;
}

// Usage
HttpRequest request = HttpRequest.builder()
    .url("https://api.example.com")
    .build(); // method="GET", timeout=5000

@RequiredArgsConstructor

This annotation generates a constructor with required arguments (final fields and fields with constraints):

The Problem: Constructor Injection Boilerplate

@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final AuditLogger auditLogger;
    private final NotificationService notificationService;
    private final CacheManager cacheManager;

    // Verbose constructor!
    @Autowired
    public UserService(UserRepository userRepository,
                       EmailService emailService,
                       AuditLogger auditLogger,
                       NotificationService notificationService,
                       CacheManager cacheManager) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.auditLogger = auditLogger;
        this.notificationService = notificationService;
        this.cacheManager = cacheManager;
    }
}

The Solution

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final AuditLogger auditLogger;
    private final NotificationService notificationService;
    private final CacheManager cacheManager;
}

That’s it! Lombok generates the constructor automatically. Since Spring 4.3+, @Autowired is optional for single-constructor classes.

Other Useful Annotations

@Slf4j - Logging Made Easy

@Slf4j
@Service
public class PaymentService {
    public void processPayment(Payment payment) {
        log.info("Processing payment: {}", payment.getId());
        // ... business logic
        log.debug("Payment processed successfully");
    }
}

Generates: private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

@Value - Immutable Classes

@Value
public class Money {
    BigDecimal amount;
    String currency;
}

Creates an immutable class with:

  • All fields are private final
  • All-args constructor
  • Getters (no setters)
  • equals(), hashCode(), toString()

@SneakyThrows - Checked Exception Handling

@SneakyThrows
public String readFile(String path) {
    return Files.readString(Path.of(path));
    // IOException is thrown sneakily!
}

Use with caution - bypasses checked exception handling.

Best Practices

  1. Use @Data for DTOs and entities - Perfect for simple data classes
  2. Prefer @Value for immutable objects - Thread-safe by design
  3. Use @Builder for complex objects - Especially with many optional fields
  4. Combine @RequiredArgsConstructor with final fields - Clean dependency injection
  5. Enable annotation processing in your IDE - For proper code navigation

Conclusion

Lombok significantly reduces boilerplate code in Java projects:

BeforeAfter
50+ lines for a simple POJO5 lines with @Data
Manual constructor injection@RequiredArgsConstructor
Builder pattern implementation@Builder

The library is widely adopted in the Java ecosystem and works seamlessly with Spring Boot, JPA, and other frameworks.

Key Takeaways:

  • Use @Data for most POJOs
  • Use @Builder for objects with many fields
  • Use @RequiredArgsConstructor for dependency injection
  • Install the IDE plugin for the best experience

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.