Java Records Aren’t Necessarily Evil

How do Java Records fit to the object-oriented design?


Records are likely the most discussed feature new in Java 14. At the same time, there is a lot of criticism due to their non-object-oriented nature. A typical argument says that records are a concept from procedural programming and have no place in an object-oriented language.

Do records really encourage procedural rather than object thinking?

Well, yes and no. I totally agree that records are no object-oriented feature, on the other hand, I believe there are valid use-cases for them even in perfectly object-oriented applications.

The reason is, there are no true objects at the boundaries. Consider for instance a REST resource, database entity or configuration properties. Those are all mere data structures, but still need to have their representations in Java code.

Evolution of Records

Of course, we have data structure types such as Map or Set in Java, and they are sufficient for a lot of cases. Sometimes, however, it is beneficial to have a typed data structure — for declarative validations, marshaling or mapping, for example. Traditionally, the Data Transfer Objects (DTO) are used in such scenarios:

class AccountData {

  private String username;
  private String password;
  private String email;

  public AccountData(
      String username, String password, String email) {
    this.username = username;
    this.password = password;
    this.email = email;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    AccountData that = (AccountData) o;
    return username.equals(that.username) &&
        password.equals(that.password) &&
        email.equals(that.email);
  }
  @Override
  public int hashCode() {
    return Objects.hash(
        username, password, email);
  }
}

Well, this is really a lot of boilerplate code for such a simple thing. You can hate it, you can love it, but Lombok is addressing exactly this gap quite elegantly:

@Data
class AccountData {

  private String username;
  private String password;
  private String email;
}
Lombok is based on code-manipulations, which could be seen as inadequate “black magic” a lot of developers don’t want to have in their codebases. Java 14 solves this problem with the records:
record AccountData (
    String username,
    String password,
    String email) {}
Any resemblance to Kotlin’s data classes is purely coincidental:
data class AccountData (
    var username: String,
    var password: String,
    var email: String)

The Problem with DTOs

So, what’s the problem here? Java records seem to introduce DTOs as a language feature. What’s the problem with DTOs? The “O” is. The Data Transfer Objects are no objects at all! A DTO is just a typed data structure.

Code should be as explicit as possible and data structures should not be called objects. This fallacy leads to a bad object design, usually to the infamous Anemic Domain Model.

On the other hand, records (and even the Lombok’s @Data annotation) have no "object" in the name. Records make it explicit, data is treated as data.

Conclusion

Java records are a new feature that brings an additional data structure type into the language. In contrast to DTOs, records make data structures in code explicit. They are right tools for right purposes. Being used incorrectly they are evil servants like everything else could be, too.

There are much more dangerous common practices in Java such as null references, static methods and global constants, but that’s a different story...

Happy recording!