Intro

Inheritance binds a derived type to a base type’s contract and implementation. Composition binds an object to a collaborator’s contract and delegates work to it. Use inheritance when the relationship is a genuine subtype with stable shared invariants; use composition when behavior varies independently or may change at runtime. Reuse alone is not enough to justify a type hierarchy.

The decision

public abstract class Robot
{
    protected Robot(IMovementStrategy movement) => Movement = movement;
 
    protected IMovementStrategy Movement { get; }
    public abstract Task PerformTaskAsync(CancellationToken cancellationToken);
}
 
public sealed class WaiterRobot(IMovementStrategy movement) : Robot(movement)
{
    public override Task PerformTaskAsync(CancellationToken cancellationToken) =>
        DeliverOrderAsync(cancellationToken);
}

WaiterRobot is a Robot: callers can rely on the robot lifecycle and every subtype shares its invariants. Movement is composed because A*, waypoints, or direct movement can vary without creating AStarWaiterRobot, WaypointWaiterRobot, and every other cross-product subtype.

SignalPrefer inheritancePrefer composition
RelationshipA durable “is-a” subtypeA “has-a” collaborator
Shared stateBase class protects a real invariantState belongs to the delegated capability
VariationFixed at type constructionSwappable by configuration or request
Change costBase changes are safe for every subtypeImplementations should evolve independently
ReuseConsequence of the hierarchyExplicit delegation is the intended reuse

Fragile base classes

A derived class can depend on undocumented call order, protected state, or a virtual method invoked from a constructor. A later base-class change can compile cleanly and still break the subtype. Keep extension points small, document their preconditions and postconditions, and prefer sealed when a class was not designed for derivation.

Deep hierarchies multiply this risk. If an override suppresses base behavior, throws for a supported operation, or needs knowledge of base internals, the subtype is probably false. Flatten the hierarchy into capability interfaces and composed policies.

Tradeoffs

Composition needs constructor wiring and delegation methods. Inheritance can be concise and lets a framework supply a template lifecycle. Pay the inheritance coupling only when callers benefit from the subtype relation and the base owns stable invariants; otherwise the explicit wiring is cheaper than a fragile hierarchy.

Questions

References