Spring’s @FieldDefaults and @Data: Lombok Annotations for Cleaner Code

Alexander Obregon
10 min readSep 5, 2023
Image Source

Introduction

As Java developers, we often find ourselves bogged down by boilerplate code. Accessor methods, mutator methods, constructors, equals(), hashCode(), and toString() are essential but take up a lot of space and distract from the core logic of the application. The Spring Framework, widely used for building enterprise applications, is a fan of reducing boilerplate. However, even with Spring, a certain amount of it is unavoidable—unless we introduce Project Lombok into the equation.

Project Lombok provides annotations that can drastically minimize boilerplate code, improving readability and maintainability. Among these annotations, @FieldDefaults and @Data are commonly used, and this post will deep-dive into these two, demonstrating their usefulness in a Spring application.

Introduction to Lombok

Project Lombok is a library that has significantly impacted the Java ecosystem by reducing the repetitive and mundane code that developers often have to write. While Java is a powerful and versatile language, it’s often criticized for its verbosity, especially when compared to more modern languages like Kotlin or Python. Developers frequently have to write a lot of “ceremonial” code just to make simple things work, such as creating Plain Old Java Objects (POJOs) with their associated getters, setters, equals(), hashCode(), and toString() methods.

What Lombok Aims to Solve

Java’s verbosity isn’t just a matter of aesthetics; it can directly impact the productivity of a development team. Writing boilerplate code is time-consuming and increases the likelihood of errors. Imagine a class with numerous fields, each requiring its own getter and setter method. The situation quickly turns into a maintenance nightmare, particularly when you start adding methods like equals() and hashCode() which should be consistent with each other and updated every time the class fields change.

This is where Lombok comes into play. By providing a set of annotations, Lombok automatically generates code at compile-time, sidestepping the verbosity and potential for human error. The end result is a more readable and maintainable codebase, which is easier to understand, debug, and extend.

How Lombok Works

Lombok works by utilizing the annotation processing tool available in the Java Compiler. When your code is compiled, Lombok scans for its annotations. Upon finding them, it generates the corresponding code, which gets incorporated into your .class files. Essentially, the Java Compiler sees a version of your class that looks like you wrote all of the boilerplate code by hand, even though you didn't actually write it.

This method of operation has both benefits and drawbacks. On the upside, you don’t have to worry about the runtime overhead associated with reflection-based approaches. On the downside, the generated code is not visible in your source files, which can be confusing for those unfamiliar with Lombok.

Setting Up Lombok in Your Project

Integrating Lombok into a Spring project is a straightforward process. If you’re using Maven, you can add the following dependency to your pom.xml:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version> <!-- Use the latest version -->
</dependency>

Or, if you’re using Gradle, include this in your build.gradle:

dependencies {
compileOnly 'org.projectlombok:lombok:1.18.22' // Use the latest version
annotationProcessor 'org.projectlombok:lombok:1.18.22'
}

You will also need to install the Lombok plugin in your IDE to ensure that it recognizes Lombok annotations and generates the appropriate code at compile-time.

Popular Lombok Annotations

Lombok offers a variety of annotations designed to serve different purposes. Here are some popular ones:

  • @Getter and @Setter: Automatically generate getter and setter methods for fields.
  • @ToString: Generates a human-readable toString() method.
  • @EqualsAndHashCode: Generates both equals() and hashCode() methods based on the fields in your class.
  • @AllArgsConstructor, @NoArgsConstructor, @RequiredArgsConstructor: Generate constructors with different configurations.

The focus of this post, however, will be on the @FieldDefaults and @Data annotations, which are highly beneficial in the context of Spring applications.

The @Data Annotation

Project Lombok’s @Data annotation is essentially a Swiss Army knife for your Java classes. With a single annotation, you can automatically generate a variety of methods that you'd otherwise have to create manually—methods that are often straightforward but can quickly clutter your codebase. These include getter and setter methods, equals(), hashCode(), and toString() methods. While some may argue that a "one size fits all" approach is not ideal for every situation, there's no denying that @Data is extremely useful in many scenarios, particularly for POJOs—Plain Old Java Objects that act as data carriers within an application.

The Anatomy of @Data

