Inheritance
Inheritance
· Last updated: 2026-08-10 · 38 min read
Inheritance is a mechanism for creating new definitions by inheriting the structure, implementation, contracts, or relationships of existing classes or types. Many class-based languages also use it to express specialization and subtype relationships, but those are use cases for inheritance, not its definition.
In class inheritance, implementations can be passed down along with the type; in interface inheritance, primarily type contracts and relationships are inherited. In object-oriented contexts, it is typically contrasted with Composition.
A summary of inheritance compared with composition is as follows.
Approach | Meaning |
|---|---|
Inheritance | Connects the members and contracts of a supertype or superclass to a subtype, and depending on the language, also passes down the implementation. |
Composition | Assembles other objects as components and delegates work to them. |
- Approach
Inheritance
- Meaning
Connects the members and contracts of a supertype or superclass to a subtype, and depending on the language, also passes down the implementation.
- Approach
Composition
- Meaning
Assembles other objects as components and delegates work to them.
The advice "favor Composition over Inheritance" has spread so widely that online communities have developed an atmosphere where using inheritance at all requires justification.
Personal note: That said, the Korean programming scene still commonly recommends using inheritance, so this reflects what "international" communities tend to think.\
I remember getting into arguments during code reviews at a certain Korean financial SI firm that still practices deep inheritance hierarchies. Of course, the client calls the shots, but I genuinely don't know where the belief comes from that implementing everything through inheritance is correct simply because someone has years of experience.
To put it plainly, this advice was never meant to prohibit inheritance.
As programming has evolved and practices have become more nuanced, the right way to read it is: reduce the broad, catch-all use of inheritance in favor of approaches better suited to specific situations. Avoiding it entirely is also a problem.
Inheritance has one property that simple delegating Composition does not provide automatically: implicit open recursion, where virtual self calls in base code resolve back to the actual child implementation. This document examines when that automatic wiring is useful and what trade-offs it entails.
This is the core point I want to address here.
Item | Summary |
|---|---|
What Implementation Inheritance Can Provide | Open recursion: virtual |
The Cost | Overridable |
The Criterion | Is this resolution actually needed, or is code reuse the real goal? |
- Item
What Implementation Inheritance Can Provide
- Summary
Open recursion: virtual
selfcalls in base code resolve back to the child implementation
- Item
The Cost
- Summary
Overridable
selfcalls and the portion of state exposed to subclasses effectively become the subclass contract
- Item
The Criterion
- Summary
Is this resolution actually needed, or is code reuse the real goal?
Terminology
Multiple concepts overlap under the single word "inheritance."
They must first be separated.
Name | Meaning | What You Gain at Implementation Time |
|---|---|---|
Implementation Inheritance | Reuses the base class's state representation and method implementations according to the language's inheritance and access rules | Code reuse, open recursion limited to virtual methods |
Interface Inheritance | Inherits contracts, and depending on the language, also inherits default implementations | Polymorphism, type relationships |
Subclassing | The relationship of being positioned lower in a class hierarchy | A syntactic relationship |
Subtyping | The relationship that the type system permits to be placed in a supertype's position | Static substitutability |
Behavioral Subtyping | The relationship that preserves even the semantic contracts of the supertype | Actual substitutability |
- Name
Implementation Inheritance
- Meaning
Reuses the base class's state representation and method implementations according to the language's inheritance and access rules
- What You Gain at Implementation Time
Code reuse, open recursion limited to virtual methods
- Name
Interface Inheritance
- Meaning
Inherits contracts, and depending on the language, also inherits default implementations
- What You Gain at Implementation Time
Polymorphism, type relationships
- Name
Subclassing
- Meaning
The relationship of being positioned lower in a class hierarchy
- What You Gain at Implementation Time
A syntactic relationship
- Name
Subtyping
- Meaning
The relationship that the type system permits to be placed in a supertype's position
- What You Gain at Implementation Time
Static substitutability
- Name
Behavioral Subtyping
- Meaning
The relationship that preserves even the semantic contracts of the supertype
- What You Gain at Implementation Time
Actual substitutability
Many people treat subclassing and subtyping as the same thing, but they are subtly different. Cook, Hill, and Canning addressed exactly this distinction in their 1990 paper, though it is not widely known in Korea.1
Inheritance can be a mechanism for inheriting implementations, subtyping is the relationship in which the type system permits substitution, and behavioral subtyping is the relationship in which that substitution also preserves semantic contracts, yet many languages express both together through a single syntax like class B : A.
That said, there are cases like C++ private inheritance where substitutability is not exposed externally, and cases like interface implementation where only a subtype relationship is created without inheriting any implementation. So while some languages and syntaxes bundle both together even when you only want one, others provide syntax to separate them.
Class-based Implementation Inheritance and Alternative Composition and Reuse Mechanisms
Name | Language | How it differs from Inheritance |
|---|---|---|
Mixin | Ruby, Scala, Python, etc. | A mechanism for composing implementation fragments. Depending on the language, it is treated as a form of |
Trait | Rust, Scala, Smalltalk/Pharo family | A unit for composing contracts and behaviors. Rules around state, conflict resolution, and linearization vary by language. |
Embedding | Go | Promotes fields and methods, but does not support override-style |
Prototype Delegation | JavaScript | Delegates along an object's prototype chain instead of a class hierarchy. This is commonly referred to as prototypal inheritance. |
- Name
Mixin
- Language
Ruby, Scala, Python, etc.
- How it differs from Inheritance
A mechanism for composing implementation fragments. Depending on the language, it is treated as a form of
mixin inheritance.2
- Name
Trait
- Language
Rust, Scala, Smalltalk/Pharo family
- How it differs from Inheritance
A unit for composing contracts and behaviors. Rules around state, conflict resolution, and linearization vary by language.
- Name
Embedding
- Language
Go
- How it differs from Inheritance
Promotes fields and methods, but does not support override-style
selfrebinding
- Name
Prototype Delegation
- Language
JavaScript
- How it differs from Inheritance
Delegates along an object's prototype chain instead of a class hierarchy. This is commonly referred to as prototypal inheritance.
"Different" in this table does not mean unrelated to inheritance. Mixins and prototype delegation are each sometimes described as variations of inheritance. The criterion for separating them here is whether they provide the same type relationships, state reuse, and dispatch rules as class-based implementation inheritance.
Embedding has a particularly confusing aspect: its surface syntax looks similar to inheritance, making it easy to mistake for inheritance, but it lacks the inheritance-style open recursion where the receiver of a base method is automatically rebound to the outer type.
Where Did It Come From
Inheritance was not invented as a fully-formed syntax all at once; its current meaning emerged from the convergence of several needs: the need to extend shared structure in simulations, the need to share common behavior in object systems, and the need to express substitutable implementations in statically typed languages.

