- Understand how sealed classes enforce compile-time constraints on inheritance hierarchies
- Apply sealed classes to model exhaustive domain states and business rules
- Combine sealed classes with pattern matching for type-safe domain logic
- Recognize when sealed classes provide advantages over traditional inheritance or enums
- Implement real-world domain models using sealed hierarchies
Ever wished Java had a cleaner way to express domain rules right in the type system? Traditional inheritance in Java has always been completely open—any class can extend your base class unless you mark it final, which prevents all extension. This all-or-nothing approach made it difficult to model domains where you know exactly which subtypes should exist. Sealed classes, stabilized in Java 17, provide the missing middle ground by letting you explicitly control which classes can extend a type. This feature transforms how we model business domains, making illegal states unrepresentable at compile time rather than catching them at runtime. For intermediate developers familiar with inheritance and polymorphism, sealed classes offer a powerful tool for creating more maintainable, self-documenting domain models where the compiler enforces business rules.
Traditional Java inheritance creates maintenance challenges in domain modeling. When you define a base class or interface, any code anywhere can extend it, making it impossible to reason about all possible implementations. Consider this payment processing scenario:
package academy.javapro;
public class OpenInheritanceProblem {
    // Problem: Anyone can implement this interface
    interface Payment {
        double amount();
    }
    static class CreditCard implements Payment {
        public double amount() {
            return 100.0;
        }
    }
    public static void main(String[] args) {
        Payment payment = new CreditCard();
        // Can't be exhaustive - new implementations possible anywhere
        if (payment instanceof CreditCard) {
            System.out.println("Processing credit card: $" + payment.amount());
        } else {
            System.out.println("Unknown payment type!");
        }
    }
}This open hierarchy means you can never write truly exhaustive handling because unknown implementations might exist anywhere in your codebase or third-party libraries.
Sealed classes solve this by explicitly declaring which classes can extend them. The compiler enforces this constraint, creating a closed world of known implementations:
package academy.javapro;
public class SealedPaymentDemo {
    // Only these three classes can implement Payment
    sealed interface Payment permits CreditCard, BankTransfer, Crypto {
        double amount();
    }
    record CreditCard(double amount) implements Payment {
    }
    record BankTransfer(double amount) implements Payment {
    }
    record Crypto(double amount) implements Payment {
    }
    public static void main(String[] args) {
        Payment payment = new Crypto(50.0);
        // Compiler knows ALL possible types - no default needed!
        String result = switch (payment) {
            case CreditCard cc -> "Card payment: $" + cc.amount();
            case BankTransfer bt -> "Bank transfer: $" + bt.amount();
            case Crypto c -> "Crypto payment: $" + c.amount();
        };
        System.out.println(result);
    }
}The compiler now guarantees exhaustive handling. If you add a new payment type to the permits clause, every switch expression using Payment immediately shows a compilation error until you handle the new case.
Sealed classes excel at representing finite states where certain transitions should be impossible. Here's a simplified order tracking system:
package academy.javapro;
public class OrderStates {
    sealed interface OrderState permits Pending, Shipped, Delivered {
        String orderId();
    }
    record Pending(String orderId) implements OrderState {
    }
    record Shipped(String orderId, String trackingNumber) implements OrderState {
    }
    record Delivered(String orderId, String signature) implements OrderState {
    }
    static String getStatusMessage(OrderState state) {
        return switch (state) {
            case Pending(var id) -> "Order " + id + " awaiting shipment";
            case Shipped(var id, var tracking) -> "Order " + id + " shipped, tracking: " + tracking;
            case Delivered(var id, var sig) -> "Order " + id + " delivered, signed by: " + sig;
        };
    }
    public static void main(String[] args) {
        OrderState order1 = new Pending("ORD-001");
        OrderState order2 = new Shipped("ORD-002", "TRACK-123");
        OrderState order3 = new Delivered("ORD-003", "John Doe");
        System.out.println(getStatusMessage(order1));
        System.out.println(getStatusMessage(order2));
        System.out.println(getStatusMessage(order3));
    }
}Each state carries exactly the data it needs—no nullable fields or inappropriate data combinations. The pattern matching extracts this data elegantly.
While enums work for simple constants, sealed classes shine when variants need different data:
package academy.javapro;
public class SealedVsEnum {
    // Enum: Good for fixed constants
    enum Color {RED, GREEN, BLUE}
    // Sealed: Better when variants carry different data
    sealed interface Shape permits Circle, Rectangle, Triangle {
    }
    record Circle(double radius) implements Shape {
    }
    record Rectangle(double width, double height) implements Shape {
    }
    record Triangle(double base, double height) implements Shape {
    }
    static double calculateArea(Shape shape) {
        return switch (shape) {
            case Circle(var r) -> Math.PI * r * r;
            case Rectangle(var w, var h) -> w * h;
            case Triangle(var b, var h) -> 0.5 * b * h;
        };
    }
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);
        System.out.printf("Circle area: %.2f%n", calculateArea(circle));
        System.out.printf("Rectangle area: %.2f%n", calculateArea(rectangle));
    }
}Sealed classes let each shape variant carry its specific measurements without forcing all shapes to have unnecessary fields.
Sealed classes support nesting for sophisticated domain modeling:
package academy.javapro;
public class NestedSealed {
    // Top-level notification types
    sealed interface Notification permits Email, SMS {
    }
    // Email has subtypes
    sealed interface Email extends Notification permits Marketing, Transactional {
    }
    record Marketing(String recipient, String campaign) implements Email {
    }
    record Transactional(String recipient, String orderId) implements Email {
    }
    // SMS is simple
    record SMS(String phone, String message) implements Notification {
    }
    static int getPriority(Notification notification) {
        return switch (notification) {
            case Transactional t -> 1;  // Highest priority
            case SMS s -> 2;
            case Marketing m -> 3;      // Lowest priority
        };
    }
    public static void main(String[] args) {
        Notification[] notifications = {
                new Marketing("user@email.com", "SALE2024"),
                new Transactional("buyer@email.com", "ORD-789"),
                new SMS("555-0123", "Your code: 4567")
        };
        for (Notification n : notifications) {
            System.out.println("Priority " + getPriority(n) + ": " + n);
        }
    }
}This creates a type-safe hierarchy where the compiler ensures exhaustive handling at every level.
Here's a practical example modeling authentication states in a web application:
package academy.javapro;
import java.time.LocalDateTime;
public class AuthenticationModel {
    sealed interface AuthState permits Anonymous, Authenticated, Locked {
    }
    record Anonymous() implements AuthState {
    }
    record Authenticated(String userId, String token, LocalDateTime expires) implements AuthState {
    }
    record Locked(String userId, int attempts, LocalDateTime until) implements AuthState {
    }
    static boolean canAccessResource(AuthState state) {
        return switch (state) {
            case Anonymous() -> false;
            case Authenticated(var id, var token, var exp) -> exp.isAfter(LocalDateTime.now());
            case Locked(var id, var attempts, var until) -> false;
        };
    }
    static String getWelcomeMessage(AuthState state) {
        return switch (state) {
            case Anonymous() -> "Please log in";
            case Authenticated(var id, var t, var e) -> "Welcome, user " + id;
            case Locked(var id, var attempts, var until) -> "Account locked. " + attempts + " failed attempts";
        };
    }
    public static void main(String[] args) {
        AuthState user1 = new Anonymous();
        AuthState user2 = new Authenticated("U123", "token-abc",
                LocalDateTime.now().plusHours(2));
        AuthState user3 = new Locked("U456", 3, LocalDateTime.now().plusMinutes(30));
        AuthState[] users = {user1, user2, user3};
        for (AuthState user : users) {
            System.out.println(getWelcomeMessage(user));
            System.out.println("Can access: " + canAccessResource(user));
            System.out.println();
        }
    }
}This model makes authentication states explicit and ensures all code handles every possible state correctly.
Sealed classes fundamentally improve domain modeling in Java by providing controlled inheritance hierarchies that the compiler can reason about exhaustively. Unlike traditional open inheritance where any class could extend your types, sealed classes explicitly enumerate their permitted subtypes, enabling pattern matching without default cases and ensuring complete handling of all business scenarios. Combined with records for data carriers and switch expressions for processing logic, sealed classes create domain models that are simultaneously more expressive and safer than traditional approaches. For intermediate Java developers, mastering sealed classes means writing code where invalid states become unrepresentable and business rules are enforced at compile time rather than hoped for at runtime.
Accelerate your tech career with our comprehensive Java Bootcamp! Master enterprise-level programming from experienced industry professionals who guide you through hands-on projects, data structures, algorithms, and Spring Framework development. Whether you’re a complete beginner or looking to level up your coding skills, our intensive program equips you with the real-world expertise employers demand. Join our dynamic learning community and transform your passion for coding into a rewarding software development career.