Back to Wiki

Inheritance

Inheritance

Jeong Dongwoo · 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 self calls in base code resolve back to the child implementation

The Cost

Overridable self calls and the portion of state exposed to subclasses effectively become the subclass contract

The Criterion

Is this resolution actually needed, or is code reuse the real goal?

Item

What Implementation Inheritance Can Provide

Summary

Open recursion: virtual self calls in base code resolve back to the child implementation

Item

The Cost

Summary

Overridable self calls 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 mixin inheritance.2

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 self rebinding

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 self rebinding

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 virtual functions in the transition to C++

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 final

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 virtual functions 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.

C#
// 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.

Text
Inheritance Greet() = hello from Derived
Composition Delegation Greet() = hello from BaseComponent
Explicit `self` Injection Greet() = hello from WrapperExplicit

The 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.

Go
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.

Text
Embedding d.Name() = Derived
Embedding d.Greet() = hello from Base
Explicit self injection e.Greet() = hello from DerivedExplicit

d.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.

Go
type Greeter interface {
	Name() string
	Greet() string
}

var g Greeter = Derived{}
Text
Concrete type d.Name() = Derived
Concrete type d.Greet() = hello from Base
Interface g.Name() = Derived
Interface g.Greet() = hello from Base

The 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.

C#
// 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.

C#
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.

Text
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.

Loading animated diagram...
Text
      A
     / \
    B   C
     \ /
      D

How 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. virtual inheritance can merge them into one.

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. virtual inheritance 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 final by default

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 final by 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 protected hooks

You can leave only the points you intend to expose open and keep everything else closed

Do not expose state as protected

Changing the internal representation becomes difficult

If you have no plans to allow inheritance, seal the class with sealed

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 protected hooks

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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#.
  7. 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.
  8. The Go Programming Language Specification. Defines Go's embedded fields, promoted methods, method sets, and interface dispatch rules.
  9. 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.
  10. 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.
  11. 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.
  12. .NET API — `ICollection<T>.Add`. The contract specifying that a NotSupportedException may be thrown for read-only collections can be confirmed here.
  13. 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.
  14. 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 trait constructs in modern languages carry the same restrictions.
  15. Language-specific trait and inheritance specifications: The Rust Reference — Traits, Scala 2.13 Language Specification — Classes and Objects, Kotlin — Inheritance. These confirm that trait and class differ across languages in terms of state, default implementations, and inheritability.
  16. 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.
  17. 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.
  18. 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".
  19. 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.
Inheritance