Lombok vs. Records: Java Comparison
Our Tech Blog – Current and interesting topics about our work
In our tech blog, we share practical insights into IT, current trends and useful lessons from our daily work. Whether it is innovative software solutions, best practices in digitalization or reports from successful projects – here you will regularly find new articles that keep you up to date. Stop by and discover how we use technology to make businesses more efficient and future-proof.
Lombok vs. Java Records vs. Native implementation: A comparison
Introduction
In this post, we compare Lombok, Java Records and the native implementation in Java to help you make the best choice for your programming tasks. Each approach has its own strengths and weaknesses, which we will look at in detail.
Lombok
Lombok is a library that aims to reduce boilerplate code in Java applications. With Lombok, you can use simple annotations to automatically generate Getter, Setter, equals(), hashCode() and toString() methods.
More details about Lombok and its implementation
Below is an example of an implementation of a DTO (DataTransferObject) with builder patterns in Lobmok:
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.annotation.Nullable;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
@EqualsAndHashCode
@ToString
@Builder
public class LombokCustomerDTO {
//Dlombok
@Nullable
private Long id;
private String firstName;
private String lastName;
private String street;
private String zip;
private String city;
@Nullable
private String phone;
@Nullable
@EqualsAndHashCode.Exclude
@ToString.Exclude
private String mail;
}
Lombok annotations
| Annotation | Description |
|---|---|
@Data | Generates Getter, Setter, TOString(), equals() and hashCode() methods for all fields of the class. |
@Getter / @Setter | Generates getter and setter methods for the specified fields or the entire class. |
@NoArgsConstructor | Generates a standard no-arg constructor. |
@AllArgsConstructor | Generates an All-Arg constructor (constructor with all fields as arguments). |
@RequiredArgsConstructor | Generates a constructor for all final fields and for fields marked with @NonNull. |
@Builder | Implements the builder pattern for the class. |
@SneakyThrows | Used to throw tested exceptions without declaration in the method or use of try-catch. |
@Slf4j | Adds an SLF4J logger field to the class. |
@NonNull | Used to mark that a field cannot be zero; generates corresponding checks in generated methods. |
@Value | Makes the class immutable (all fields final) and generates corresponding methods as in @Data. |
Java Records
Java Records, introduced in Java 14, are special classes that represent immutable data structures. They offer simplified syntax and automatically generate methods such as equals() and hashCode().
More details about Java Records and its implementation
Below is an example of a Java Records implementation of a DTO (DataTransferObject) with builder patterns:
import static com.google.common.base.Preconditions.checkNotNull;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.annotation.Nullable;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record RecordCustomerDTO(Long id, String firstName, String lastName, String street, String zip, String city,
String phone, String mail) {
public RecordCustomerDTO(final Long id, final String firstName, final String lastName, final String street,
final String zip, final String city, final String phone, final String mail) {
this.id = id;
this.firstName = checkNotNull(firstName, "missing firstName");
this.lastName = checkNotNull(lastName, "missing lastName");
this.street = checkNotNull(street, "missing street");
this.zip = checkNotNull(zip, "missing zip");
this.city = checkNotNull(city, "missing city");
this.phone = phone;
this.mail = mail;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
@Nullable
private Long id;
private String firstName;
private String lastName;
private String street;
private String zip;
private String city;
@Nullable
private String phone;
@Nullable
private String mail;
public RecordCustomerDTO build() {
return new RecordCustomerDTO(this.id, this.firstName, this.lastName, this.street, this.zip, this.city,
this.phone, this.mail);
}
public Builder city(final String city) {
this.city = city;
return this;
}
public Builder firstName(final String firstName) {
this.firstName = firstName;
return this;
}
public Builder id(@Nullable final Long id) {
this.id = id;
return this;
}
public Builder lastName(final String lastName) {
this.lastName = lastName;
return this;
}
public Builder mail(@Nullable final String mail) {
this.mail = mail;
return this;
}
public Builder phone(@Nullable final String phone) {
this.phone = phone;
return this;
}
public Builder street(final String street) {
this.street = street;
return this;
}
public Builder zip(final String zip) {
this.zip = zip;
return this;
}
}
}
Native Implementation
The native implementation in Java requires you to implement all methods manually. This gives you full control but also means more writing effort and increased susceptibility to errors.
More details about native implementation
Below is an example of a native Java implementation of a DTO (DataTransferObject) with builder patterns:
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Objects;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.base.MoreObjects;
import jakarta.annotation.Nullable;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class NativeCustomerDTO {
public static Builder builder() {
return new Builder();
}
public static class Builder {
@Nullable
private Long id;
private String firstName;
private String lastName;
private String street;
private String zip;
private String city;
@Nullable
private String phone;
@Nullable
private String mail;
public Builder city(final String city) {
this.city = city;
return this;
}
public Builder firstName(final String firstName) {
this.firstName = firstName;
return this;
}
public Builder id(@Nullable final Long id) {
this.id = id;
return this;
}
public Builder lastName(final String lastName) {
this.lastName = lastName;
return this;
}
public Builder mail(@Nullable final String mail) {
this.mail = mail;
return this;
}
public Builder phone(@Nullable final String phone) {
this.phone = phone;
return this;
}
public Builder street(final String street) {
this.street = street;
return this;
}
public Builder zip(final String zip) {
this.zip = zip;
return this;
}
public NativeCustomerDTO build() {
return new NativeCustomerDTO(this);
}
}
@Nullable
private Long id;
private String firstName;
private String lastName;
private String street;
private String zip;
private String city;
@Nullable
private String phone;
@Nullable
private final String mail;
NativeCustomerDTO() {
this.id = null;
this.firstName = null;
this.lastName = null;
this.street = null;
this.zip = null;
this.city = null;
this.phone = null;
this.mail = null;
}
NativeCustomerDTO(final Builder builder) {
this.id = builder.id;
this.firstName = checkNotNull(builder.firstName, "missing firstName in builder");
this.lastName = checkNotNull(builder.lastName, "missing lastName in builder");
this.street = checkNotNull(builder.street, "missing street in builder");
this.zip = checkNotNull(builder.zip, "missing zip in builder");
this.city = checkNotNull(builder.city, "missing city in builder");
this.phone = builder.phone;
this.mail = builder.mail;
}
public Optional<Long> getID() {
return Optional.ofNullable(this.id);
}
public void setID(@Nullable final Long id) {
this.id = id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(final String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(final String lastName) {
this.lastName = lastName;
}
public String getStreet() {
return this.street;
}
public void setStreet(final String street) {
this.street = street;
}
public String getZIP() {
return this.zip;
}
public void setZIP(final String zip) {
this.zip = zip;
}
public String getCity() {
return this.city;
}
public void setCity(final String city) {
this.city = city;
}
public Optional<String> getPhone() {
return Optional.ofNullable(this.phone);
}
public void setPhone(final String phone) {
this.phone = phone;
}
public Optional<String> getMail() {
return Optional.ofNullable(this.mail);
}
@Override
public String toString() {
//@formatter:off
return MoreObjects.toStringHelper(this)
.add("id", this.id)
.add("firstName", this.firstName)
.add("lastName", this.lastName)
.add("street", this.street)
.add("zip", this.zip)
.add("city", this.city)
.add("phone", this.phone)
.add("mail", this.mail)
.toString();
// @formatter:on
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.firstName, this.lastName, this.street, this.zip, this.city, this.phone, this.mail);
}
@Override
public boolean equals(final Object other) {
if (other == this) {
return true;
}
if (!(other instanceof NativeCustomerDTO)) {
return false;
}
final NativeCustomerDTO nativeCustomerDTO = (NativeCustomerDTO) other;
return Objects.equals(this.id, nativeCustomerDTO.id)
&& Objects.equals(this.firstName, nativeCustomerDTO.firstName)
&& Objects.equals(this.lastName, nativeCustomerDTO.lastName)
&& Objects.equals(this.street, nativeCustomerDTO.street)
&& Objects.equals(this.zip, nativeCustomerDTO.zip)
&& Objects.equals(this.city, nativeCustomerDTO.city)
&& Objects.equals(this.phone, nativeCustomerDTO.phone)
&& Objects.equals(this.mail, nativeCustomerDTO.mail);
}
}
Comparison table
| Criterion | Lombok | Java Records | Native Implementation |
|---|---|---|---|
| Boilerplate code | significantly reduced | Automatic generation, very reduced | Maybe manual code |
| Flexibility | Means (adjustable by annotations) | Restricted (determined structure) | Very high (full control) |
| Understandability | May contain hidden functionality | Clear and simple | Direct and transparent |
| Tooling | Requires Lombok plugin and configuration | Part of the JDK from Java 16, no extras required | No additional tools required |
| Learning curve | Requires understanding of annotations | Simple with understanding immutability | No specific knowledge required |
| Immutability | Not standard | Standard unchangeable | Depending on the implementation |
| Adaptability | Some adjustments possible | No adaptation of automatic methods | Fully adaptable |
| Thread security | Depending on the implementation | Inherently thread-safe through immutability | Depending on the implementation |
| Additional dependencies | Requires Lombok Library | No additional dependencies | No additional dependencies |
| Compatibility | Can conflict with other tools | Good compatibility in modern Java | Universally compatible |
| Maintenance | Less maintenance effort | Minimum maintenance effort | Higher maintenance costs for changes |
Conclusion
In summary, the choice between Lombok, Java Records and the native implementation depends on your specific requirements and preferences. Each approach has its advantages and disadvantages, which should be considered.