Introduction of Java Records

Mohit jainMohit jain
3 min read

Records are another modern Java feature (finalised in Java 16) that dramatically reduce boilerplate when modelling simple immutable data carriers.


1. What problem do records solve?

In Java 8, if you wanted a class that just holds data (say User with id and name), you had to write:

public class User {
    private final String id;
    private final String name;

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

    public String getId() { return id; }
    public String getName() { return name; }

    @Override
    public boolean equals(Object o) { /* tedious */ }
    @Override
    public int hashCode() { /* tedious */ }
    @Override
    public String toString() { /* tedious */ }
}

That’s a lot of boilerplate for something that’s just a tuple of values.


2. Enter Records (Java 16+)

A record is a special class that is:

  • immutable by default (all fields are final)

  • provides canonical constructor, equals, hashCode, and toString automatically

  • compact to declare

Example:

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

This line generates:

  • private final fields id, name

  • constructor User(String id, String name)

  • accessors id(), name()

  • equals, hashCode, toString

So instead of 50+ lines, you get 1.


3. When to use

  • Data transfer objects (DTOs) in REST APIs

  • Events in event-driven systems (e.g., PaymentCreated, UserRegistered)

  • Keys in maps/sets (they get proper equals/hashCode)

  • Immutable configs


4. Advanced features

  • Custom constructors

1. Canonical Constructor

  • A canonical constructor matches all record components.

  • Exists by default (generated by compiler).

  • You can override it to add validation or normalisation.

A. Explicit canonical constructor:

public record User(String id, String name) {
    public User(String id, String name) {
        if (id == null || id.isBlank())
            throw new IllegalArgumentException("id required");
        this.id = id;
        this.name = name.trim();
    }
}

B. Compact canonical constructor:

public record User(String id, String name) {
    public User {
        if (id == null || id.isBlank())
            throw new IllegalArgumentException("id required");
        name = name.trim(); // compiler auto-assigns to this.name
    }
}

2. Non-Canonical (Overloaded) Constructors

  • Provide extra ways to create an object (convenience).

  • Must delegate to the canonical constructor using this(...).

  • Useful for default values or alternative parameter types.

Example:

public record Point(int x, int y) {

    // default y = 0
    public Point(int x) {
        this(x, 0); // must delegate
    }

    // convert from double
    public Point(double x, double y) {
        this((int) x, (int) y);
    }
}
  • Additional methods
    You can add extra behavior:
public record Point(int x, int y) {
    public int distanceFromOrigin() {
        return (int) Math.sqrt(x*x + y*y);
    }
}
  • Implements interfaces
public record CardPayment(String number, double amount) implements PaymentMethod { }
  • Pattern matching synergy: Works seamlessly with switch/instanceof (especially with sealed hierarchies).

5. Limitations

  • Records are implicitly final: you cannot extend them.

  • All fields are shallowly immutable (the reference is final, but if you store a mutable object, it can still be changed).

  • Not a replacement for full domain models—best used for data carriers.

6. Example with flow

public record Employee(String name, int age) {

    // Canonical constructor (with validation)
    public Employee {
        if (age < 18) throw new IllegalArgumentException("Too young");
        name = name.toUpperCase();
    }

    // Non-canonical constructor (default age)
    public Employee(String name) {
        this(name, 25); // calls canonical constructor
    }
}

public class Main {
    public static void main(String[] args) {
        new Employee("Alice");   // calls non-canonical, which calls canonical
        new Employee("Bob", 30); // calls canonical directly
    }
}

Execution order:

  1. new Employee("Alice") → non-canonical → this(name, 25) → canonical runs validation/normalization → object created.

  2. new Employee("Bob", 30") → canonical runs directly.

1
Subscribe to my newsletter

Read articles from Mohit jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohit jain
Mohit jain

Oracle‬‭ Certified‬‭ Java‬‭ Developer‬‭ with‬‭ 7+ years‬‭ of‬‭ experience‬‭ specializing‬‭ in‬‭ backend‬‭ development,‬‭ microservices‬ architecture,‬‭ and‬‭ cloud-based‬‭ solutions.‬‭ Proven‬‭ expertise‬‭ in‬‭ designing‬‭ scalable‬‭ systems,‬‭ optimizing‬‭ performance,‬‭ and‬ mentoring‬‭ teams‬‭ to‬‭ enhance‬‭ productivity.‬‭ Passionate‬‭ about‬‭ building‬‭ high-performance‬‭ applications‬‭ using‬‭ Java,‬‭ Spring‬ Boot, Kafka, and cloud technologies (AWS/GCP)