When you annotate a class with @Data, Lombok generates:

  • Getter methods for all non-static fields
  • Setter methods for all non-final, non-static fields
  • An equals() method that checks for field-by-field equality
  • A hashCode() method that calculates hash code based on the fields
  • A toString() method that returns a string representation of the object

The annotation is meta-annotated with other Lombok annotations such as @Getter, @Setter, @ToString, and @EqualsAndHashCode. Therefore, using @Data is equivalent to using all of these annotations simultaneously.

Here’s a simple example to illustrate what @Data does.

Without Lombok:

public class Book {
private String title;
private String author;
private int pages;

public Book() {
}

public Book(String title, String author, int pages) {
this.title = title;
this.author = author;
this.pages = pages;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public int getPages() {
return pages;
}

public void setPages(int pages) {
this.pages = pages;
}

@Override
public boolean equals(Object o) {
// Implementation
}

@Override
public int hashCode() {
// Implementation
}

@Override
public String toString() {
// Implementation
}
}

With Lombok’s @Data:

import lombok.Data;

@Data
public class Book {
private String title;
private String author;
private int pages;
}

As you can see, the latter example is much cleaner and more focused, allowing you to concentrate on your application’s actual business logic rather than getting lost in boilerplate code.

Trade-offs and Considerations

While @Data is a powerful tool, it's essential to understand its limitations and consider when to use it or avoid it.

  1. Performance: The automatically generated equals() and hashCode() methods can be inefficient for large classes or complex hierarchies. In such cases, you might be better off with custom implementations.
  2. Immutability: @Data generates setters for all non-final fields, making the class mutable. If immutability is a requirement, you might not want to use this annotation or consider using it in conjunction with @FieldDefaults to make fields final.
  3. Overhead: With @Data, it's easy to lose track of what's happening under the hood. Developers unfamiliar with Lombok may find it confusing to work with classes where significant functionality is hidden behind a single annotation.

Customizing Behavior

Lombok provides ways to customize the behavior of @Data. For instance, you can exclude certain fields from the generated equals() and hashCode() methods by marking them with @EqualsAndHashCode.Exclude. Similarly, you can customize your toString() representation by using @ToString.Exclude for fields you don't want to include.

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Data
public class CustomBook {
private String title;
private String author;

@EqualsAndHashCode.Exclude
@ToString.Exclude
private int pages;
}

In this modified example, the pages field is excluded from both the equals() and hashCode() methods as well as the toString() method.

In summary, the @Data annotation is an incredibly versatile tool for any Java developer's toolkit, especially those using the Spring Framework. By eliminating boilerplate code, it allows you to write more concise and readable code, although it does come with its own set of trade-offs that you need to consider carefully.

The @FieldDefaults Annotation

One of the less-discussed but equally powerful Lombok annotations is @FieldDefaults. This annotation provides a way to set default modifiers for the fields in a class. You can control the access level of fields and specify whether they should be final. This is particularly useful in Spring-based applications where you might want to establish consistent field-level access control without explicitly declaring it for each field.

What @FieldDefaults Does

When you annotate a class with @FieldDefaults, you can specify two key elements:

