Mastering Control in Java 17: How Sealed Classes Secure Your Codebase

~
~
Published on
Authors
java-17-sealed-banner

Mastering Control in Java 17: How Sealed Classes Secure Your Codebase

Java 17 continues the evolution of the Java language with the introduction of sealed classes.
Designed to give developers more control over inheritance, sealed classes help improve API design, security, and maintainability.

java-17-title-banner

If you’ve ever opened a codebase and found a bizarre subclass doing something it absolutely shouldn’t… sealed classes are for you.

Java-17-what-are-sealed-classes-banner

What Are Sealed Classes?

In classic Java, if a class isn’t marked final, anyone can extend it. That’s flexible—but also risky:

  • Unexpected subclasses can appear.
  • Library authors lose control over how their types are used.
  • Refactoring becomes harder because you can’t safely reason about all subclasses.

Sealed classes fix this by letting you explicitly declare which classes or interfaces are allowed to extend or implement a type.

You use the sealed keyword on the base class or interface, followed by a permits clause that lists all allowed subclasses.

Basic Example

public sealed class Vehicle permits Car, Truck {}

public final class Car extends Vehicle {}

public final class Truck extends Vehicle {}

Here’s what’s happening:

  • Vehicle is a sealed class.
  • Only Car and Truck are allowed to extend Vehicle.
  • Any attempt to write:
public class Bus extends Vehicle {}  // ❌ Not allowed

will result in a compile-time error.

This is already a huge win: your hierarchy is explicit, and the compiler is on your side.

Why Use Sealed Classes?

Sealed classes aren’t just a “nice to have”. They solve real problems that show up in production codebases.

1. Controlled Extension

You can now define exactly who gets to extend your class.

This is crucial when:

  • Designing public APIs or frameworks.
  • Defining core domain types that should not be arbitrarily extended.
  • Avoiding “surprise” subclasses created by other teams or third-party code.

Instead of relying on documentation like “please don’t extend this class”, you enforce the rule in the language.

2. Enhanced Maintainability

When you know all possible subclasses, you can:

  • Refactor with more confidence.
  • Understand the full impact of changes.
  • Avoid unexpected behavior from rogue subclasses.

This is especially powerful in large organizations where multiple teams depend on shared libraries. Sealed classes communicate intent and enforce it.

3. Better Pattern Matching and Exhaustiveness Checks

Sealed classes pair beautifully with pattern matching (e.g., switch expressions in newer Java versions).

Because the compiler knows all permitted subclasses, it can warn you if your switch doesn’t handle every case.

Conceptually:

public sealed class Vehicle permits Car, Truck {}

public final class Car extends Vehicle {
    int numberOfDoors;
}

public final class Truck extends Vehicle {
    int maxLoad;
}

public int calculateTax(Vehicle vehicle) {
    return switch (vehicle) {
        case Car car   -> car.numberOfDoors * 100;
        case Truck truck -> truck.maxLoad * 10;
        // No default needed if all subclasses are covered
    };
}

This kind of exhaustiveness is fantastic for safety and readability: the compiler helps ensure you don’t “forget” a subtype.


java-17-3-inheritance-options-banner

The Three Inheritance Options

Once a class is marked sealed, all permitted subclasses must choose one of these modifiers:

  • final – No further subclassing.
  • sealed – Continue the restriction further down the hierarchy.
  • non-sealed – Re-open the hierarchy from that point.

This is how you shape your type tree.

Example: Mixed Strategy

public sealed class Vehicle permits Car, Bike {}

public final class Car extends Vehicle {
    // No one can extend Car
}

public non-sealed class Bike extends Vehicle {
    // Bike can be subclassed freely
}

Now:

public class MountainBike extends Bike {}   // ✅ Allowed
public class RoadBike extends Bike {}       // ✅ Allowed

public class Pickup extends Car {}          // ❌ Not allowed (Car is final)

This gives you nuanced control:

  • Vehicle is sealed.
  • Car is a “closed” branch.
  • Bike is an “open” branch where further specialization is allowed.

You can also have a permitted subclass that is itself sealed to create multi-level controlled hierarchies.

Rules and Constraints to Remember

A few important rules when working with sealed classes:

  1. Permitted subclasses must be in the same module (when using modules) or the same package (if you’re not using the module system).

  2. Every permitted subclass must:

    • Directly extend or implement the sealed type.
    • Declare itself as final, sealed, or non-sealed.
  3. If you add a new permitted subclass in the permits list, you must create that class—otherwise, compilation fails.

These rules are what make the hierarchy predictable and safe.

Real-World Use Cases

So where do sealed classes shine in real projects?

1. Framework and Library APIs

If you’re building a framework, you may want to expose a base type but control how it’s extended.

Examples:

  • An AuthenticationResult sealed hierarchy:

    • Success
    • Failure
    • LockedOut

External code can use these types but cannot invent a random fourth state like MaybeSuccessButNotReally.


java-17-domain-model-banner

2. Domain Models

In domain-driven design, you often model a closed set of states.

Example: an order lifecycle.

public sealed class OrderStatus
    permits Pending, Paid, Shipped, Cancelled {}

public final class Pending extends OrderStatus {}
public final class Paid extends OrderStatus {}
public final class Shipped extends OrderStatus {}
public final class Cancelled extends OrderStatus {}

Now your domain logic can rely on this set being complete and not unexpectedly extended elsewhere.


java-17-guardians-banner

3. Security-Sensitive Code

In security or infrastructure layers, you may want to prevent subclass-based attacks or misuses.

Example:

  • sealed base types for security tokens.
  • Restricted hierarchies for permission models.
  • Internal framework classes that must never be extended by user code.

Sealed classes allow the API to remain expressive without being too open.

java-17-migration-banner

Migration Tips and Gotchas

If you’re thinking of introducing sealed classes into an existing codebase, keep these in mind:

1. Start with Internal Hierarchies

Begin with classes that:

  • Are already treated like sealed (even if not enforced by the compiler).
  • Have a known, small set of subclasses.
  • Are not widely extended by user code.

This minimizes breakage and gives you quick wins.

2. Watch Your Packages/Modules

If your subclasses live across many packages or modules, you may need to reorganize a bit. Remember:

  • Sealed base and permitted subclasses must be in the same module, or
  • In the same package, if you’re not using the module system.

3. Communicate with Other Teams

If other teams rely on extending your types, converting a widely-extended class to sealed will be a breaking change.

  • Consider introducing a new sealed hierarchy instead of sealing an existing open one.
  • Or provide a non-sealed subclass designed to be extended instead.

java-17-section-breaker-banner

Conclusion

Sealed classes in Java 17 give you explicit control over your class hierarchies:

  • You define who can extend your types.
  • You reduce accidental or malicious subclassing.
  • You enable better pattern matching and safer refactoring.

For both junior and senior developers, sealed classes are a powerful tool to:

  • Write cleaner APIs
  • Model clearer domains
  • Build safer, more maintainable Java applications

If you’re already on Java 17 (or planning the upgrade), sealed classes are absolutely worth exploring in your next refactor or redesign.

They don’t just secure your codebase—they help your architecture tell a clearer story.