Part 35: Java 17 (LTS) – Sealed Classes – Designing Safer Domain Models for Enterprise Applications

Introduction

Object-Oriented Programming is built around inheritance.

For decades, Java allowed any class to extend another class unless it was declared final.

This gave developers only two choices:

Open for everyone

or

Closed for everyone

There was no middle ground.

However, enterprise applications often contain business models where only a fixed set of subtypes should ever exist.

Examples include:

  • Payment Methods
  • Notification Channels
  • Account Types
  • Loan Types
  • Shipping Methods
  • Insurance Policies
  • Authentication Providers
  • Workflow Commands

Consider a payment system.

                 Payment
                     │
      ┌──────────────┼──────────────┐
      ▼              ▼              ▼
 Credit Card        UPI          Net Banking

Should another developer be allowed to create:

CryptoMonkeyPayment

Probably not.

The business itself defines the valid payment methods.

The language should be able to express that rule.

Java 17 introduced Sealed Classes, allowing developers to explicitly declare which classes may extend a superclass or implement an interface.

This makes domain models safer, easier to understand, and easier for the compiler to analyze.


Learning Objectives

By the end of this article, you will be able to:

  • Understand why Sealed Classes were introduced.
  • Create sealed classes and interfaces.
  • Use permits.
  • Understand final, sealed, and non-sealed.
  • Design finite business hierarchies.
  • Combine Records with Sealed Classes.
  • Prepare for Pattern Matching for switch.

The Problem Before Java 17

Suppose we define:

public abstract class Payment {

}

Anyone can create:

public class CryptoPayment
        extends Payment {

}

or

public class TestPayment
        extends Payment {

}

The compiler has no way to enforce business constraints.


Java 17 Solution

public sealed class Payment

    permits

        CreditCardPayment,

        UpiPayment,

        NetBankingPayment {

}

Now only those three classes may extend Payment.

Any other subclass results in a compilation error.


What Is permits?

The permits clause explicitly lists all allowed subclasses.

public sealed interface Notification

    permits

        EmailNotification,

        SmsNotification,

        PushNotification {

}

The hierarchy becomes part of the language itself.


Every Child Must Choose

Every permitted subclass must explicitly declare one of three modifiers.

1. final

public final class UpiPayment

        extends Payment {

}

No further inheritance is allowed.


2. sealed

public sealed class CardPayment

        extends Payment

        permits

            VisaPayment,

            MasterCardPayment {

}

The hierarchy continues but remains controlled.


3. non-sealed

public non-sealed class CustomPayment

        extends Payment {

}

Inheritance becomes unrestricted again from this point onward.


Visualizing the Hierarchy

                Payment (sealed)
                     │
     ┌───────────────┼───────────────┐
     ▼               ▼               ▼
 CreditCard      UPI(final)    NetBanking(final)
      │
      ▼
 Visa
 MasterCard

The hierarchy is explicit and finite.


Enterprise Example – Payment Gateway

public sealed interface PaymentMethod

        permits

            CardPayment,

            UpiPayment,

            WalletPayment {

}

The business determines all supported payment methods.

Developers cannot accidentally introduce unsupported implementations.


Enterprise Example – Notification System

public sealed interface Notification

        permits

            EmailNotification,

            SmsNotification,

            PushNotification {

}

Notification processing becomes more predictable because the compiler knows every valid subtype.


Enterprise Example – Banking

public sealed interface Account

        permits

            SavingsAccount,

            CurrentAccount,

            LoanAccount {

}

The banking domain becomes self-documenting.


Enterprise Example – Workflow Engine

WorkflowCommand

↓

Approve

Reject

Escalate

Using a sealed hierarchy ensures that only valid workflow commands exist.


Combining Records and Sealed Classes

One of the most powerful modern Java patterns is:

public sealed interface Payment

        permits

            CardPayment,

            UpiPayment {

}

Each implementation can be a Record.

public record CardPayment(

        String cardNumber,

        BigDecimal amount)

        implements Payment {

}
public record UpiPayment(

        String upiId,

        BigDecimal amount)

        implements Payment {

}

This combination provides:

  • Immutable data
  • Restricted inheritance
  • Strong compiler guarantees

Why Is This Important?

Suppose a service processes payments.

Traditional approach:

Payment

↓

Unknown subclasses

↓

Runtime surprises

Modern Java:

Payment

↓

Known subclasses

↓

Compiler verification

The compiler now understands the complete business model.


Sealed Classes vs final

Many developers ask:

“If final already prevents inheritance, why do we need Sealed Classes?”

finalsealed
No subclassesSelected subclasses
Completely closedControlled hierarchy
Single typeFamily of related types

Sealed Classes provide controlled extensibility rather than eliminating inheritance entirely.


Sealed Interfaces

Sealed interfaces are often even more useful than sealed classes.

Example:

public sealed interface Event

        permits

            OrderCreated,

            PaymentReceived,

            ShipmentCompleted {

}

Events become a well-defined contract.


Spring Boot Example

REST request:

PaymentRequest

↓

CardPayment

↓

UPIPayment

Using sealed hierarchies improves validation and makes business logic easier to reason about.


Domain-Driven Design

Sealed Classes fit naturally into Domain-Driven Design.

Examples include:

  • Commands
  • Events
  • Policies
  • Value hierarchies
  • State machines

The compiler helps enforce the ubiquitous language of the domain.


Performance

Sealed Classes are primarily a language modeling feature.

Their main advantages are:

  • Better readability
  • Stronger type safety
  • Improved compiler analysis

Performance differences are generally negligible.


Common Mistakes

Sealing Everything

Not every hierarchy should be sealed.

Use sealing only when the set of valid subtypes is intentionally finite.


Using Sealed Classes for Plugin Architectures

Plugin systems often require third-party extensions.

Sealed hierarchies would unnecessarily restrict that flexibility.


Forgetting Child Modifiers

Every permitted subclass must declare:

  • final
  • sealed
  • or non-sealed

The compiler enforces this rule.


Best Practices

✔ Seal finite business hierarchies.

✔ Use Records for immutable implementations.

✔ Keep the list of permitted subclasses focused.

✔ Prefer sealed interfaces for contracts.

✔ Avoid sealing extension points intended for external developers.


Interview Questions

Why were Sealed Classes introduced?

To allow developers to explicitly control which classes may extend a superclass or implement an interface.


What is the purpose of the permits clause?

It lists the only classes that are allowed to inherit from a sealed class or implement a sealed interface.


What modifiers can a permitted subclass use?

final, sealed, or non-sealed.


When should Sealed Classes be used?

When the business domain defines a fixed and known set of valid subtypes.


Can Records implement a sealed interface?

Yes.

In fact, Records and Sealed Interfaces work exceptionally well together for modeling immutable domain hierarchies.


Summary

Sealed Classes give Java something it lacked for decades: the ability to model finite inheritance hierarchies directly in the language. They allow developers to express business constraints that previously existed only in documentation or code reviews.

Combined with Records and Pattern Matching, Sealed Classes form the foundation of Java’s modern type system. The compiler gains a much deeper understanding of your domain, enabling safer code, better tooling, and more expressive language features.


Coming Up Next

Part 36 – Java 17: Pattern Matching for switch – The Future of Decision Making in Java

We’ll bring together everything you’ve learned so far:

  • Switch Expressions
  • Pattern Matching
  • Records
  • Sealed Classes

You’ll learn how modern Java replaces long if-else chains and complex instanceof logic with concise, type-safe switch expressions that understand object hierarchies. This is one of the most exciting language features in Java 21 and represents the culmination of several releases of language evolution.

Leave a Reply

Your email address will not be published. Required fields are marked *