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
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
Sealing prevents subclasses from violating the base class's invariants.
Enables Performance Optimizations
The .NET JIT and JVM HotSpot can devirtualize calls to sealed/final methods, making them faster.
Makes the API Contract Clear
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
Sealing forces you to create interfaces for testability, which some developers view as unnecessary boilerplate.
Reduces Flexibility for Consumers
Composition works but requires wrapping every method, which is tedious.
Internal Code Is Different from Public APIs
The risk of fragile base class is much lower in internal code where the same team controls all callers.
Recommended Approach
| Context | Recommendation |
| Public library/API | Seal by default, unseal only when designed for extension |
| Internal application | Do not seal by default, but design for composition |
| Kotlin/Rust/Swift | Already sealed by default — use open/inheritance intentionally |
| C#/Java | Seal data classes and utility classes; leave services open |
| Performance-critical code | Seal to enable devirtualization |
Practical Guidelines
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
sealedin C# withsealedin Kotlin/Java: C#sealedprevents inheritance. Kotlin classes arefinalby default. Javasealed(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 (
recordin C# ordata classin 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