It was evident that when prefixing was introduced, it could be extended to multiple prefixing, establishing hierarchies of process classes. (In the example, "car" would be a subclass of "link," "truck" and "bus" subclasses of "car.")
— "The Development of the SIMULA Languages" (ACM SIGPLAN Notices, 1978)
Most people trace the starting point to Simula. Simula I was a language for simulation, and in Simula 67, one class could extend the structure of another through classes and prefixing. In the history that Dahl and Nygaard later recorded, the central problem was defining the common parts of simulated entities and extending those parts for various specialized processes.3 It is therefore more accurate to say that the mechanism introduced for classifying and extending simulation models later became the prototype for code reuse and inheritance hierarchies, rather than asserting flatly that "the original purpose was classification, not code reuse."
After that, inheritance was adapted in various languages for different reasons. Smalltalk-72 had no inheritance, but as the problem of copying common behavior across multiple classes grew, inheritance was introduced in Smalltalk-76.
Dan Ingalls's retrospective connects this change to a maintenance problem: "we wanted to share common behavior without copying it."4
C++ began with C with Classes in 1979, bringing Simula's concept of classes and derived classes into the context of C's performance and compatibility, and already had public/private access control from its early implementation in 1980.
At that point, however, there were no virtual functions; dynamic polymorphism via virtual functions was added when C++ was introduced in 1983. Multiple inheritance came even later, introduced in Release 2.0 in June 1989.5
Java adopted a model that combines a single base class with multiple interfaces rather than inheriting from multiple classes.6
Meanwhile, various languages and research efforts developed ways to separate the features that inheritance had bundled together, such as mixins, traits, delegation, and embedding. This branch did not begin after Java.
The formalization of mixin-based inheritance had already appeared in 1990.2
The History of Inheritance and the Problems of Its Time
As we know, the history of most technological progress is a history of struggle — working to solve problems as they arise.
With that in mind, let's take a look at the history of inheritance.
Period | Inflection Point | Problem It Was Trying to Solve |
|---|---|---|
1960s | Classes and prefixing in Simula 67 | Defining and specializing common structures in simulation models |
1970s | Inheritance in Smalltalk-76 | Sharing common behavior across multiple classes without copying it |
1979–1983 | From C with Classes to C++ | Introducing Simula-style classes and derived classes into C, then adding dynamic polymorphism via |
1990s | Subtyping theory, GoF, Java | Separating implementation reuse, substitutability, and hierarchy complexity |
1990s | Research on multiple inheritance, mixins, and subtyping separation | Distinguishing reuse from type relationships, and analyzing hierarchy coupling |
2000s | Refinement of trait research | Narrowing the unit of reuse and making conflict-resolution rules explicit |
2009 onward | Go's embedding, Rust's traits | Providing composition and contracts without class inheritance |
Publicly released in 2011, version 1.0 in 2016 | Kotlin's default | Making inheritance and overriding explicit opt-in decisions |
- Period
1960s
- Inflection Point
Classes and prefixing in Simula 67
- Problem It Was Trying to Solve
Defining and specializing common structures in simulation models
- Period
1970s
- Inflection Point
Inheritance in Smalltalk-76
- Problem It Was Trying to Solve
Sharing common behavior across multiple classes without copying it
- Period
1979–1983
- Inflection Point
From C with Classes to C++
- Problem It Was Trying to Solve
Introducing Simula-style classes and derived classes into C, then adding dynamic polymorphism via
virtualfunctions in the transition to C++
- Period
1990s
- Inflection Point
Subtyping theory, GoF, Java
- Problem It Was Trying to Solve
Separating implementation reuse, substitutability, and hierarchy complexity
- Period
1990s
- Inflection Point
Research on multiple inheritance, mixins, and subtyping separation
- Problem It Was Trying to Solve
Distinguishing reuse from type relationships, and analyzing hierarchy coupling
- Period
2000s
- Inflection Point
Refinement of trait research
- Problem It Was Trying to Solve
Narrowing the unit of reuse and making conflict-resolution rules explicit
- Period
2009 onward
- Inflection Point
Go's embedding, Rust's traits
- Problem It Was Trying to Solve
Providing composition and contracts without class inheritance
- Period
Publicly released in 2011, version 1.0 in 2016
- Inflection Point
Kotlin's default
final- Problem It Was Trying to Solve
Making inheritance and overriding explicit opt-in decisions
This trajectory shows that inheritance is not simply a trend that rose and faded away.
Each generation kept the parts of inheritance that were useful and pushed the problematic coupling into different syntax or different defaults. One reason inheritance feels natural, in my view, is that we understand reality through classification. In practical code, however, you more often have to deal with axes of change that vary independently rather than through classification, and that is where the inheritance tree starts to break down.
Inheritance trees are strong at expressing classification but weak at separating axes of change.

