Java 16 record Tutorial: How record, Lombok, and POJO Compare
Traditional Java has a pattern called POJO (Plain Old Java Object) — a class used purely as a data holder, with private fields plus their matching getter and setter methods. POJO is a convention, not a language-level construct. Java 16 added a new kind of class called record, which bakes the data-carrier idea directly into the language and auto-generates a lot of the boilerplate. record partly replaces POJOs and Lombok, but not entirely.
This post first walks through the three approaches — POJO, Lombok, and record — side by side, then compares the trade-offs.
Examples of each approach
Assume we have a User class for storing user data, with two fields: name and address.
Traditional POJO
public class User {
private String name;
private String address;
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;
}
}
This is verbose. Even with an IDE like Eclipse auto-generating all the getters and setters, every time you add a field you have to regenerate them, and if you delete or rename a field you may have to adjust the generated methods by hand. This is one of the classic “Java is noisy” complaints.
Using Lombok
@lombok.Data
public class User {
private String name;
private String address;
}
With Lombok it’s dramatically shorter. @Data auto-generates getters, setters, equals(), hashCode(), and toString(), and when you add or remove a field those methods stay in sync automatically. You just add a dependency in Gradle or Maven and you’re good.
Lombok is well-supported in Visual Studio Code — Ctrl+I (Windows) or Cmd+I (macOS) surfaces the auto-generated methods in code completion. Eclipse is more painful: you have to install a plugin into the Eclipse installation itself, which is more work than most Eclipse plugins. Without it, Eclipse flags every Lombok-generated method as missing, and even after I got it working it sometimes silently stopped taking effect.
I usually stack more Lombok annotations on top to get extra features:
@lombok.Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public class User {
private String name;
private String address;
}
@Builder requires @AllArgsConstructor — it needs to auto-generate a constructor that takes every field. But per Java’s rules, once you define any constructor, the default no-arg constructor stops being auto-generated. So if you want both, you also need @NoArgsConstructor. It looks noisy, but the four lines are copy-paste the same every time, and since the annotations use their fully-qualified names there’s nothing extra to import.
All that just for the Builder — which is what makes it worth it:
var u1 = User.builder()
.name("Alice")
.build();
var u2 = User.builder()
.name("Alice")
.address("Wonderland")
.build();
System.out.println(u1.equals(u2));
// false
u1.setAddress("Wonderland");
System.out.println(u1.equals(u2));
// true
The static builder() method that Lombok generates lets you pick which fields to set when constructing the object. When a class has a lot of fields, this is invaluable.
For reference, here’s how to add the Lombok dependency in Gradle:
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
Using record
public record User(String name, String address) {
}
record is even shorter, and it automatically provides equals(), hashCode(), and toString(). Quick example:
var u1 = new User("Alice", "Wonderland");
var u2 = new User("Alice", "Wonderland");
var u3 = new User("Alice", "Narnia");
System.out.println(u1.toString());
// User[name=Alice, address=Wonderland]
System.out.println(u1.name());
// Alice
System.out.println(u1.address());
// Wonderland
System.out.println(u1.equals(u2));
// true
System.out.println(u1.equals(u3));
// false
record is essentially syntactic sugar, so you can add regular methods to it like any normal class. The difference: records don’t allow non-static fields — only static fields are permitted. If you add one, the compiler tells you “User declared non-static fields n are not permitted in a record.” And the declared record components are all final, so you can’t reassign them anywhere — trying to gives you “The final field User.name cannot be assigned.” Here’s how to extend a User record with extra behavior:
public record User(String name, String address) {
private static int sayHiCount = 0;
public String sayHi() {
sayHiCount++;
return "Hi! I am " + name + ", I say " + sayHiCount + " times.";
}
}
var user = new User("Alice", "Wonderland");
System.out.println(user.sayHi());
// "Hi! I am Alice, I say 1 times."
System.out.println(user.sayHi());
// "Hi! I am Alice, I say 2 times."
Comparing the approaches
POJO vs Lombok
POJO’s upside is that it’s native Java code — no install step, no dependency on a third-party JAR. The downside is the boilerplate, even when the IDE helps.
Lombok saves a huge amount of typing. Beyond the auto-generated getters, setters, equals(), hashCode(), and toString(), it also gives you @Builder. Adding the dependency is straightforward in Gradle and Maven. The one pain point is Eclipse integration, which was honestly part of why I ended up moving to VS Code.
Lombok vs record
As the record examples show, record is very convenient, and since Java 16 it’s a native feature — no extra install or dependency. But it doesn’t fully replace Lombok, mainly because records produce immutable objects — like Java’s String, once created they can’t change. Beyond String, a growing set of immutable types have shown up in the standard library since Java 8 added the Stream API, such as collections returned from List.of() and Map.of(), which throw java.lang.UnsupportedOperationException if you call add or put on them. Immutability has real benefits, but it’s also why record can’t fully replace POJO or Lombok.
A second difference: when you create a record instance you must pass all components (null is allowed). Like any other Java constructor, you can’t reorder arguments or omit one. When you actually need that flexibility, Lombok’s @Builder is very helpful.
A third difference: records can’t extend another class, and they can’t be extended. The intent here (combined with the second point) seems to be pushing developers toward smaller record types composed together — a record inside a record inside a record — rather than deep inheritance hierarchies. That fits the broader “composition over inheritance” principle, which React also encourages via higher-order components and component composition.
A smaller difference, worth noting: POJO and Lombok expose data through getXxx()-style methods, so reading name looks like getName(). record exposes accessors as bare name() instead. Worth getting used to if you switch back and forth.