Back to Modern Java OOP: Records & Sealed
Q2Question 2 of 2

What are sealed classes and interfaces?

Key Concepts
  • Sealed classes restrict which classes can extend them using the permits clause
  • Each permitted subtype must be final (leaf), sealed (controlled further), or non-sealed (open: escape hatch)
  • The compiler knows all possible subtypes at compile time, enabling exhaustive pattern matching in switch
  • Exhaustive switch: no default needed; adding a new subtype causes compile errors everywhere it is not handled
  • Sealed plus records create algebraic data types: TransactionResult can only be Success, Failure, or Pending
  • Java 17 feature: complementary to records for building type-safe, exhaustive hierarchies

Definition

Sealed means the parent controls which children exist via a permits list. Children must be final, sealed, or non-sealed. Enables exhaustive switch: compiler ensures all subtypes handled. Adding a new subtype causes compile errors everywhere.

A sealed class or interface restricts which classes can extend or implement it using the permits clause.

  • permits clause: sealed interface PaymentChannel permits Neft, Upi, Rtgs means only Neft, Upi, and Rtgs can implement PaymentChannel. Any other class gets a compile error.
  • Permitted subtypes must be one of: final (no further extension, leaf node), sealed (controlled further extension), or non-sealed (open for anyone to extend, escape hatch).
  • Exhaustive pattern matching: the compiler knows all possible subtypes at compile time. In a switch, no default is needed. If a new subtype is added to the permits list, every switch that does not handle it gets a compile error.
  • Sealed plus records: creates algebraic data types where the type hierarchy is a closed set and every case is a record with named components.

Before sealed: the PaymentChannel interface at National Bank of India was open. Anyone could create BitcoinChannel implements PaymentChannel without the bank's approval. With sealed: PaymentChannel says only Neft, Upi, and Rtgs are approved channels. If someone tries to create BitcoinChannel, it is a compile error. Adding ImpsChannel to the permits list causes compile errors in every switch that does not handle it.

Understanding the Concepts

What are sealed classes and interfaces?

Sealed types give the parent control over which children can exist.

How do you declare a sealed hierarchy?

JAVA
package com.bank.service;

import java.math.BigDecimal;

// Sealed interface: only 3 permitted implementations
public sealed interface PaymentChannel permits NeftChannel, UpiChannel, RtgsChannel {
    String transfer(String from, String to, BigDecimal amount);
    String getChannelName();
}

// Final: cannot be extended further (leaf node)
public final class NeftChannel implements PaymentChannel {
    @Override public String transfer(String from, String to, BigDecimal amount) {
        return "NEFT: " + from + " to " + to + " Rs." + amount;
    }
    @Override public String getChannelName() { return "NEFT"; }
}

public final class UpiChannel implements PaymentChannel {
    @Override public String transfer(String from, String to, BigDecimal amount) {
        return "UPI: " + from + " to " + to + " Rs." + amount + " (instant)";
    }
    @Override public String getChannelName() { return "UPI"; }
}

public final class RtgsChannel implements PaymentChannel {
    @Override public String transfer(String from, String to, BigDecimal amount) {
        return "RTGS: " + from + " to " + to + " Rs." + amount + " (high-value)";
    }
    @Override public String getChannelName() { return "RTGS"; }
}

// This would NOT compile:
// public class BitcoinChannel implements PaymentChannel { }
// Compile error: "BitcoinChannel is not allowed in the sealed hierarchy"

How does exhaustive pattern matching work?

JAVA
public String describeChannel(PaymentChannel channel) {
    return switch (channel) {
        case NeftChannel neft -> "NEFT: next-day settlement, IFSC-based";
        case UpiChannel upi -> "UPI: instant, VPA-based, max Rs.1L";
        case RtgsChannel rtgs -> "RTGS: real-time, min Rs.2L";
        // No default needed: compiler knows only 3 types exist.
        // Add ImpsChannel to permits -> compile error here until we add a case.
    };
}

What are the three modifiers for permitted subtypes?

JAVA
// Final: no further extension (leaf node)
public final class NeftChannel implements PaymentChannel { }

// Sealed: controlled further extension
public sealed class DigitalChannel implements PaymentChannel permits UpiChannel, ImpsChannel { }
public final class UpiChannel extends DigitalChannel { }
public final class ImpsChannel extends DigitalChannel { }