Thus, the class Dog is functionally cohesive if its semantics embrace the behavior of a dog, the whole dog, and nothing but the dog.
— Grady Booch, Object-Oriented Design with Applications (1991)
Personal note: The reason school examples almost always involve Animal, Dog, and Cat seems to trace back to Grady Booch's book. To explain the concept of polymorphism, he used biological classification as a domain-neutral example and built the narrative around it.
What actually comes up in the real world is more like payment methods, discount policies, and report formats, and those tend to split along axes of change rather than along classification lines.
The four things inheritance bundles together
In a 1996 ACM Computing Surveys paper, Taivalsaari noted that even among researchers there is little consensus on the meaning and usage of inheritance.7 Looking at it from a practical standpoint in this document, part of that confusion comes from a single syntactic construct taking on roles that are quite different in character. The four categories below are not drawn from that paper; they represent this document's own framework, that is, Dongwoo Jeong's classification.
What inheritance does | Who wants it | Problems that can come along uninvited |
|---|---|---|
Passes down implementation | Someone who wants to reuse code | In many languages, a type relationship and substitutability come along with it |
Creates a type hierarchy | Someone who wants to use polymorphism | Coupling to the parent implementation |
Classifies concepts | People who want to express a domain | A constraint that splits along only one axis |
Allows only part of the behavior to be overridden | People who want to keep the skeleton and change only the steps | The parent's internal call order becomes the contract |
- What inheritance does
Passes down implementation
- Who wants it
Someone who wants to reuse code
- Problems that can come along uninvited
In many languages, a type relationship and substitutability come along with it
- What inheritance does
Creates a type hierarchy
- Who wants it
Someone who wants to use polymorphism
- Problems that can come along uninvited
Coupling to the parent implementation
- What inheritance does
Classifies concepts
- Who wants it
People who want to express a domain
- Problems that can come along uninvited
A constraint that splits along only one axis
- What inheritance does
Allows only part of the behavior to be overridden
- Who wants it
People who want to keep the skeleton and change only the steps
- Problems that can come along uninvited
The parent's internal call order becomes the contract
When all four point in the same direction, there is no problem. As a program grows, they tend to diverge. The reason you want to reuse code and the reason you need a type relationship start pointing to different places.
Composition allows implementation reuse and domain classification, two of the four concerns, to be shifted into object assembly. When a type relationship is needed, an interface or a separate type abstraction can be used alongside it.
So it cannot be said that composition always leaves only the fourth function of inheritance, but the places where inheritance's implicit self rebinding is needed still remain a separate problem.
Implicit open recursion provided by implementation inheritance
When a base method calls one of its own other virtual methods, the property whereby that call is dispatched to the override of the actual runtime type is called open recursion. Not every call in a system with inheritance has this property; virtual-less methods, final methods, and statically bound calls do not provide it. The advantage of inheritance discussed here means that class-based implementation inheritance wires this self-reference implicitly. It is hard to explain in words, so it is faster to simply run it.
// 1. Inheritance: open recursion comes for free.
public class Base
{
public virtual string Name() => "Base";
// `Greet` lives in `Base`, but the `Name` it calls internally
// is open to the override of the actual runtime type.
public string Greet() => "hello from " + Name();
}
public sealed class Derived : Base
{
public override string Name() => "Derived";
}
// 2. Simulating this with Composition breaks the chain.
public sealed class BaseComponent
{
public string Name() => "BaseComponent";
// Here, `this` refers to `BaseComponent` itself.
// No matter how the wrapper redefines `Name`, control never returns through it.
public string Greet() => "hello from " + Name();
}
public sealed class WrapperNaive
{
private readonly BaseComponent inner = new BaseComponent();
public string Name() => "WrapperNaive";
public string Greet() => inner.Greet();
}
// 3. You can restore the behavior by explicitly injecting `self`, but you have to wire it by hand.
public interface INamer
{
string Name();
}
public sealed class BaseWithSelf
{
private readonly INamer self;
public BaseWithSelf(INamer self) => this.self = self;
public string Greet() => "hello from " + self.Name();
}
public sealed class WrapperExplicit : INamer
{
private readonly BaseWithSelf inner;
// Pass `this` to the collaborator object in the constructor.
// If the collaborator calls `this` or exposes it externally during construction,
// an uninitialized state may be observed.
// This example only stores the reference, but it adds wiring discipline.
public WrapperExplicit() => inner = new BaseWithSelf(this);
public string Name() => "WrapperExplicit";
public string Greet() => inner.Greet();
}The output is as follows.
Inheritance Greet() = hello from Derived
Composition Delegation Greet() = hello from BaseComponent
Explicit `self` Injection Greet() = hello from WrapperExplicitThe second line deserves close attention.
Delegation alone does not automatically produce the call-back behavior.
This is because the inner object's this refers to the inner object itself, not the wrapper. That said, as the third example shows, explicitly injecting self into the inner object lets composition achieve the same effect, and mixins, traits, and higher-order functions can also model open recursion in other ways. Open recursion therefore cannot be called the logical exclusive province of inheritance. What this document compares is the difference between the way it is provided implicitly and the way it must be wired explicitly.
The third line shows that the semantics can be restored. The trade-off is that this must be passed to the collaborating object in the constructor, and if that object calls out or leaks the reference during construction, it may observe a state in which initialization is not yet complete. This example only stores the reference, so no immediate problem arises, but the fact remains that one more wiring rule is added compared to inheritance.
Personal note: what is being restored here is the open recursion semantics itself, where a method called inside the parent-role component is dynamically dispatched to the wrapper object's method. From the perspective of programming language theory, when a syntax or structure simulates a particular behavior, that is said to preserve or restore the 'semantics' of the original construct.
It is important to know that embedding in Go is not inheritance.
Go has no inheritance; it has embedding instead. The syntax looks similar enough that it is easy to mistake the two for the same thing, but this is precisely the point where they behave differently.
type Base struct{}
func (b Base) Name() string { return "Base" }
func (b Base) Greet() string { return "hello from " + b.Name() }
type Derived struct {
Base // Embedding. On the surface, it reads like inheritance.
}
// It looks as though `Derived` has "overridden" `Name`.
func (d Derived) Name() string { return "Derived" }Running it produces the following output.
Embedding d.Name() = Derived
Embedding d.Greet() = hello from Base
Explicit self injection e.Greet() = hello from DerivedExplicitd.Name() calls the one defined on Derived, but d.Greet() still calls Base's Name. Embedding only promotes methods to the outer type; it does not produce the inheritance-style redirection that would rebind the receiver of an embedded method to the outer type.
A common misconception at this point is that calling through an interface should change the behavior.
To be clear, it does not.
type Greeter interface {
Name() string
Greet() string
}
var g Greeter = Derived{}Concrete type d.Name() = Derived
Concrete type d.Greet() = hello from Base
Interface g.Name() = Derived
Interface g.Greet() = hello from BaseThe interface call itself follows the method set of the dynamic value, so Go does have dynamic dispatch. However, Greet is a method promoted from Base, and the b.Name() call inside it is still bound to the Base receiver. Dynamic dispatch happens at the outer boundary; self-calls inside an embedded method are not redirected to the outer type's overrides.
In Go, if you need the same self-redirection effect as in this example, you must manually inject self via an interface field, and that is what the third line does.8
It seems more accurate to call this a deliberate choice in Go rather than a deficiency. When an embedded method does not redirect back to the outer type's overrides, as in this example, the open-recursion-based Fragile Base Class Problem simply does not arise. You can recreate similar coupling by explicitly injecting self through an interface, but then the wiring is visible in the code.
Personal note: quite a few articles refer to embedding as inheritance, but running the three lines above yourself will ensure you never confuse the two again. Don't bother arguing about the concept in programming communities. Few things waste more time than getting into a programming argument with a Korean.
The Fragile Base Class Problem
Open recursion has a cost. The moment a parent internally calls an overridable method, the order and number of those calls no longer stay confined to the implementation. A subclass can observe or depend on them, and even without being documented, they effectively become part of the subclass contract.
This phenomenon appears to have been formally studied: Mikhajlov and Sekerinski, in their 1998 ECOOP paper, defined the Fragile Base Class Problem and enumerated the conditions that must be satisfied to evolve a base class safely.9
To demonstrate this in action, two base classes have been prepared. Their externally observable behavior is identical; only their internal implementations differ.
// Base class v1. `AddRange` calls `Add` internally.
public class BagV1
{
protected readonly List<string> items = new List<string>();
public virtual void Add(string item) => items.Add(item);
public virtual void AddRange(IEnumerable<string> range)
{
foreach (var item in range)
{
Add(item); // Internal self-dispatch. This effectively becomes a contract.
}
}
public int Count => items.Count;
}
// Base class v2. The external contract is the same; only the internal implementation has changed.
public class BagV2
{
protected readonly List<string> items = new List<string>();
public virtual void Add(string item) => items.Add(item);
public virtual void AddRange(IEnumerable<string> range)
{
items.AddRange(range); // `Add` is no longer called along the way.
}
public int Count => items.Count;
}The subclass simply wanted to count how many items had been added.
public sealed class CountingV1 : BagV1
{
public int Added;
public override void Add(string item) { Added++; base.Add(item); }
public override void AddRange(IEnumerable<string> range) { base.AddRange(range); }
}
public sealed class CountingV2 : BagV2
{
public int Added;
public override void Add(string item) { Added++; base.Add(item); }
public override void AddRange(IEnumerable<string> range) { base.AddRange(range); }
}The result speaks for itself.
Base v1 Count=3 Added=3
Base v2 Count=3 Added=0
Not a single character of the child class changed.
Only the internal implementation of the base class changed, yet Added dropped from 3 to 0.Count is 3 in both cases. The public contract was honored, yet the count the subclass was tracking has vanished.
We need to diagnose the nature of the problem precisely. The base class author did not violate the public contract for ordinary users, because the fact that AddRange internally calls Add was not spelled out in the general public API contract. However, once a class is left open for inheritance, the story changes. Internal calls to overridable methods are observable by subclasses, so they should have been documented as part of the inheritance contract. The root of the problem is that internal implementation details like these get promoted to contract status through inheritance. This is precisely the structure Snyder identified in his 1986 paper when he noted that inheritance can undermine the benefits of encapsulation.10
Three practical rules emerge from this.
Rule | Reason |
|---|---|
Consciously limit internal calls to overridable methods | Prevents call order from becoming an implicit contract |
Fix the algorithm in a non-virtual private method and carve out override points as separate protected hooks | Makes it explicit what is and is not part of the contract |
Document when and how many times those hooks are called in the inheritance contract | Without documentation, subclasses have no choice but to guess at the behavior |
- Rule
Consciously limit internal calls to overridable methods
- Reason
Prevents call order from becoming an implicit contract
- Rule
Fix the algorithm in a non-virtual private method and carve out override points as separate protected hooks
- Reason
Makes it explicit what is and is not part of the contract
- Rule
Document when and how many times those hooks are called in the inheritance contract
- Reason
Without documentation, subclasses have no choice but to guess at the behavior
If the goal was simply for the subclass to count items, a delegation wrapper is safer than inheritance. Because the wrapper uses only the public API, it won't break if the base class's internals change. This is precisely the point where composition fits more accurately than inheritance.
Inheritance and subtyping are different things
Writing class B : A allows an instance of B to be used in place of A in most languages. Up to this point, it is purely a judgment made by the static type system. The compiler checks the type rules defined by the language, but it generally does not guarantee that the substitution preserves the program's semantic contracts. Whether the program remains correct after the substitution is a question at a higher level, and this is where the behavioral subtyping conditions formulated by Liskov and Wing come into play: properties proven for the supertype must be preserved in the subtype.11
This is not a problem unique to inheritance. Implementing an interface also permits substitution at the type-system level, and the obligation of behavioral subtyping follows just the same. However, if the supertype's contract guarantees that Add succeeds while the subtype alone rejects it, that violates substitutability. When the supertype contract explicitly permits throwing an exception for read-only collections, as with .NET's ICollection<T>.Add, throwing that exception cannot by itself be called an LSP violation.12
So the precise summary is as follows.
Statement | True? |
|---|---|
Inheritance creates a subtype relationship | In most languages, yes. What it creates, however, is permission for substitution at the type-system level |
If the type system permits substitution, behavioral subtyping follows | No. Static type conformance and preservation of behavioral contracts are separate issues |
Behavioral subtyping requires adherence to the semantic contracts of the supertype as well | True. This is something the author must uphold, not the compiler |
Therefore, inheritance is bad | No |
Therefore, if substitutability is not needed, do not use inheritance to create a type relationship | This is the correct conclusion |
- Statement
Inheritance creates a subtype relationship
- True?
In most languages, yes. What it creates, however, is permission for substitution at the type-system level
- Statement
If the type system permits substitution, behavioral subtyping follows
- True?
No. Static type conformance and preservation of behavioral contracts are separate issues
- Statement
Behavioral subtyping requires adherence to the semantic contracts of the supertype as well
- True?
True. This is something the author must uphold, not the compiler
- Statement
Therefore, inheritance is bad
- True?
No
- Statement
Therefore, if substitutability is not needed, do not use inheritance to create a type relationship
- True?
This is the correct conclusion
If you only want to reuse an implementation and have no need for substitutability, inheritance gives you one extra thing you did not ask for. C++ private inheritance or delegation is what fits that situation.
Multiple Inheritance and Linearization
When a class inherits from multiple parents, the same name can arrive through several different paths. This is the so-called Diamond Problem.
A
/ \
B C
\ /
DHow many copies of A's state does D hold, and which of A's methods gets called? Different languages answer these questions differently.
Language | Approach |
|---|---|
C++ | By default, two copies of A exist. |
Python | C3 linearization defines a single method resolution order. |
Java | Multiple inheritance from several classes is prohibited. If default methods from multiple interfaces conflict, the implementing class must override them; failing to do so results in a compile error.13 |
C# | Multiple inheritance from several classes is prohibited. When multiple interface default implementations are available, the language selects the single most specific one; if no such implementation exists, a compile error is raised. The implementing class must either implement the member directly or resolve the conflict explicitly within the interface hierarchy. 13 |
Ruby, Scala | Mixins are inserted according to linearization order. |
Rust | There is no inheritance; composition is done with traits |
- Language
C++
- Approach
By default, two copies of A exist.
virtualinheritance can merge them into one.
- Language
Python
- Approach
C3 linearization defines a single method resolution order.
- Language
Java
- Approach
Multiple inheritance from several classes is prohibited. If default methods from multiple interfaces conflict, the implementing class must override them; failing to do so results in a compile error.13
- Language
C#
- Approach
Multiple inheritance from several classes is prohibited. When multiple interface default implementations are available, the language selects the single most specific one; if no such implementation exists, a compile error is raised. The implementing class must either implement the member directly or resolve the conflict explicitly within the interface hierarchy. 13
- Language
Ruby, Scala
- Approach
Mixins are inserted according to linearization order.
- Language
Rust
- Approach
There is no inheritance; composition is done with traits
Linearization does not eliminate the problem; it merely resolves it by rule. When a class is added to the hierarchy, the order can change, causing code that nobody touched to behave differently.
Schärli et al., in their 2003 ECOOP paper, argued that single inheritance, multiple inheritance, and mixin inheritance each have conceptual and practical limitations, and proposed the original trait as a stateless unit of reusable behavior.14
This description applies to the trait model from that paper, not to the common rules of every language that uses the name trait today. Rust traits do not directly declare fields but can have default methods, associated types, and associated constants, while Scala traits can have concrete members and state.15 How conflicts are resolved also varies by language, so one should not generalize by saying "traits are always stateless and always resolve conflicts explicitly."
Decisions Made by Different Languages
Looking at the decisions languages have made about inheritance over the past thirty years reveals a clear direction.
Decision | Language | Semantics |
|---|---|---|
No multiple class inheritance | Java, C# | Rather than directly inheriting from multiple classes, relationships are formed through multiple interface implementations combined with separate default implementation rules .13 |
Methods are non-virtual by default | C++, C# | Makes overriding an explicit opt-in, independent of whether the class itself is inheritable |
Classes are | Kotlin | Makes extension opt-in by permission |
No class inheritance | Go, Rust | Go uses embedding and method sets; Rust uses traits and supertraits |
Default interface implementations | Java 8 and later, C# 8 and later | Provides reuse and compatibility paths that differ from class-based Implementation Inheritance |
- Decision
No multiple class inheritance
- Language
Java, C#
- Semantics
Rather than directly inheriting from multiple classes, relationships are formed through multiple interface implementations combined with separate default implementation rules .13
- Decision
Methods are non-virtual by default
- Language
C++, C#
- Semantics
Makes overriding an explicit opt-in, independent of whether the class itself is inheritable
- Decision
Classes are
finalby default- Language
Kotlin
- Semantics
Makes extension opt-in by permission
- Decision
No class inheritance
- Language
Go, Rust
- Semantics
Go uses embedding and method sets; Rust uses traits and supertraits
- Decision
Default interface implementations
- Language
Java 8 and later, C# 8 and later
- Semantics
Provides reuse and compatibility paths that differ from class-based Implementation Inheritance
There have also been arguments for replacing inheritance-based Polymorphism entirely with value semantics and type erasure.16 In practice, though, mainstream languages did not go that route.
It would be hard to say there was one unified direction. Java and C# blocked multiple class Inheritance but retained multiple interfaces and default implementation paths; C++ made non-virtual methods the default; Kotlin closed classes and members by default. Go and Rust opted out of class Inheritance altogether, offering embedding, traits, and interfaces instead. A more precise common thread is that rather than funneling all extension boundaries into a single inheritance syntax, each language sought to explicitly separate the boundaries among Inheritance, contracts, and Composition.
The prescription of Composition over inheritance rose and fell like a trend, but the direction of separating Inheritance, overriding, contracts, and Composition into more explicit choices has been absorbed into the syntax and defaults of languages and endures there. The next generation will simply use those languages without having to study that debate separately.
A personal note: Korea is a different story, at least in my experience. Korean SI (system integration) shops carry deeply entrenched legacy practices, and the way cohesion and coupling are understood there is, to put it charitably, unusual from the ground up. Whether this stems from differences in the Korean language's structure I cannot say. I can't speak for Korea's large tech companies, but the mid-tier IT codebases I have seen show none of these distinctions. I have been genuinely startled by how much multiple Inheritance was in use.
When is Inheritance the right choice
Inheritance is a natural fit when any of the following conditions apply.
Condition | Explanation |
|---|---|
You want to fix an algorithm's skeleton and expose only certain steps | Template Method. Open recursion is intrinsically necessary here.17 |
The framework requires inheritance-based extension points | UI lifecycle hooks, game engine components, test fixtures |
The type hierarchy itself is central to the domain model | Expression trees, abstract syntax trees, simulating algebraic data types |
The parent and child are maintained by the same team within the same module | When the base class changes, the subclasses can be updated together |
A stable subtype relationship genuinely exists | Substitutability is required and will remain so going forward |
- Condition
You want to fix an algorithm's skeleton and expose only certain steps
- Explanation
Template Method. Open recursion is intrinsically necessary here.17
- Condition
The framework requires inheritance-based extension points
- Explanation
UI lifecycle hooks, game engine components, test fixtures
- Condition
The type hierarchy itself is central to the domain model
- Explanation
Expression trees, abstract syntax trees, simulating algebraic data types
- Condition
The parent and child are maintained by the same team within the same module
- Explanation
When the base class changes, the subclasses can be updated together
- Condition
A stable subtype relationship genuinely exists
- Explanation
Substitutability is required and will remain so going forward
Conversely, inheritance is the wrong tool when the following signals appear (these reflect my own experience, of course).
Signal | Alternative |
|---|---|
You only want to reuse a few lines of the parent's code | Extract function, delegation |
The child class name grows long as a compound | Composition along axes of variation |
You need to know the parent's internal implementation to use the child | Delegation wrapper |
You need to change behavior at runtime | Strategy object |
The parent belongs to another team or an external library | Delegation wrapper |
- Signal
You only want to reuse a few lines of the parent's code
- Alternative
Extract function, delegation
- Signal
The child class name grows long as a compound
- Alternative
Composition along axes of variation
- Signal
You need to know the parent's internal implementation to use the child
- Alternative
Delegation wrapper
- Signal
You need to change behavior at runtime
- Alternative
Strategy object
- Signal
The parent belongs to another team or an external library
- Alternative
Delegation wrapper
The last item is particularly important. If you inherit a public class managed by another team and rely on its undocumented internal call order, your code will silently break when that implementation changes in the next version. The other party may not have violated any documented inheritance contract, leaving you with no recourse. Conversely, if that call order was documented as part of the inheritance contract, the situation changes entirely: that would be a breach of contract on their side. So before inheriting an external class, the question to ask is whether that class is documented for inheritance.
Personal note: code that extends an external library through inheritance is the first thing to break during an upgrade. Yet when you first write it, it looks like the fastest path forward — though it depends on the amount involved.
For well-paying, friendly clients, give them your full attention; for smaller amounts, use a library and deliver quickly. Be kind to programmers. They will write your code in a way that can be maintained.
What to decide first when opening up inheritance
Bloch covers inheritance in two separate items in Effective Java. One is to favor Composition, and the other is to design and document for inheritance, or else prohibit it.18 The second comes up more often in practice.
A class designed for inheritance requires more than just marking a few methods virtual. You must first decide what subclasses are allowed to rely on and how far the base class is responsible.
What to check | Reason |
|---|---|
The timing and order in which overridable methods are called | Subclasses can observe and depend on that flow |
Reduce self-calls and extract them into private helpers | This limits how much internal calls can grow into hidden contracts |
Do not call overridable methods from constructors | They may be called before the derived object has finished initializing |
Expose extension points narrowly as | You can leave only the points you intend to expose open and keep everything else closed |
Do not expose state as | Changing the internal representation becomes difficult |
If you have no plans to allow inheritance, seal the class with | You can keep the default on the safe side |
- What to check
The timing and order in which overridable methods are called
- Reason
Subclasses can observe and depend on that flow
- What to check
Reduce self-calls and extract them into private helpers
- Reason
This limits how much internal calls can grow into hidden contracts
- What to check
Do not call overridable methods from constructors
- Reason
They may be called before the derived object has finished initializing
- What to check
Expose extension points narrowly as
protectedhooks- Reason
You can leave only the points you intend to expose open and keep everything else closed
- What to check
Do not expose state as
protected- Reason
Changing the internal representation becomes difficult
- What to check
If you have no plans to allow inheritance, seal the class with
sealed- Reason
You can keep the default on the safe side
The third item is a frequent pitfall: you should not call overridable methods from a constructor because the derived object's initialization has not yet completed.
The exact order varies by language.
In Java, the parent constructor runs before the child's fields are initialized. In C#, the child's field initializers run before the parent constructor, but the child constructor body runs after the parent constructor finishes.6
So an override called from a C# base constructor can see the values set by field initializers, but cannot yet see state that the child constructor body will set. In either case, the risk is the same: you are observing a partially initialized object.
Performance
Inheritance and virtual dispatch are distinct concepts, but in practice they are almost always used together, and in tight inner loops their cost can become visible.
Cost | Description |
|---|---|
Virtual call | Depending on the runtime implementation, calls pass through indirect call mechanisms, dispatch tables, inline caches, and so on |
Inlining impediment | When the call target is not known statically, inlining may be difficult |
Branch prediction | If the call target changes frequently, the branch predictor may mispredict |
Object size | Depending on the implementation, dispatch information must be stored in the object or its type metadata. Not every language attaches a vtable pointer to every object. |
- Cost
Virtual call
- Description
Depending on the runtime implementation, calls pass through indirect call mechanisms, dispatch tables, inline caches, and so on
- Cost
Inlining impediment
- Description
When the call target is not known statically, inlining may be difficult
- Cost
Branch prediction
- Description
If the call target changes frequently, the branch predictor may mispredict
- Cost
Object size
- Description
Depending on the implementation, dispatch information must be stored in the object or its type metadata. Not every language attaches a vtable pointer to every object.
One point worth stating precisely: modern compilers and runtimes attempt devirtualization and inlining when only one concrete type is observed at a call site. So virtual calls are not always slow; the cost manifests at points where multiple dispatch targets diverge and optimizations break down. The real takeaway is measurement. Removing inheritance for performance reasons without measurement is almost always unfounded and risks being premature optimization.
The opposite direction exists too, but with caveats. Composing with reference-type objects can add one extra level of indirection. By contrast, composition using value types or directly embedded members can co-locate data inside the outer object. Because the actual memory layout of inheritance versus composition depends on the language and runtime, neither side universally wins on data locality. Only when comparing reference-based composition against base-class embedding in a specific implementation might inheritance have one fewer level of indirection.19
When there are many objects and a hot loop is the bottleneck, switching to an array or SoA (Structure of Arrays) layout often yields a larger gain than choosing between inheritance and composition. That belongs to the data-oriented design conversation.
My criteria
These are the checklists I use when deciding whether to reach for inheritance.
Question | If yes |
|---|---|
Does parent code need to call back into a child's implementation? | There is a strong reason to consider inheritance |
Is substitutability actually required? | An explicit type abstraction such as an interface, protocol, or base type is needed |
Can I modify the parent myself? | If not, use a delegation wrapper |
Can the child be used without knowing the parent's internal implementation? | If not, the design is underspecified |
Is the child class name a compound expression? | Split the axis using Composition. |
Are the override points documented? | If not, start by verifying those boundaries. |
Will this hierarchy remain a single axis going forward? | If not, the inheritance tree will soon hit a dead end. |
- Question
Does parent code need to call back into a child's implementation?
- If yes
There is a strong reason to consider inheritance
- Question
Is substitutability actually required?
- If yes
An explicit type abstraction such as an interface, protocol, or base type is needed
- Question
Can I modify the parent myself?
- If yes
If not, use a delegation wrapper
- Question
Can the child be used without knowing the parent's internal implementation?
- If yes
If not, the design is underspecified
- Question
Is the child class name a compound expression?
- If yes
Split the axis using Composition.
- Question
Are the override points documented?
- If yes
If not, start by verifying those boundaries.
- Question
Will this hierarchy remain a single axis going forward?
- If yes
If not, the inheritance tree will soon hit a dead end.
When comparing inheritance and Composition purely for implementation reuse, the answer leans strongly toward Composition if the base code never needs to call back into child implementations. That said, if nominal subtyping, framework extension points, or a closed type hierarchy are separately required, the remaining questions still deserve attention.
In summary
The key behavioral difference this document has emphasized when comparing Implementation Inheritance with Composition is implicit open recursion. The distinction is whether the language automatically wires the connection by which base code calls back into child implementations. However, because the same effect can be achieved through self-injection or other composition techniques, calling this a logical monopoly of inheritance is not accurate.
The price of this, of course, is that the order and number of overridable self calls, along with parts of the internal structure observable to child classes such as protected state and construction order, become part of the subclass contract. Whatever the choice, there are trade-offs.
This does not mean the base class cannot be changed, but if you do not know which points child classes depend on, changes can break them silently.
From an implementation-reuse perspective, the first question to ask is whether open recursion is needed. If it is, consider inheritance, but keep extension points narrow and document those boundaries. If it is not, delegation is simpler. However, if nominal type relationships or framework contracts are separately required, those conditions should be examined together.
"Favor Composition over inheritance" should be understood not as a directive to avoid inheritance altogether, but rather as a warning not to bundle type relationships and extension contracts along with implementation reuse as a shortcut.
See also
Composition
Object-Oriented Programming
Programming Patterns
Strategy Pattern
Decorator Pattern
Algebraic Data Types
Data-Oriented Design
Footnotes
- William R. Cook; Walter L. Hill; Peter S. Canning. "Inheritance Is Not Subtyping". POPL 1990. DOI: 10.1145/96709.96721. A classic discussion arguing that inheritance and subtyping should not be treated as the same thing. ↩
- Gilad Bracha; William R. Cook. "Mixin-based Inheritance". OOPSLA/ECOOP 1990, 303-311. DOI: 10.1145/97945.97982. Treats mixins as a variant of inheritance and as a means of composing abstract subclasses. ↩
- Ole-Johan Dahl; Kristen Nygaard. "The Development of the SIMULA Languages". History of Programming Languages, 1978. DOI: 10.1145/800025.1198392. The designers themselves explain what needs in simulation languages gave rise to classes, subclasses, and the prefixing mechanism. ↩
- Daniel H. H. Ingalls. "The Evolution of Smalltalk: From Smalltalk-72 through Squeak". Proceedings of the ACM on Programming Languages, 4(HOPL), Article 85, 2020. DOI: 10.1145/3386335. Note that the HOPL IV conference itself was held in June 2021 co-located with PLDI, so it is best not to conflate the publication year with the conference year when citing this work. The designer's retrospective confirms that Smalltalk-72 had no inheritance, and that inheritance in Smalltalk-76 emerged as a solution to the problem of copying and sharing common behavior. ↩
- Bjarne Stroustrup. "A History of C++: 1979–1991". HOPL 1993. Describes the progression from C with Classes to C++, the influence of Simula, and design decisions such as access control and multiple inheritance. ↩
- Language specifications: Java Language Specification, Java SE 8, C# language specification — Classes. Reference documents for verifying the rules governing class inheritance, interfaces, and virtual methods in Java and C#. ↩
- Antero Taivalsaari. "On the Notion of Inheritance". ACM Computing Surveys, 28(3), 438-479, 1996. DOI: 10.1145/243439.243441. A survey paper documenting the lack of consensus even among researchers on what inheritance is and how it should be used. ↩
- The Go Programming Language Specification. Defines Go's embedded fields, promoted methods, method sets, and interface dispatch rules. ↩
- Leonid Mikhajlov; Emil Sekerinski. "A Study of The Fragile Base Class Problem". ECOOP 1998, LNCS 1445. DOI: 10.1007/BFb0054099. Formally defines the phenomenon in which derived classes break when a base class is modified, and proposes safety conditions. ↩
- Alan Snyder. "Encapsulation and Inheritance in Object-Oriented Programming Languages". OOPSLA 1986. DOI: 10.1145/28697.28702. An early discussion of how inheritance can undermine the benefits of encapsulation. ↩
- Barbara Liskov; Jeannette Wing. "A Behavioral Notion of Subtyping". ACM Transactions on Programming Languages and Systems, 16(6), 1811-1841, 1994. DOI: 10.1145/197320.197383. ↩
- .NET API — `ICollection<T>.Add`. The contract specifying that a
NotSupportedExceptionmay be thrown for read-only collections can be confirmed here. ↩ - Language-specific documentation on default interface implementations and multiple inheritance: Oracle Java Tutorial — Multiple Inheritance of State, Implementation, and Type, C# language specification — Interfaces. While neither Java nor C# supports inheriting from multiple classes, both handle the implementation of multiple interfaces and conflicts among default implementations according to each language's own rules. Section 19.4.10 "Most specific implementation" of the C# specification mandates a unique most-specific implementation and requires a compile error when no such implementation exists. ↩
- Nathanael Schärli; Stéphane Ducasse; Oscar Nierstrasz; Andrew P. Black. "Traits: Composable Units of Behaviour". ECOOP 2003, 248-274. DOI: 10.1007/978-3-540-45070-2_12. The trait model proposed in this paper treats stateless collections of methods as the unit of reuse. This does not mean that all
traitconstructs in modern languages carry the same restrictions. ↩ - Language-specific trait and inheritance specifications: The Rust Reference — Traits, Scala 2.13 Language Specification — Classes and Objects, Kotlin — Inheritance. These confirm that
traitandclassdiffer across languages in terms of state, default implementations, and inheritability. ↩ - Sean Parent. "Inheritance Is The Base Class of Evil". GoingNative 2013. A talk proposing value semantics and type erasure as alternatives to inheritance-based polymorphism. ↩
- Erich Gamma; Richard Helm; Ralph Johnson; John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. Contains both the principle of preferring object Composition over class inheritance and patterns such as Template Method that presuppose inheritance. ↩
- Joshua Bloch. Effective Java, 3rd edition. Addison-Wesley, 2018. Item 18 "Favor composition over inheritance", Item 19 "Design and document for inheritance or else prohibit it". ↩
- C# reference types, C# value types. The language rules specifying that reference types store references while value types directly contain their values can be confirmed here. ↩