Published on

Java Records, Lombok and POJOs

Authors

For various reasons like carrying or storing data, deserializing/serializing api requests or simply organizing our code base, we use java entities called POJOs or Plain Old Java Objects. They usually have multiple variables with accessor methods and little/no business logic. In most cases, they are immutable and short-lived.

POJO

A simple POJO would typically have a public constructor, getters and setters, equals, hashCode and toString methods. Since these classes are used frequently, IDEs have shortcuts to create these methods. So it is actually not a big loss of time to create them. There is still the possibility of forgetting to update an equals or toString method after adding a new field though. Considering how many of these we have, it is still a lot of code to look at and manage.

public class User {

    public User(String id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    private String id;
    private String name;
    private String address;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User that = (User) o;
        return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(address, that.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, address);
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

Lombok

Lombok is an annotation processor which works at compile time to add getters, setters and constructors to the code. It is a pretty popular library since java is incredibly verbose compared to other languages and there is always a desire to have simpler and shorter code. Lombok has more features related to logging, builders, validations etc.

With this library however, we need to understand that the code we are working on is not syntactically correct until compilation. So we also need a plugin to tell our IDE everything is fine. There are less popular alternatives like Immutables and AutoValue and they use a more natural way of annotation processing.

This is fine

With Lombok, we could have the same functionality as the POJO above with a much shorter definition

@Getter
@Setter
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class User {
    private String id;
    private String name;
    private String address;
}

To make this even simpler, we can use the @Data annotation which is equivalent to @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode. We still need the @AllArgsConstructor to have a constructor with all variables though. Also, we might not want to have @Setters or a @NoArgsConstructor in many cases.

@Data
@AllArgsConstructor
public class User {
    private String id;
    private String name;
    private String address;
}

Records

Records are a relatively new addition to java that solves the same problem without any external libraries, annotation processing or plugins. You can refer to JEP-384 and JEP-359 for more context about this feature. Motivation, Goals and Non-Goals are pretty well-defined in these JEPs.

record User(String id, String name, String address) {
}

With this definition, we can create new User instances with the new keyword like a normal class. Similar to how @Data from Lombok works, we get public getters for all fields, equals, hashCode and toString methods. However, since every field is final, we do not have setters for records.

Designed to have a very specific role in java, they have some restrictions.

  • They only have private final fields for each parameter in their definition, no more instance fields
  • They can not extend any other class
  • They are implicitly final and cannot be abstract
  • They are designed to be immutable, so no setters

While records may not be suitable to work with Spring Data JPA since they are immutable, they work really well as API layer request and responses or messaging DTOs. They also work really well when you just need them to carry data across layers instead of passing down 5 parameters. Immutability is also an advantage to consider when dealing with records.

Compact Constructor

If we want our constructor to do something after initializing the final fields, we can use something called compact constructors. Note that we are not defining a typical constructor, this code block will be running before the actual compiled constructor, and we can use the parameters from the actual constructor itself.

record User(String id, String name, String address) {
    public User {
        if (StringUtils.isBlank(id) || StringUtils.isBlank(name)) {
            throw new ValidationException();
        }
    }
}

We can also use annotations like javax validations on records

record User(@NotBlank String id, @NotBlank String name, String address) {
}

Static Methods in Records

To have more customizations on the record, we can have a static factory method inside the body.

public record User(String id, String name, String address) {
    public static User create(String name, String address) {
        return new User(UUID.randomUUID().toString(), name, address);
    }
}

I think records are really useful in all places where immutability is not a concern. Using a native solution instead of libraries like Lombok is a good move for Java overall.