Concept · Code design

Composition over inheritance

Build behaviour by combining small objects that hold and delegate to each other, rather than by extending class hierarchies.

2 minfavor compositionfragile base classstruct embeddinghas-a vs is-a

Composition over inheritance, popularised by the Design Patterns book (1994), advises that objects gain behaviour by holding other objects that provide it, rather than by inheriting it from a parent class.

The problem with deep inheritance

Inheritance couples a subclass to its parent's implementation, not just its interface:

  • Fragile base class: a change in the parent — even an internal one, like which method calls which — can silently break subclasses.
  • Rigid hierarchies: behaviours that don't fit one tree force awkward choices. Is FlyingFish a Fish or a Bird? Real requirements rarely respect a single taxonomy.
  • Inherited baggage: subclasses get every method of the parent, including ones that don't make sense for them — a classic route to Liskov substitution violations.

Composition instead

Compose an object from parts that each do one thing, behind small interfaces:

go
type Notifier struct {
	sender Sender       // email, SMS, push — swappable
	limit  *RateLimiter // shared policy
	log    *slog.Logger
}

func (n *Notifier) Notify(ctx context.Context, userID, msg string) error {
	if !n.limit.Allow(userID) {
		return ErrRateLimited
	}
	return n.sender.Send(ctx, userID, msg)
}

Each part can be replaced, tested or reused independently, and new combinations need no new subclass. Patterns like strategy and decorator are composition by design.

In Go and TypeScript

Go has no inheritance. Struct embedding looks similar but is composition with automatic method forwarding: the embedded type doesn't know about the outer type, and there's no virtual dispatch from the inner type back to the outer one. Interfaces provide polymorphism without a hierarchy.

TypeScript supports classes and extends, but idiomatic modern TypeScript — and React since hooks — leans on functions, interfaces and composition. One level of inheritance from a framework base class is usually fine; deep hierarchies of your own rarely are.

Inheritance still fits genuine, stable "is-a" relationships with little shared state. The default should be composition. See the SOLID lesson.

Learn it in depth