The Hidden Costs of Lombok in Enterprise Java Solutions
Should enterprises consider Lombok a hero or a villain in disguise? Explore Lombok's benefits and potential drawbacks for enterprise Java solutions.
Join the DZone community and get the full member experience.
Join For FreeImagine inheriting a codebase where classes are clean and concise, and developers don't have to worry about boilerplate code because they can get automatically generated getters, setters, constructors, and even builder patterns. Meet Lombok, a library used for accelerating development through "cleaning" boilerplate code and injecting it automatically during compile time. But, is Lombok a hero or a villain in disguise?
Let's explore the widespread perceived benefits and potential drawbacks of its adoption in enterprise Java solutions.
Overview
Enterprise software is designed as a stable and predictable solution. When adopting Lombok, a framework that modifies code at compile time, we are navigating towards the opposite direction, through seas of unpredictable results and hidden complexities. It's a choice that may put an enterprise application's long-term success at risk.
Architects have the tough responsibility of making decisions that will reflect throughout a software's life cycle - from development to sustaining. During development phases, when considering ways to improve developer productivity, it's crucial to balance the long-term impacts of each decision on the code complexity, predictability, and maintainability, while also considering that software will rely on multiple frameworks that must be able to correctly function with each other without incompatibilities that directly interfere on each other's behaviors.
Let's have a close look at different ways that Lombok is used, the common thoughts around it, and explore associated trade-offs.
In-Depth Look
Let's explore practical use cases and some developer's statements I've heard over the years, while we explore the aspects around the ideas.
“Lombok Creates Getters and Constructors, Saving Me Time on Data Classes”
Nowadays, we can use powerful IDEs and their code-generation features to create our getters, setters, and builders. It's best to use it to zealously and consciously generate code. Lombok's annotations can lead to unexpected mutability:
@Data
, by default, generates public setters, which violates encapsulation principles. Lombok offers ways to mitigate this through the usage of annotations such as@Value
and@Getters(AccessLevel.NONE)
, although this is an error-prone approach, as now your code is "vulnerable by default," and it's up to you to remember to adjust this every time.
Given the fact that code generation to some degree reduces the thought processes during the implementation, these configurations can be overseen by developers who might happen to forget, or maybe who don't know enough about Lombok to be aware of this need.
@Builder
generates a mutable builder class, which can lead to inconsistent object states.
Remember the quote from Joshua Bloch in his book, Effective Java:
"Classes should be immutable unless there's a very good reason to make them mutable."
See an example of an immutable class, which is not an anemic model:
public final class Customer { //final class
private final String id;
private final String name;
private final List<Order> orders;
private Customer(String id, String name, List<Order> orders) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.orders = List.copyOf(orders); // Defensive copy
}
public static Customer create(String id, String name, List<Order> orders) {
return new Customer(id, name, orders);
}
// Getters (no setters,for immutability)
public String getId() { return id; }
public String getName() { return name; }
public List<Order> getOrders() { return List.copyOf(orders); }
// Explicit methods for better control
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer)) return false;
Customer customer = (Customer) o;
return id.equals(customer.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
“Utility Classes and Exceptions Are a Breeze With Lombok”
Developers may often use Lombok to accelerate exception class creation:
@Getter
public class MyAppGenericException extends RuntimeException {
private final String error;
private final String message;
}
While this approach reduces boilerplate code, you may end up with overly generic exceptions and add difficulties for those wanting to create proper exception handling. A suggestion for a better approach is to create specific exception classes with meaningful constructors.
In this case, it's essential to keep in mind that, as discussed before, hidden code leads to reduced clarity and creates uncertainty on how exceptions should be used and extended properly.
In the example, if the service was designed to use the MyAppGenericException
as the main parent exception, developers would now rely on a base class that can be confusing since all constructors and methods are hidden. This particular characteristic may result in worse productivity in larger teams, as the level of understanding of Lombok will differ across developers, not to mention the increased complexity of new developers or code maintainers to understand how everything fits together.
For the reasons presented so far, Lombok's @UtilityClass
can also be misleading:
@UtilityClass
public class ParseUtils {
public static CustomerId parseCustomerId(String CustomerIdentifier) {
//...
}
}
Instead, a standard-based approach is recommended:
public final class ParseUtils {
private ParseUtils() {
throw new AssertionError("This class should not be instantiated.");
}
public static CustomerId parseCustomerId(String CustomerIdentifier) {
//...
}
}
"Logging Is Effortless With @Slf4j in My Classes"
Another usage of the auto-generation capabilities of Lombok is for boilerplate logging setup within classes through the @Slf4j
annotation :
@Slf4j
public class MyService {
public void doSomething() {
log.info("log when i'm doing something");
}
}
You have just tightly coupled the implementation of logging capabilities using a particular framework (Slf4j) with your code implementation. Instead, consider using CDI for a more flexible approach:
public class SomeService {
private final Logger logger;
public SomeService(Logger logger) {
this.logger = Objects.requireNonNull(logger);
}
public void doSomething() {
logger.info("Doing something");
}
}
“Controlling Access and Updating an Attribute To Reflect a DB Column Change, for Example, Is Way Simpler With Lombok”
Developers argue that addressing some types of changes in the code can be way faster when not having boilerplate code. For example, in Hibernate entities, changes in database columns could reflect updating the code of attributes and getters/setters. Instead of tightly coupling the database and code implementation (e.g., attribute name and column name), consider alternatives that provide proper abstraction between these two layers, such as the Hibernate annotations for customizing column names. Finally, you may also want better control over the persistence behaviors instead of hidden generated code.
Another popular annotation in Lombok is @With
. It's used to create a deep copy of an object and may result in excessive object creation, without any validation of business rules.
“@Builder Simplifies Creating and Working With Complex Objects”
Oversimplified domain models and anemic models are expected results for projects that rely on Lombok. On the generation of the equals
, hashcode
and toString
methods, be aware of the following:
@EqualsAndHashCode
may conflict with entity identity in JPA, resulting in unexpected behaviors on the comparison between detached entities, or collections' operations.
@Entity
@EqualsAndHashCode
public class Order {
@Id
@GeneratedValue
private Long id;
private String orderNumber;
// Other fields...
}
@Data
automatically createstoString()
methods that by default expose all attributes, including, sensitive information. Consider carefully implementing these methods based on domain requirements:
@Entity
public class User {
@Id
private Long id;
private String username;
private String passwordHash; // Sensitive information
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "'}";
// Note: passwordHash is deliberately excluded
}
}
“Lombok Lets Me Use Inheritance, Unlike Java Records”
It's true that when using records, we can't use hierarchy. However, this is a limitation that often has us delivering better code design. Here's how to address this need through the use of composition and interfaces:
public interface Vehicle {
String getRegistrationNumber();
int getNumberOfWheels();
}
public record Car(String registrationNumber, String model) implements Vehicle {
@Override
public int getNumberOfWheels() {
return 4;
}
}
public record Motorcycle(String registrationNumber, boolean hasSidecar) implements Vehicle {
@Override
public int getNumberOfWheels() {
return hasSidecar ? 3 : 2;
}
}
“Lombok Streamlines My Build Process and Maintenance”
Lombok's magic comes at the cost of code clarity and goes against SOLID principles:
- Hidden implementation: You can not see the generated methods in the source code. Developers may face challenges to fully understanding all the class' behaviors without dedicating time to learn how Lombok works behind the scenes.
- Debugging complications: Debugging the code may not work consistently as the source code you have often is not a reflection of the behavior in runtime.
Final Thoughts
"The ratio of time spent reading versus writing is well over 10 to 1... Making it easy to read makes it easier to write."
- Robert C. Martin, Clean Code
While Lombok offers short-term productivity gains, its use in enterprise Java development introduces significant risks to code maintainability, readability, and long-term project health. To avoid the challenges we've explored that derive from Lombok usage, consider alternative options that give you much higher chances of creating more stable, maintainable, and predictable code.
Developers who seek to deliver successful, long-term, enterprise software projects in critical domains have higher chances to succeed in their endeavors by embracing best practices and good principles of Java development for creating robust, maintainable, and secure software.
Learn More
- "Unraveling Lombok’s Code Design Pitfalls: Exploring the Pros and Cons," Otavio Santana
Opinions expressed by DZone contributors are their own.
Comments