Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java 21 — New Features

Data-Oriented Programming

  • Records
  • Sealed Classes
  • Pattern Matching

Virtual Threads (Project Loom)

  • Virtual Threads
Chaturmind
← Java 21 — New Features

Data-Oriented Programming

  • Records
  • Sealed Classes
  • Pattern Matching

Virtual Threads (Project Loom)

  • Virtual Threads
HomeLearnJavaJava 21 — New FeaturesData-Oriented Programming
✓ FreeIntermediate· 6 min read

Sealed Classes

Restrict class hierarchies with sealed/permits — the foundation of pattern matching.

Published September 21, 2026


Sealed Classes

Normally anyone can extend a non-final class or implement a public interface. That's flexible, but it means the compiler can never know all the subtypes of a type. So code that handles "every kind of Shape" needs a default branch "just in case", and adding a new subtype silently falls into that branch instead of being handled.

A sealed class or interface (final in Java 17) lists exactly which types may extend it. The hierarchy is closed, and that lets the compiler check that you've handled every case. It's Java's version of what functional languages call a sum type or algebraic data type: "a Shape is either a Circle or a Rectangle or a Triangle, and nothing else".

Declaring a sealed hierarchy

public sealed interface Shape permits Circle, Rectangle, Triangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

Records are the natural partner for sealed types: each variant carries its own data, is immutable, and is implicitly final. If the subtypes are declared in the same file as the sealed type, the permits clause can be omitted and the compiler infers it.

The rules

  1. Every permitted subtype must directly extend or implement the sealed type.
  2. Every permitted subtype must declare how it continues the seal, using one of:
    • final: no further subclasses (records and enums are implicitly final);
    • sealed: its own closed list of subtypes;
    • non-sealed: opens that branch back up so anyone can extend it (a deliberate escape hatch).
  3. The sealed type and its permitted subtypes must be in the same module, or, if you don't use modules, the same package.
public sealed abstract class Vehicle permits Car, Truck, Bike {}
public final class Car extends Vehicle {}
public sealed class Truck extends Vehicle permits PickupTruck, SemiTruck {}   // further closed
public non-sealed class Bike extends Vehicle {}                               // anyone may extend Bike

The payoff: exhaustive switch (Java 21)

With a sealed type, a switch that covers every permitted subtype needs no default, and the compiler verifies it's complete:

double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
    };                                                   // no default: all cases covered
}

Now add record Hexagon(double side) implements Shape {} to the permits list. Every switch over Shape that lacks a Hexagon case stops compiling. That's the key benefit: the compiler finds every place that must handle the new variant, instead of a default branch silently swallowing it at runtime.

Combined with record patterns (see Pattern Matching), you can take the data apart right in the case:

case Rectangle(double w, double h) when w == h -> "square of side " + w;
case Rectangle(double w, double h)             -> "rectangle " + w + "×" + h;

Modelling domain results without exceptions

A classic use is a result type where each outcome carries different data:

public sealed interface PaymentResult {
    record Approved(String transactionId) implements PaymentResult {}
    record Declined(String reason) implements PaymentResult {}
    record RequiresAction(URI challengeUrl) implements PaymentResult {}     // e.g. 3-D Secure
}

String message(PaymentResult r) {
    return switch (r) {
        case PaymentResult.Approved a       -> "Paid, ref " + a.transactionId();
        case PaymentResult.Declined d       -> "Declined: " + d.reason();
        case PaymentResult.RequiresAction x -> "Please verify at " + x.challengeUrl();
    };
}

Compared with throwing exceptions for expected outcomes, or returning a class with nullable fields for every case, this makes each outcome explicit, type-safe and impossible to forget.

Other good fits: commands and events in an event-driven system, states in a state machine, AST nodes in a parser, API responses with a fixed set of shapes.

Sealed classes vs enums vs plain inheritance

EnumSealed hierarchyOpen inheritance
Fixed set of variants✅✅❌
Each variant carries different data❌ (same fields for all)✅✅
Multiple instances per variant❌ (one instance each)✅✅
Exhaustive switch checking✅✅❌
Third parties can add variants❌❌ (unless non-sealed)✅

Use an enum for a fixed set of constants (OrderStatus), a sealed hierarchy when the variants hold different data, and open inheritance when extension by others is the point (a plugin API).

Things to watch

  • Sealing is a design commitment. Adding a permitted subtype breaks every exhaustive switch over the type, which is intended inside your own codebase but a breaking change for external users of a library.
  • default defeats the purpose. Adding a default to a switch over a sealed type compiles, but turns off the "you forgot a case" check for future variants.
  • Serialization and frameworks: Jackson can serialize sealed hierarchies with @JsonTypeInfo/@JsonSubTypes to record which variant each JSON object is.

Follow-up questions this topic invites — and their answers

Q: What problem do sealed classes solve that final doesn't? A: final allows zero subclasses, and an open class allows any. Sealed sits in between: an exact, known list. That knowledge lets the compiler check switches exhaustively, and it documents the design intent that "these are the only kinds".

Q: Why must each permitted subclass be final, sealed or non-sealed? A: So the author explicitly decides how the closed hierarchy continues at every level. Without the rule, a permitted subclass could quietly be extended by anyone, and the "closed set" guarantee would be meaningless.

Q: Do I need permits? A: Not if all the subtypes are declared in the same source file as the sealed type (for example, nested records inside a sealed interface). The compiler infers the list. Otherwise it's required.

Q: How is a sealed interface of records different from an enum? A: Enum constants are single instances with the same fields. Sealed records can each have different fields and many instances (new Declined("insufficient funds"), new Declined("card expired")). Both give exhaustive switches.

Q: What happens at runtime if a new subtype sneaks in after compilation, e.g. through a changed library? A: The compiler inserts a hidden default into exhaustive switches that throws a MatchException (an IncompatibleClassChangeError for enum switches in older versions), so you fail loudly rather than silently doing the wrong thing.

Previous

Records

Next

Pattern Matching

AI Tutor

Lesson: Sealed Classes

Quick actions

AI responses can be inaccurate. Verify critical information.