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

Should I recommend sealing classes by default?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.