The OpenJDK project Amber has released a new design note, Data-Oriented Programming for Java: Beyond Records, outlining an exploratory approach to extending record-like capabilities to more flexible class designs. The document introduces the concepts of carrier classes and carrier interfaces, which aim to generalize the core benefits of records without imposing strict representation rules.
Records, introduced in Java 16, provide a concise way to model immutable data carriers. A record declaration such as:
record Point(int x, int y) { }
It automatically defines a canonical constructor, accessor methods, and implementations of equals, hashCode, and toString. Records also participate in deconstruction patterns for use with instanceof and switch. Combined with sealed classes and pattern matching, records support modelling algebraic data types in Java. For example, an HTTP client or gateway may represent different response types as follows:
public sealed interface HttpResponse permits HttpResponse.Success, HttpResponse.NotFound, HttpResponse.ServerError {
record Success(int status, String body) implements HttpResponse {}
record NotFound(String message) implements HttpResponse {}
record ServerError(int status, String error) implements HttpResponse {}
}
Such response hierarchies can then be handled using exhaustive pattern matching:
static String handle(HttpResponse response) {
return switch (response) {
case Success(var code, var body) -> "OK (" + code + "): " + body;
case NotFound(var msg) -> "404: " + msg;
case ServerError(var code, var err) -> "Error (" + code + "): " + err;
};
}
In this example, the compiler ensures that all permitted response types are covered. If a new response variant is introduced, the switch expression must be updated to reduce the risk of incomplete error handling.
In a recent discussion, Brian Goetz, Java Language Architect, Oracle, noted that this combination enables powerful data modelling, but adoption is often limited by long-standing object-oriented design habits. He observed that developers continue to design APIs that mediate data access, even when modern language features allow much of that indirection to be removed.
The design note focuses on situations where records cannot be used. Many real-world types require derived or cached values, alternative internal representations, mutability, or inheritance. In these cases, developers must fall back to traditional classes and reintroduce boilerplate. The document describes this transition as falling off a cliff; in which small deviations from the reference model result in significantly more code.
Carrier classes are proposed to smooth this transition. A carrier class begins with a state description similar to a record header, but otherwise behaves as a normal class:
class Point(int x, int y) {
private final component int x;
private final component int y;
}
The state description defines the class’s logical components. From these components, the compiler could derive accessors, object methods, and deconstruction patterns. Unlike records, carrier classes are not required to store their state exclusively in these components.
This flexibility enables patterns that are difficult to express with records, such as caching derived values:
class Point(int x, int y) {
private final component int x;
private final component int y;
private final double norm;
Point { norm = Math.hypot(x, y); }
double norm() { return norm; }
}
Here, norm is computed during construction but is not part of the state description. The class could still benefit from compiler-generated methods based on its components.
Carrier classes are also designed to integrate with pattern matching:
if (obj instanceof Point(var x, var y)) {
// use x and y
}
The design note further discusses compatibility with future reconstruction features, such as the proposed JEP 468, which are being explored with expressions for records.
In addition to classes, the proposal introduces carrier interfaces. An interface may declare a state description and participate in pattern matching across implementations:
interface Pair(T first, U second) { }
switch (pair) {
case Pair(var a, var b) -> ...
}
This approach could simplify common tuple-like abstractions while retaining strong typing.
The design note situates carrier classes within Java’s broader shift toward data-oriented programming. By combining records, sealed types, pattern matching, and potentially carrier classes, the language increasingly encourages developers to model data structures directly rather than relying on heavily layered APIs. Goetz has argued that a key challenge is helping developers recognize how much supporting code can be eliminated when data is treated as a primary abstraction.
At present, Beyond Records is an exploratory document. No concrete syntax, JEP, or release timeline has been announced. However, it signals continued work within Project Amber to reduce boilerplate and extend modern language features to more complex class designs, which could influence how Java developers structure data-centric APIs in future releases.w
