software development
object-oriented programming
class sealing
programming best practices
software design principles

Should I recommend sealing classes by default?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Sealing classes by default means marking classes as sealed (C#) or final (Java/Kotlin) unless you explicitly design them for inheritance. The argument for this practice: inheritance is a strong coupling mechanism that is hard to change later, so you should only allow it when you have intentionally designed for it. The argument against: it reduces flexibility and makes testing harder. Both sides have merit, and the right answer depends on whether you are building a public API or an internal application.

What Sealed/Final Means

csharp
1// C# — sealed class
2public sealed class PaymentProcessor
3{
4    public void ProcessPayment(decimal amount) { /* ... */ }
5}
6
7// Cannot inherit:
8// public class SpecialProcessor : PaymentProcessor { }  // Compile error
java
1// Java — final class
2public final class PaymentProcessor {
3    public void processPayment(BigDecimal amount) { /* ... */ }
4}
5
6// Cannot extend:
7// public class SpecialProcessor extends PaymentProcessor { }  // Compile error
kotlin
1// Kotlin — classes are final by default
2class PaymentProcessor {
3    fun processPayment(amount: BigDecimal) { /* ... */ }
4}
5
6// Must use 'open' to allow inheritance
7open class BaseProcessor { /* ... */ }

Kotlin made the deliberate choice to seal classes by default — you must opt in to inheritance with open.

Arguments For Sealing by Default

Prevents Fragile Base Class Problem

csharp
1// Unsealed class — anyone can override
2public class OrderService
3{
4    public virtual decimal CalculateTotal(Order order)
5    {
6        return order.Items.Sum(i => i.Price * i.Quantity);
7    }
8
9    public void PlaceOrder(Order order)
10    {
11        var total = CalculateTotal(order);  // Calls overridden version
12        ChargePayment(total);               // Might charge wrong amount
13    }
14}
15
16// A subclass breaks the internal contract
17public class DiscountOrderService : OrderService
18{
19    public override decimal CalculateTotal(Order order)
20    {
21        return 0;  // Everything is free!
22    }
23}

Sealing prevents subclasses from violating the base class's invariants.

Enables Performance Optimizations

csharp
1// Sealed classes enable devirtualization
2// The JIT compiler can inline method calls because it knows
3// no subclass can override them
4public sealed class FastMath
5{
6    public double Square(double x) => x * x;  // Can be inlined
7}

The .NET JIT and JVM HotSpot can devirtualize calls to sealed/final methods, making them faster.

Makes the API Contract Clear

csharp
1// Sealed: "This class is complete. Use it as-is."
2public sealed class HttpClient { /* ... */ }
3
4// Unsealed + virtual: "This class is designed for extension."
5public abstract class HttpMessageHandler
6{
7    protected abstract Task<HttpResponseMessage> SendAsync(/*...*/);
8}

Sealing communicates intent. If you did not design for inheritance, pretending the class is extensible creates a false contract.

Arguments Against Sealing by Default

Makes Unit Testing Harder

csharp
1// Sealed class — cannot create a test double
2public sealed class EmailService
3{
4    public void SendEmail(string to, string body) { /* sends real email */ }
5}
6
7// Test needs to mock EmailService, but can't inherit from it
8// Fix: extract an interface
9public interface IEmailService
10{
11    void SendEmail(string to, string body);
12}
13
14public sealed class EmailService : IEmailService { /* ... */ }

Sealing forces you to create interfaces for testability, which some developers view as unnecessary boilerplate.

Reduces Flexibility for Consumers

java
1// A library seals this class
2public final class JsonParser { /* ... */ }
3
4// Consumer wants to add logging — can't extend
5// Must use composition instead:
6public class LoggingJsonParser {
7    private final JsonParser parser = new JsonParser();
8
9    public Object parse(String json) {
10        log.info("Parsing: " + json);
11        return parser.parse(json);
12    }
13}

Composition works but requires wrapping every method, which is tedious.

Internal Code Is Different from Public APIs

csharp
1// Public library — seal by default to protect the API contract
2public sealed class PublicWidget { /* ... */ }
3
4// Internal application code — sealing adds friction with little benefit
5internal class OrderRepository { /* ... */ }
6// Nobody outside the team will inherit from this

The risk of fragile base class is much lower in internal code where the same team controls all callers.

ContextRecommendation
Public library/APISeal by default, unseal only when designed for extension
Internal applicationDo not seal by default, but design for composition
Kotlin/Rust/SwiftAlready sealed by default — use open/inheritance intentionally
C#/JavaSeal data classes and utility classes; leave services open
Performance-critical codeSeal to enable devirtualization

Practical Guidelines

csharp
1// 1. Seal data/value classes — they should not be extended
2public sealed record Address(string Street, string City, string Zip);
3
4// 2. Seal utility/helper classes
5public sealed class StringHelper
6{
7    public static string Truncate(string s, int maxLen) => /* ... */;
8}
9
10// 3. Use interfaces + sealed implementations
11public interface IPaymentGateway
12{
13    Task<PaymentResult> Charge(decimal amount);
14}
15
16public sealed class StripeGateway : IPaymentGateway { /* ... */ }
17public sealed class PayPalGateway : IPaymentGateway { /* ... */ }
18
19// 4. Leave abstract/base classes open (they exist for inheritance)
20public abstract class Controller { /* ... */ }

Common Pitfalls

  • Sealing without providing interfaces: If you seal a class and do not extract an interface, consumers cannot mock it for testing. Always provide an interface alongside a sealed service class.
  • Sealing classes in internal code unnecessarily: For application code that is not a public API, sealing adds friction (especially for testing) with minimal benefit. Reserve sealing for public APIs and data classes.
  • Confusing sealed in C# with sealed in Kotlin/Java: C# sealed prevents inheritance. Kotlin classes are final by default. Java sealed (Java 17+) restricts which classes can extend a class — different from preventing inheritance entirely.
  • Assuming sealed prevents all misuse: Sealing prevents inheritance but not composition, reflection, or other forms of coupling. It is one tool, not a complete encapsulation strategy.
  • Not sealing record types: Records (record in C# or data class in Kotlin) are value-oriented types that should almost always be sealed. Inheriting from a record breaks value equality semantics.

Summary

  • Seal classes by default in public libraries and APIs to protect the inheritance contract
  • In internal application code, sealing is often unnecessary overhead — use interfaces and composition instead
  • Kotlin and Rust seal by default; C# and Java require explicit sealed/final
  • Always provide interfaces alongside sealed service classes for testability
  • Seal data classes and records unconditionally — inheritance breaks value semantics
  • The core principle: only allow inheritance when you have designed and documented for it

Course illustration
Course illustration

All Rights Reserved.