// Non-sealed: open for anyone to extend (escape hatch)
public non-sealed class CustomChannel implements PaymentChannel { }
public class VendorXChannel extends CustomChannel { } // anyone can extend
// Caution: non-sealed breaks exhaustiveness; switch needs default for this branch.

How do sealed types combine with records?

JAVA
// Sealed + records = algebraic data types
public sealed interface TransactionResult {
    record Success(String utr, BigDecimal amount) implements TransactionResult {}
    record Failure(String errorCode, String message) implements TransactionResult {}
    record Pending(String referenceId, java.time.Duration expectedWait) implements TransactionResult {}
}

public String handleResult(TransactionResult result) {
    return switch (result) {
        case TransactionResult.Success s -> "Transferred Rs." + s.amount() + " UTR: " + s.utr();
        case TransactionResult.Failure f -> "Failed: " + f.errorCode() + " - " + f.message();
        case TransactionResult.Pending p -> "Pending: " + p.referenceId();
    };
    // All three cases handled. No default. Adding Timeout -> compile error everywhere.
}

How do sealed types compare to regular interfaces?

| Aspect | Regular Interface | Sealed Interface |
|--------|------------------|------------------|
| Who can implement? | Anyone | Only permitted classes |
| Compiler knows all types? | No | Yes |
| Exhaustive switch? | No (needs default) | Yes (no default needed) |
| New subtype | Silently falls to default | Compile error until handled |
| Use case | Open extension | Closed hierarchy |

What are common mistakes?

The most common mistake is using non-sealed when exhaustive handling is needed.

JAVA
// WRONG: non-sealed breaks exhaustiveness
public non-sealed class CustomChannel implements PaymentChannel { }
// Switch needs default for CustomChannel and any subclass.

// CORRECT: use final or sealed for exhaustive hierarchies
public final class CustomChannel implements PaymentChannel { }

What should a senior engineer know?

  • Sealed types enable the compiler to enforce that every handler (switch, visitor) covers all cases.
  • In functional programming, this is called a sum type or tagged union.
  • Sealed types are complementary to records: sealed defines the shape of the hierarchy, records define the shape of each leaf.
  • In Spring, sealed types can be used for event hierarchies where each event type is a record.

Visual Overview

DIAGRAM
  SEALED HIERARCHY
  ──────────────────────────────────────────

  sealed interface PaymentChannel
  permits NeftChannel, UpiChannel, RtgsChannel
  ┌──────────────────────────────┐
  │ PaymentChannel (sealed)       │
  │   permits:                    │
  │   ├── NeftChannel (final)     │
  │   ├── UpiChannel (final)      │
  │   └── RtgsChannel (final)     │
  │                               │
  │   BitcoinChannel -> COMPILE   │
  │                    ERROR      │
  └──────────────────────────────┘

  EXHAUSTIVE SWITCH
  switch (channel) {
    case NeftChannel  -> handled
    case UpiChannel   -> handled
    case RtgsChannel  -> handled
    // No default needed.
    // Add ImpsChannel to permits
    // -> compile error HERE
  }

  SEALED + RECORDS = ALGEBRAIC DATA TYPE
  sealed interface TransactionResult
  ├── record Success(utr, amount)
  ├── record Failure(code, message)
  └── record Pending(refId, wait)

Key Takeaway

Sealed classes and interfaces (Java 17) give the supertype control over its hierarchy using the permits clause. Only listed subtypes can extend or implement the sealed type. Any other class gets a compile error. Each permitted subtype must be final (leaf), sealed (controlled further), or non-sealed (open escape hatch).

The key benefit is exhaustive pattern matching in switch (Java 21). The compiler knows all possible subtypes, verifies every case is handled, and no default clause is needed. Adding a new subtype (ImpsChannel) to the permits list causes compile errors in every switch statement that does not handle it: zero silent drops, zero missed cases.

Sealed plus records create algebraic data types: TransactionResult can only be Success, Failure, or Pending. The switch destructures each record type and handles it exhaustively. If the team adds a Timeout result type, every switch in the codebase immediately shows a compile error.

Remember one sentence: sealed types control which children can exist, enabling exhaustive pattern matching in switch where adding a new subtype causes compile errors everywhere it is not handled.

---

Share
Use ← → arrow keys to navigate