Java record class, What is it, when to use it and why.

Java record class vs lombok vs groovy

Java, a language known for its robustness and verbosity, has been making strides toward becoming more concise and developer-friendly. Java 14, released in March 2020, introduced a preview feature that takes a step in this direction: the Java record class, or keyword. I say class, as according to Oracle JEP 395, they refer to a record as a special type of class. This feature graduated to a standard feature in Java 16. Let’s delve into what this feature is and how it can enhance Java development. We’ll also look at alternatives to records provided by Lombok and Groovy.

What is a ‘record’?

A record in Java provides a compact syntax to declare classes that are supposed to be transparent holders for shallowly immutable data. For a class that primarily exists to hold data and requires little additional behavior, a record might be the perfect fit.

Let’s consider a common requirement, especially in web development where we work with ‘Users’.

In traditional Java, representing a User object with fields like id, firstName, lastName, email, dateOfBirth, address, and phoneNumber would require a substantial amount of boilerplate, for a small set of fields. This typically includes private fields, constructors, getters, setters, equals(), hashCode(), and toString().

The record keyword aims to simplify this. Here’s how you would define the User object using a record:

record User(UUID id, 
            String firstName, 
            String lastName, 
            String email, 
            LocalDate dateOfBirth, 
            Address address, 
            String phoneNumber) {}

With this concise syntax, you get:

  • A private final field for each component.
  • A public constructor.
  • Implementations for equals(), hashCode(), and toString().

Now let’s compare the equivalent using a traditional Java POJO (Plain Old Java Object).

import java.time.LocalDate;
import java.util.UUID;
import java.util.Objects;

public class User {

    private UUID id;
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate dateOfBirth;
    private Address address;  // Assuming Address is another class you've defined.
    private String phoneNumber;

