DEV Community

Cover image for Java Records and Sealed Classes: Designing Type-Safe Domain Models
DEVANSHU PATIL
DEVANSHU PATIL

Posted on AI-assisted

Java Records and Sealed Classes: Designing Type-Safe Domain Models

Java Records and Sealed Classes: Designing Type-Safe Domain Models

Modern Java has undergone a significant renaissance. Long criticized for excessive boilerplate, verbosity, and the sheer amount of ceremony required to model simple concepts, Java now provides powerful features that rival modern functional languages. Among the most impactful additions to the language are Records and Sealed Classes.

When combined, Records and Sealed Classes allow developers to construct domain models that are concise, immutable, and strictly type-safe. This article explores how to leverage these features to build robust enterprise software architectures.

The Problem with Traditional Domain Modeling

Historically, modeling a simple data carrier in Java required a staggering amount of code. Consider a simple Point class representing a 2D coordinate:

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point{x=" + x + ", y=" + y + "}";
    }
}
Enter fullscreen mode Exit fullscreen mode

This is nearly thirty lines of code to represent two integers. Developers frequently relied on libraries like Lombok to hide this boilerplate, but Lombok introduces external dependency management overhead, IDE integration friction, and bytecode manipulation concerns.

Java Records: Transparent Data Carriers

Introduced as a permanent feature in Java 16, Records are nominal "tuples" that act as transparent carriers for immutable data. They replace the massive boilerplate of traditional Plain Old Java Objects (POJOs) with a compact syntax.

Rewriting our Point class as a record reduces it to a single line:

public record Point(int x, int y) {}
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, the Java compiler automatically generates:

  • private final fields for each component (x and y).
  • A canonical constructor.
  • Read-only public accessor methods (x() and y(), rather than getX() and getY()).
  • Implementations of equals() and hashCode() based on all state components.
  • An implementation of toString() that prints the record name and its state.

Compact Constructors and Validation

Records are not restricted to trivial data structures. You can perform validation or normalization during instantiation using a compact constructor. Unlike traditional constructors, compact constructors do not have a parameter list.

public record Account(String id, BigDecimal balance) {
    // Compact constructor for validation
    public Account {
        Objects.requireNonNull(id, "Account ID must not be null");
        if (balance.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This ensures that an invalid Account instance cannot exist in memory, drastically reinforcing domain invariants.

Sealed Classes: Controlled Hierarchies

Traditional object-oriented programming relies heavily on inheritance. However, unrestricted extensibility via public class often leads to brittle domain logic. If any codebase can extend your class, you lose control over the type hierarchy.

Sealed Classes (introduced in Java 17) restrict which other classes or interfaces may extend or implement them. This gives domain modelers explicit control over inheritance hierarchies, enabling exhaustive type checks.

To define a sealed hierarchy, use the sealed modifier and specify permitted subtypes using the permits clause:

public sealed interface PaymentResult permits PaymentResult.Success, PaymentResult.Failure, PaymentResult.Pending {

    record Success(String transactionId) implements PaymentResult {}

    record Failure(String errorCode, String reason) implements PaymentResult {}

    record Pending(String trackingId, Instant estimatedCompletion) implements PaymentResult {}
}
Enter fullscreen mode Exit fullscreen mode

In this example, PaymentResult can only be implemented by the three nested records defined inside it. No external code can introduce a fourth unexpected state.

Combining Records and Sealed Classes for Domain Modeling

By uniting Records and Sealed Classes, you can model complex algebraic data types (ADTs) natively in Java. This is exceptionally useful when dealing with state machines, business workflows, or asynchronous event processing.

Consider an e-commerce order processing system where an order can go through several distinct states:

public sealed interface OrderState permits OrderState.Created, OrderState.Paid, OrderState.Shipped, OrderState.Cancelled {

    record Created(String orderId, Instant createdAt) implements OrderState {}

    record Paid(String orderId, String paymentId, Instant paidAt) implements OrderState {}

    record Shipped(String orderId, String trackingNumber, Instant shippedAt) implements OrderState {}

    record Cancelled(String orderId, String reason, Instant cancelledAt) implements OrderState {}
}
Enter fullscreen mode Exit fullscreen mode

Exhaustive Pattern Matching

Sealed hierarchies shine when combined with pattern matching (switch expressions). Because the compiler knows all possible implementations of a sealed interface, it can enforce exhaustiveness. If you add a new state to OrderState, the compiler will flag every switch statement that fails to handle the new case.

public class OrderProcessor {

    public String handleOrder(OrderState state) {
        return switch (state) {
            case OrderState.Created c -> "Order " + c.orderId() + " is awaiting payment.";
            case OrderState.Paid p -> "Payment received for order " + p.orderId() + ". Preparing shipment.";
            case OrderState.Shipped s -> "Order " + s.orderId() + " has shipped with tracking: " + s.trackingNumber();
            case OrderState.Cancelled ca -> "Order " + ca.orderId() + " was cancelled due to: " + ca.reason();
            // No default clause needed because the hierarchy is sealed and exhaustive!
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

If you were to add an OrderState.Refunded record later, the code above would fail to compile until you explicitly handled it in the switch expression. This eliminates entire classes of runtime errors, such as unexpected MatchException or unhandled edge cases.

Best Practices for Modern Domain Modeling

  1. Embrace Immutability: Records are shallowly immutable. Ensure that any reference types stored inside records (like collections or dates) are also immutable (e.g., using List.copyOf() or java.time classes).
  2. Keep Records Focused: Records should represent data, not business logic. Keep complex domain operations inside service classes or attach concise helper methods directly to the record if appropriate.
  3. Leverage Sealed Interfaces for State Machines: When modeling workflows or finite state machines, use sealed interfaces to represent the domain states and records to hold state-specific data.
  4. Avoid Deep Hierarchies: Sealed hierarchies should generally be shallow. A sealed interface with a set of record implementations provides the cleanest syntax and easiest comprehension.

Conclusion

Java Records and Sealed Classes represent a paradigm shift in how developers design enterprise domain models. By eliminating boilerplate and introducing exhaustive pattern matching, Java empowers engineers to write code that is clean, highly expressive, and provably safe at compile-time.

Top comments (0)