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

AnnotationDescription
@DataGenerates Getter, Setter, TOString(), equals() and hashCode() methods for all fields of the class.
@Getter / @SetterGenerates getter and setter methods for the specified fields or the entire class.
@NoArgsConstructorGenerates a standard no-arg constructor.
@AllArgsConstructorGenerates an All-Arg constructor (constructor with all fields as arguments).
@RequiredArgsConstructorGenerates a constructor for all final fields and for fields marked with @NonNull.
@BuilderImplements the builder pattern for the class.
@SneakyThrowsUsed to throw tested exceptions without declaration in the method or use of try-catch.
@Slf4jAdds an SLF4J logger field to the class.
@NonNullUsed to mark that a field cannot be zero; generates corresponding checks in generated methods.
@ValueMakes 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

CriterionLombokJava RecordsNative Implementation
Boilerplate codesignificantly reducedAutomatic generation, very reducedMaybe manual code
FlexibilityMeans (adjustable by annotations)Restricted (determined structure)Very high (full control)
UnderstandabilityMay contain hidden functionalityClear and simpleDirect and transparent
ToolingRequires Lombok plugin and configurationPart of the JDK from Java 16, no extras requiredNo additional tools required
Learning curveRequires understanding of annotationsSimple with understanding immutabilityNo specific knowledge required
ImmutabilityNot standardStandard unchangeableDepending on the implementation
AdaptabilitySome adjustments possibleNo adaptation of automatic methodsFully adaptable
Thread securityDepending on the implementationInherently thread-safe through immutabilityDepending on the implementation
Additional dependenciesRequires Lombok LibraryNo additional dependenciesNo additional dependencies
CompatibilityCan conflict with other toolsGood compatibility in modern JavaUniversally compatible
MaintenanceLess maintenance effortMinimum maintenance effortHigher 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.