    // Constructor
    public User(UUID id, String firstName, String lastName, String email, LocalDate dateOfBirth, Address address, String phoneNumber) { = id;
        this.firstName = firstName;
        this.lastName = lastName; = email;
        this.dateOfBirth = dateOfBirth;
        this.address = address;
        this.phoneNumber = phoneNumber;

    // Getters
    public UUID getId() {
        return id;

    public String getFirstName() {
        return firstName;

    public String getLastName() {
        return lastName;

    public String getEmail() {
        return email;

    public LocalDate getDateOfBirth() {
        return dateOfBirth;

    public Address getAddress() {
        return address;

    public String getPhoneNumber() {
        return phoneNumber;

    // Setters
    public void setId(UUID id) { = id;

    public void setFirstName(String firstName) {
        this.firstName = firstName;

    public void setLastName(String lastName) {
        this.lastName = lastName;

    public void setEmail(String email) { = email;

    public void setDateOfBirth(LocalDate dateOfBirth) {
        this.dateOfBirth = dateOfBirth;

    public void setAddress(Address address) {
        this.address = address;

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;

    // equals() and hashCode()
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, &&
               Objects.equals(firstName, user.firstName) &&
               Objects.equals(lastName, user.lastName) &&
               Objects.equals(email, &&
               Objects.equals(dateOfBirth, user.dateOfBirth) &&
               Objects.equals(address, user.address) &&
               Objects.equals(phoneNumber, user.phoneNumber);

    public int hashCode() {
        return Objects.hash(id, firstName, lastName, email, dateOfBirth, address, phoneNumber);

    // toString()
    public String toString() {
        return "User{" +
               "id=" + id +
               ", firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", email='" + email + '\'' +
               ", dateOfBirth=" + dateOfBirth +
               ", address=" + address +
               ", phoneNumber='" + phoneNumber + '\'' +

One can immediately see the benefits, in readability, maintainability, and time to produce such code.

Benefits of using a record

  1. Conciseness: Records allow developers to express data classes in a much more compact manner compared to POJOs.
  2. Readability: With reduced boilerplate code, the class’s intent is much clearer.
  3. Immutability: Records are inherently immutable, which is excellent for ensuring data safety.
  4. Safety: Automatic implementations of equals() and hashCode() are less prone to errors compared to manually-written ones.

When to use a record?

Use records when:

  • The primary purpose of the class is to communicate data.
  • The class doesn’t need to mutate its state.
  • You want automatic implementations of standard methods.

Avoid using records when:

  • You require extensive customization for methods like equals(), hashCode(), or toString().
  • The class needs to extend another class.

Java’s record class, demonstrated using the User object above, clearly offers a way to significantly reduce boilerplate, enhancing clarity and readability. For developers who’ve spent countless hours writing and maintaining POJOs, the record construct is a breath of fresh air. It’s essential, however, to understand its purpose and when to utilize it for maximum effectiveness.

Alternatives to the Java record class

While Java’s record class marks a significant stride towards reducing boilerplate and enhancing clarity in data classes, it isn’t the only player in the field. Over the years, developers have sought to minimize the verbosity inherent in Java through various means. Before record even saw the light of day, tools and languages on the JVM, such as Lombok and Groovy, offered their solutions to the same challenge. In this section, we’ll explore these alternatives, shedding light on their approach, advantages, and how they compare with the modern record.

Lombok @Data annotation

With Lombok’s @Data, getters, setters, equals(), hashCode(), and toString() are generated. You can further control the mutability using @Getter, @Setter individually or make it immutable with @Value.

Using Lombok, our User class would look something like:

public class User {
    private UUID id;
    private String firstName;
    private String lastName;
    private String email;
    private LocalDate dateOfBirth;
    private Address address;
    private String phoneNumber;

Groovy POGO (Plain Old Groovy Object)

Using Groovy, there’s even less. Our User object can be written as:

class User {
    UUID id
    String firstName
    String lastName
    String email
    LocalDate dateOfBirth
    Address address
    String phoneNumber

Just declaring the fields provides you with getters and setters. equals(), hashCode(), and toString() are also generated.

Java record class vs. Lombok

Java’s record feature and the Lombok library share a primary goal: to streamline the creation of Java data classes by removing excess boilerplate. Although they aim to achieve similar outcomes, the method, capabilities, and integration between the two are distinctly different.

Java Record:


  1. Inherent Feature: Being a native language feature means record doesn’t necessitate any external dependencies. This ensures compatibility and lessens concerns related to third-party library maintenance.
  2. Clarity: The record syntax is succinct and purpose-driven, making the intent of the class evident.
  3. Immutable by Default: Records inherently guarantee data immutability.
  4. Consistent Behavior: Auto-generated methods like equals(), hashCode(), and toString() are standardized, minimizing human error.


  1. Limited Customization: Records are primarily designed for pure data representation and aren’t suitable for classes that demand extensive behavior or custom methods.
  2. No Class Inheritance: Records cannot inherit from other classes, which can be restrictive for certain designs.



  1. Versatility: Lombok boasts a wide range of annotations, such as @Data, @Getter, @Setter, giving developers greater control over their classes.
  2. Optional Immutability: With Lombok, it’s possible to choose between mutable and immutable objects.
  3. Integration: Lombok is popular in the Java community and integrates well with many IDEs and build tools.


  1. Dependency Concerns: As an external library, introducing Lombok means adding another dependency, which could lead to potential maintenance challenges.
  2. Annotation Processor Issues: Lombok works through annotation processors during compile-time, which can occasionally conflict with other processors or produce IDE warnings.
  3. Learning Overhead: With its array of features and annotations, new users might find Lombok’s learning curve a bit steep.

Java Record Class Vs. Groovy POGO (Plain Old Groovy Object)

Groovy, as a language, was developed with the intention of simplifying Java’s verbosity. POGOs, or Plain Old Groovy Objects, are Groovy’s response to Java’s POJOs, offering developers a more concise way to define data objects.

Java Record:


  1. Standardization: As a built-in feature, record ensures uniformity across projects that use it, without external dependencies.
  2. Immutability Guarantee: The immutable nature of records ensures data consistency and safety.
  3. Directness: The structure and intent of a record are immediately obvious, simplifying its comprehension.


  1. No Behavior Customization: Records are tailored for data representation, not behavior-driven classes.
  2. No Inheritance: You can’t extend other classes with records.

Groovy POGO:


  1. Simplicity: Groovy inherently provides a less verbose syntax. With POGOs, defining properties automatically gives you getters, setters, equals(), hashCode(), and toString() implementations.
  2. Interoperability: Being a JVM language, Groovy integrates seamlessly with existing Java code.
  3. Flexibility: Groovy provides dynamic typing in addition to static typing, offering more flexibility in certain scenarios.


  1. Performance Overhead: Dynamic typing and other Groovy features can sometimes lead to a performance overhead compared to pure Java.
  2. External Dependency: Introducing Groovy means adding another language and its runtime to the project, which might not be ideal for all Java projects.
  3. Learning Curve: Developers familiar with Java may need some time to get accustomed to Groovy’s syntax and paradigms.


The record feature, Lombok, and Groovy POGO are all commendable tools that strive to enhance the Java development experience by curbing verbosity. Their applicability hinges on project needs, team familiarity, and the specific challenges at hand. While record is a direct Java feature focusing on conciseness and immutability, both Lombok and Groovy offer broader functionalities with their own sets of trade-offs. As always, the best tool is contingent on the context in which it’s applied.

We hope you have found this article useful and now have a better understanding of this topic. Please contact us for further details or assistance.

Founder of Tucanoo Solutions Ltd, a Cloud / Web Application development company. AWS Cloud Solutions Architect. Specialties: Spring Boot, Java, Grails, React.JS, App Architecture, Agile, Scrum, Git, AWS, Javascript.

Leave a Reply

Your email address will not be published. Required fields are marked *