Modern Java

33. Records vs Lombok

Records are the standard Java alternative to Lombok @Value; records cannot have mutable fields or extend classes, but they require no annotation processing and are a first-class language feature.

Java 16+ (records standard)

Explanation

Lombok @Value generates an immutable class with final fields, a constructor, getters, equals(), hashCode(), and toString() via annotation processing. Records do the same natively, with zero tooling dependencies.

Records cannot: extend other classes (they extend Record implicitly), have mutable (non-final) fields, or have setter methods. Records can: implement interfaces, have custom methods, use generic type parameters, and appear in switch patterns and deconstruction patterns.

Lombok has some features records do not: @Builder (rich builder generation), @Data (mutable data class), @SuperBuilder (inheritance-aware builders), and @EqualsAndHashCode(callSuper=true). For complex mutable DTOs, Lombok still has a role. For pure immutable value types, prefer records โ€” no IDE plugin, no annotation processor, less magic.

Example

// Lombok (hypothetical โ€” requires Lombok dependency):
// @Value
// public class LombokUser {
//     String name;
//     int age;
// }
// LombokUser u = new LombokUser("Alice", 30);
// u.getName(); u.getAge(); u.equals(...); u.hashCode(); u.toString();

public class RecordsVsLombok {
    // Native record: same result, no dependency
    record User(String name, int age) {}

    // Record + manual builder (alternative to @Builder)
    record Config(String host, int port, boolean ssl) {
        static Builder builder() { return new Builder(); }

        static class Builder {
            private String host = "localhost";
            private int    port = 8080;
            private boolean ssl = false;

            Builder host(String h) { this.host = h; return this; }
            Builder port(int p)    { this.port = p; return this; }
            Builder ssl(boolean s) { this.ssl  = s; return this; }
            Config build() { return new Config(host, port, ssl); }
        }
    }

    // Records support interfaces โ€” Lombok @Value does not easily
    interface Displayable { String display(); }

    record Product(String name, double price) implements Displayable {
        @Override public String display() {
            return String.format("%s โ€” $%.2f", name, price);
        }
    }

    public static void main(String[] args) {
        User u = new User("Alice", 30);
        System.out.println(u);           // User[name=Alice, age=30]
        System.out.println(u.name());    // Alice (not getName())
        System.out.println(u.age());     // 30

        Config cfg = Config.builder()
                .host("prod.example.com")
                .port(443)
                .ssl(true)
                .build();
        System.out.println(cfg);

        Product p = new Product("Java Book", 49.99);
        System.out.println(p.display());

        // Pattern matching โ€” records integrate natively
        Object obj = new User("Bob", 25);
        if (obj instanceof User(String name, int age)) { // deconstruction pattern
            System.out.println(name + " is " + age);
        }
    }
}

Sources