  1. Access Level: The visibility of the fields, such as PRIVATE, PROTECTED, PACKAGE, or PUBLIC. The default value is PRIVATE.
  2. Final: A boolean value specifying whether the fields should be final. The default is false.

By using this annotation, you can conveniently set the default behavior for all the fields in a class, thus promoting cleaner and more consistent code. The annotation can be particularly powerful when combined with other Lombok annotations, such as @Data.

Basic Usage

Here’s a simple example that shows how to use @FieldDefaults:

import lombok.experimental.FieldDefaults;
import lombok.AccessLevel;

@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class ImmutablePerson {
String name;
int age;
}

In this example, we’ve set all the fields to private and final. Without @FieldDefaults, you would have had to explicitly declare each field as private final.

Combining with Other Annotations

You can often see @FieldDefaults being used along with @Data in Spring applications. While @Data provides a wealth of functionalities like getters, setters, and equals() and hashCode() methods, @FieldDefaults ensures that all the fields are private and potentially immutable. This can create a robust model class that is both succinct and secure.

import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.AccessLevel;

@Data
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class SecureBook {
String title;
String author;
int pages;
}

In this modified Book class example, not only do we have all the functionalities provided by @Data, but we also make sure that all fields are private and final, thereby enhancing the class's immutability and encapsulation.

A Note on the makeFinal Attribute

Setting makeFinal = true works well when you are creating immutable classes, but it does limit the functionality of other Lombok annotations like @Setter. In classes annotated with @Data, the makeFinal = true setting will essentially disable the generation of setter methods, because final fields cannot be modified once initialized.

Therefore, when using @FieldDefaults in combination with @Data, you must consider whether immutability is more critical than the ability to change the field values.

The @FieldDefaults annotation provides an elegant way to establish default field-level settings for a Java class. While the annotation may seem simple, its impact on code readability and maintainability can be significant. When used wisely, particularly in combination with other Lombok annotations like @Data, @FieldDefaults can help you create cleaner, more efficient code with fewer lines and better consistency.

When to Use and When Not to Use

Deciding when to use Lombok’s @Data and @FieldDefaults annotations is not a black-and-white decision. While these annotations offer a host of advantages, such as cleaner and more maintainable code, they come with their own set of considerations. Below, we break down some scenarios where using these annotations would be beneficial and some cases where you might want to proceed with caution.

When to Use @Data and @FieldDefaults

  1. Rapid Prototyping: When you need to quickly develop prototypes or proofs of concept, these annotations can speed up your coding significantly.
  2. POJOs/Data Classes: When you have simple classes intended primarily for holding data, and you’re sure you’ll require all the methods that @Data generates, using this annotation is a no-brainer.
  3. Spring Boot Applications: For Spring Boot applications, which often value convention over configuration, using @Data and @FieldDefaults aligns well with the framework's philosophy of reducing boilerplate code.
  4. Short-lived Projects: In projects with a short lifespan where maintainability is not a huge concern, using these annotations can make the development process more efficient.
  5. Team Consistency: If everyone on the team is familiar with Lombok and its annotations, using @Data and @FieldDefaults can make the code more uniform and easier to understand.

When Not to Use @Data and @FieldDefaults

  1. Complex Business Logic: For classes that have complex business logic, using @Data can be dangerous as it generates setters that could bypass your logic. In these scenarios, manually writing methods can give you greater control.
  2. Immutability: If your application requires immutable objects, be cautious with @Data as it generates setters by default. You could still use @FieldDefaults(makeFinal = true) to make fields final, but this would make the @Data annotation's setter-generation feature moot.
  3. Large Team with Varying Skill Levels: In a team where not everyone is familiar with Lombok, using these annotations might introduce a learning curve and could make the codebase harder to understand for newcomers.
  4. Library Development: If you’re developing a library intended for public or broad internal use, using Lombok annotations could force dependencies or unexpected behaviors on your users.
  5. Performance-Critical Code: In scenarios where performance is key, auto-generated methods may not be as optimized as manually written ones, especially for operations like equals() and hashCode() on large objects.
  6. Inheritance and Polymorphism: @Data generates equals() and hashCode() methods that might not adhere to the contract required when extending a class or implementing an interface that has its own equals() and hashCode() implementations. In such cases, writing these methods manually is advised.

While @Data and @FieldDefaults can significantly reduce boilerplate code and make your code cleaner, their usage should be carefully considered. They are excellent tools for simplifying development but are not a cure-all for every scenario. Knowing when to use these annotations—and perhaps more importantly, when not to use them—can be crucial for maintaining a balanced, efficient, and understandable codebase.

Conclusion

Project Lombok’s @FieldDefaults and @Data annotations can significantly reduce the boilerplate code in your Spring applications, making the code more readable and maintainable. However, one should be cautious about when and where to use these annotations. They are extremely beneficial for simple data classes but may not be suitable for classes that contain complex behavior or need custom implementations of methods like equals(), hashCode(), or toString().

By striking a balance, you can make the most out of these powerful annotations and write cleaner, more maintainable code.

  1. Lombok Official Documentation
  2. Java Official Documentation
Spring Boot icon by Icons8

--

--

Alexander Obregon

Software Engineer, fervent coder & writer. Devoted to learning & assisting others. Connect on LinkedIn: https://www.linkedin.com/in/alexander-obregon-97849b229/