Last Updated: May 17, 2026
Inheritance lets you extend a class and override its behavior, but sometimes that flexibility is exactly what you don't want. The sealed keyword tells the compiler to stop the chain: either a class can never be inherited from, or a specific overridden method can never be overridden again. This chapter covers why that lock-down exists, the two places sealed can appear, what the compile-time errors look like, and when to reach for it in practice.
Inheritance is a contract. When you make a class extensible, you're telling other developers (including future you) that they can plug in subclasses, override behavior, and rely on the base class continuing to call into those overrides in the right places. That promise is harder to keep than it looks.
Take a PremiumCustomer class that calculates a customer's monthly fee. Three months later, someone subclasses it as PremiumGoldCustomer and overrides the discount calculation. A month after that, the base class adds a new method that other parts of the codebase depend on. The subclass didn't override that method, so it inherits the default. That default might do the wrong thing for a gold customer, but the bug only shows up in production when a specific code path runs.
This is sometimes called the fragile base class problem. Once a class has subclasses you don't control, changing the base is risky. Every refactor has to consider how subclasses might break.
sealed is the answer: you mark a class as not-inheritable, and the compiler refuses to let anyone extend it. The contract becomes "this class is the end of the line. If you need different behavior, compose instead of inherit, or use a different abstraction entirely."
A simple sealed class:
From the outside, OrderConfirmation looks like any other class. You can construct it, call its methods, pass it around. The only thing you can't do is write class X : OrderConfirmation. Sealed is a one-word declaration that closes the door on inheritance while leaving every other behavior untouched.
sealed Class KeywordPlace sealed before the class keyword in a class declaration. That's the whole syntax.
A sealed class can still inherit from another class. Sealing only forbids further inheritance _below_ it; it doesn't forbid inheritance _above_ it.
The chain Customer to PremiumCustomer to PremiumGoldCustomer is valid. PremiumGoldCustomer is the leaf. Nothing can extend it. If a developer tries:
The compiler stops them at build time with error CS0509. The author of PremiumGoldCustomer declared the hierarchy complete at this leaf, and the compiler enforces that decision.
A picture of the same hierarchy, with the seal at the leaf:
The dashed arrow shows what sealed blocks: the attempt to extend further. Solid arrows are the inheritance that already existed before the seal was applied.
A few rules that follow from the definition:
virtual members inherited from a base class and override them. The seal only prevents further subclasses, not the override that this class itself performs.abstract. The two keywords have opposite intent: abstract means "you must subclass me to use me", sealed means "you cannot subclass me." Combining them is contradictory, and the compiler reports CS0418.public, internal, private). The seal is independent of visibility.The error message for inheriting from a sealed class is short and specific. Trigger it deliberately to see what it says:
Two things to notice. First, the error is reported at the line of the derived class, not the sealed base. The base did its job by declaring itself sealed; the derived class is the one breaking the rule. Second, the error includes both type names. When a build log shows CS0509, you know to look at the inheritance relationship, not the body of either class.
The error is CS0509 specifically. Memorizing the number isn't important, but recognizing it in a build log saves time when triaging a failed build.
You can fix the error in a few ways:
GiftCard as a field and delegates to it for the parts it wants to reuse.A composition fix for the GiftCard example:
PromotionalGiftCard holds a GiftCard instead of being one. The behavior is layered on top, the seal is respected, and the relationship is "has-a" rather than "is-a." This is the favored fix when the original class wasn't designed for extension.
The second place sealed appears is on a method that's already overriding a virtual method. Writing sealed override says "I'm overriding the base method, and after my override, no further subclass can override it again."
The shape:
The class itself is still open for inheritance. Middle isn't sealed, so a developer can still write class Leaf : Middle. They just can't override DoWork inside that subclass. Any other virtual method on the chain is still overrideable. This is a finer-grained tool than sealing the whole class.
A working e-commerce example. The base class defines a discount calculation that subclasses are encouraged to override, but at a certain level in the hierarchy, the business rule says "this is the final discount logic for this class and everything below it."
PremiumGoldCustomer inherits CalculateDiscount from PremiumCustomer. The discount is fixed at 10% no matter how deep the inheritance chain goes. If a future developer tries to override it inside PremiumGoldCustomer, the compiler refuses with CS0239.
The two requirements for sealed override are:
virtual (or override) member from a base class. You can't apply sealed to a method that isn't an override; there's nothing to seal.sealed override does not itself have to be sealed. Sealing a method and sealing a class are independent decisions.A diagram of the override chain with sealed override cutting it off:
The cyan node is the original virtual. The orange node is where the seal happens. The green node is allowed to exist as a subclass, but it carries the inherited (and now frozen) version of the method. The red node is what the compiler blocks.
sealed Exists: Design IntentThe first reason to seal is design clarity. Inheritance is a powerful coupling. When a class can be inherited, its public and protected surface becomes a contract that other classes depend on. Adding a method, renaming a field, or changing an algorithm can break subclasses that you've never seen. Sealing says "I'm not committing to that contract. This is a complete class, not a base class."
This matters most for classes that:
GiftCard with a balance that should never go negative is a good example. A subclass could override a method that bypasses the check.A second reason is JIT optimization. When the runtime knows a class is sealed, it can sometimes devirtualize method calls: instead of looking up the method through the virtual dispatch table at runtime, the JIT inlines a direct call. The same applies to sealed override methods: once the compiler knows no further override is possible, the call can be resolved statically. The performance difference is usually small for typical code, but in tight loops over millions of objects it can be measurable.
Cost: JIT devirtualization on sealed types is a free performance win. It doesn't change your code, doesn't change the API, and shaves off the cost of virtual dispatch on hot paths. Don't seal for performance alone, but it's a real bonus when the design already calls for sealing.
A third reason is correctness for value-like types. System.String is sealed. So are System.Uri, System.Version, System.Enum (effectively, since you can't subclass enums), and many other BCL types meant to behave like immutable values. Sealing prevents weird subclasses that pretend to be strings but break the contract (equality, hash codes, immutability).
The BCL's String class shows the convention. It's public sealed class String, and that single keyword has been there since .NET 1.0. Every guarantee string makes (immutability, value equality, ordinal hashing) holds because no subclass exists to bend the rules.
Reading the .NET source code, you see sealed over and over on classes that should behave as final, immutable, or self-contained units. A short tour:
| Type | Why it's sealed |
|---|---|
System.String | Immutability, ordinal equality, and the interning contract depend on no subclass existing. |
System.Uri | Parsing rules and equality semantics must stay consistent. |
System.Version | A simple value type representing a version number; no extension is intended. |
System.Net.Http.StringContent | Built on top of HttpContent; the leaf doesn't need further specialization. |
System.Text.Json.JsonElement | Internally optimized struct; sealing avoids unexpected subclass behavior. |
Most Attribute subclasses in the BCL | Attributes describe metadata; subclassing one usually doesn't make sense. |
The pattern: when a class represents a concrete, self-contained idea with carefully designed invariants, sealing it locks in those invariants. The class isn't a framework for extension; it's a finished product.
You'll also see sealed on inner implementation classes that the public API exposes through interfaces. The public type is the interface, the concrete sealed class is the implementation, and consumers who want different behavior implement the interface themselves rather than subclassing the implementation. This is a common, healthy pattern in modern .NET libraries.
The two keywords sit at opposite ends of inheritance intent. Comparing them side by side makes the contrast easy to see:
| Aspect | sealed class | abstract class | Regular class |
|---|---|---|---|
Can be instantiated with new? | Yes | No | Yes |
| Can be inherited from? | No | Yes (and usually required) | Yes |
| Can have abstract members? | No | Yes | No |
| Can have virtual members? | Yes | Yes | Yes |
| Intent | "I'm a finished leaf" | "I'm an incomplete template, override me" | "I'm a complete class but open for extension" |
| Common use | Value-like or self-contained classes | Base classes for a family of related types | Most ordinary classes |
A regular class sits in the middle. It works on its own and also supports inheritance. sealed and abstract push to the two extremes: one forbids subclasses entirely, the other forbids the class from standing alone. You cannot combine the two, and the compiler tells you so (CS0418).
In practice, most classes in a large codebase are either regular or sealed. Abstract classes are rarer and usually appear as the root of a small family. Sealed appears at the leaves of those families, and on standalone classes that aren't meant to be extended at all.
Opinions vary, but a useful default is: seal by default, open for inheritance only when you have a reason. This is the position recommended in Microsoft's own framework design guidelines and adopted by many widely-used libraries.
The reasoning has three parts:
Reasons to leave a class open:
Reasons to seal a class:
Reasons to use sealed override on a specific method rather than sealing the whole class:
A short way to remember it: if you find yourself asking "could a subclass break this?", and the answer is "yes, easily," then sealing (or sealing the specific method) is worth considering.
Cost: Sealing isn't free in flexibility. Test doubles (mocks, fakes for unit tests) often rely on subclassing. If your testing strategy depends on creating a subclass to substitute behavior, sealing makes that impossible. Consider exposing an interface as well, so consumers can mock the interface and the concrete class can stay sealed.
Records, introduced in C# 9 and covered in detail in the Modern C# Features section, support inheritance just like classes do. The sealed keyword works on records the same way it works on classes: it prevents further inheritance.
A short example. Don't worry about all the record details; the point is that the seal applies the same way:
DigitalOrder inherits from Order and is itself sealed. No record or class can inherit from DigitalOrder. The reasoning is the same as for sealed classes: records are often used as value-like data carriers, and sealing reinforces their "finished value" character.
You'll see sealed records often in code that uses records for domain modeling, where the hierarchy is small and the leaves represent final, fixed shapes of data. sealed works on records the same way it works on classes.
The JIT can devirtualize calls on sealed types and methods. Devirtualization replaces a virtual-dispatch call with a direct call, which can also enable inlining. In a tight loop over a million sealed objects calling a sealed method, this can show up in benchmarks.
The numbers to remember are approximate and depend heavily on workload, but as a rule of thumb:
A short benchmark sketch (don't run this without BenchmarkDotNet and proper methodology; the numbers below are illustrative, not measured):
The output is identical because the math is the same. The runtime cost can differ by a small amount because the JIT has more information about FastPriceCalculator. The practical takeaway: don't seal for performance, but don't be surprised when a profiler shows a small win after sealing types in a hot path.
Cost: Sealing as a performance technique is a micro-optimization. Don't reach for it until you have a measured bottleneck on a virtual call. For design reasons, however, it's a strong default.
Pull it all together with a small customer hierarchy. The base is open for extension, the middle is sealed at a specific method, and one leaf is fully sealed.
Three things to notice in the output:
Bob is a Customer. The base virtual CalculateDiscount returns 0, so no discount.Alice is a PremiumCustomer. Her tier overrides CalculateDiscount with the 10% rate, sealed at this level.Carol is a PremiumGoldCustomer. She inherits the sealed CalculateDiscount from PremiumCustomer, so she also gets 10%. But she overrides GetTierLabel because that method is still virtual.The hierarchy demonstrates fine-grained control. The CalculateDiscount algorithm is frozen at the PremiumCustomer level; every subclass below gets the same rate. The GetTierLabel method is open all the way down, so each subclass can customize its label. The whole PremiumGoldCustomer leaf is sealed, so no further subclass exists.
If the company later adds a PremiumPlatinumCustomer, two design decisions follow naturally:
PremiumGoldCustomer because it's sealed. They must inherit from PremiumCustomer directly. This forces a parallel structure rather than a deep chain, which keeps the hierarchy flat.CalculateDiscount. The 10% rate is final. To express a different rate, they'd need to override at a different level or use a separate calculation. This forces the team to confront the business question instead of quietly diverging.A picture of the final hierarchy with the two locks marked:
The Customer root is fully open. PremiumCustomer is open for inheritance but locks CalculateDiscount. PremiumGoldCustomer is the terminal leaf, fully sealed. Every step makes the hierarchy more constrained, and the compiler enforces the constraints.
A few patterns come up often when teams start using sealed:
Sealing too eagerly inside the codebase. When everything is sealed, legitimate uses of inheritance (test doubles, decorators, framework hooks) become awkward. Default to sealed for _public API surface_, where you can't see how consumers will extend. For internal classes, sealing is less urgent because you can refactor freely.
Forgetting that `sealed override` is different from `override`. A method without sealed override is still overrideable by further subclasses, even if the containing class isn't sealed. If the intent is "this is the final version of this method," the sealed keyword is needed.
Trying to seal a method that isn't an override. The compiler rejects sealed void DoWork() on a regular method because there's no inherited member to seal. sealed only makes sense in conjunction with override.
Combining sealed with abstract. As noted, this fails with CS0418. The intent is opposite, and the compiler stops the contradiction at the declaration.
Sealing classes that are meant to be mocked in tests. Most mocking libraries work by creating a runtime subclass of the target type. A sealed class can't be subclassed, so it can't be mocked through inheritance. The fix is to expose an interface and mock the interface, leaving the concrete class sealed.
A worked example of the last point:
The test code accepts an IEmailSender, the production wiring passes the real EmailSender2, the test wiring passes the FakeEmailSender. The seal stays, testability is preserved, and the design improves because the dependency is now expressed as an interface rather than a concrete class.
sealed on a class prevents any class from inheriting from it. The error for inheriting from a sealed class is CS0509.sealed override on a method overrides a virtual method and prevents any subclass from overriding it again. The error is CS0239. The class itself can remain open for inheritance.abstract. The compiler reports CS0418 for sealed abstract.System.String, System.Uri, System.Version, and most attribute classes) are sealed to lock in their value-like behavior and invariants.sealed override methods, giving a small free performance bonus on hot paths. Don't seal for performance alone, but accept the win when sealing is the right design choice.sealed, with the same meaning: the record can be inherited from until it's sealed, and no further inheritance is allowed below the seal.