Composition vs Inheritance - Choosing the Right Relationship in C#
Vaibhav • September 10, 2025
In the previous article, we explored class design principles - how to structure classes for clarity, cohesion, and maintainability. We discussed encapsulation, single responsibility, constructor validation, and API clarity. Now, we turn to two foundational techniques for structuring relationships between classes: composition and inheritance.
Both composition and inheritance allow you to build complex systems from simpler parts. But they serve different purposes and come with different trade-offs. In this article, we’ll explore what each technique means, when to use one over the other, and how to apply them effectively in your C# applications.
Understanding Inheritance
Inheritance is a mechanism that allows one class to derive from another. The derived class inherits the fields, properties, and methods of the base class and can extend or override them.
public class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}
In this example, Dog
inherits from Animal
. It can call Eat()
because that
method is defined in the base class. Inheritance expresses an “is-a” relationship - a dog is an animal.
You can also override methods in the derived class using the virtual
and override
keywords:
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal sound");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
This allows polymorphic behavior - different classes can provide their own implementation of a shared method.
Inheritance is useful when multiple classes share common behavior and you want to reuse code. But it creates tight coupling and can lead to fragile hierarchies if overused.
Understanding Composition
Composition is a design technique where one class contains instances of other classes. Instead of inheriting behavior, it delegates behavior to its components. This expresses a “has-a” relationship.
public class Engine
{
public void Start()
{
Console.WriteLine("Engine started");
}
}
public class Car
{
private Engine engine = new Engine();
public void StartCar()
{
engine.Start();
Console.WriteLine("Car is running");
}
}
Here, Car
has an Engine
. It uses composition to delegate the Start()
behavior. This keeps the classes loosely coupled and easier to maintain.
Composition allows you to build complex behavior by combining simple, focused classes. Each component can be tested and reused independently.
Choosing Between Composition and Inheritance
The choice between composition and inheritance depends on the nature of the relationship and the design goals. Use inheritance when:
- There is a clear “is-a” relationship.
- You want to reuse base class behavior.
- You need polymorphism.
Use composition when:
- There is a “has-a” or “uses-a” relationship.
- You want to build behavior from smaller components.
- You want to avoid tight coupling and fragile hierarchies.
Favor composition over inheritance when possible. It leads to more flexible and maintainable designs.
Extending Behavior with Composition
Composition allows you to extend behavior without modifying existing classes. You can inject different components to change behavior dynamically.
public interface IPrinter
{
void Print(string message);
}
public class ConsolePrinter : IPrinter
{
public void Print(string message)
{
Console.WriteLine(message);
}
}
public class Logger
{
private IPrinter printer;
public Logger(IPrinter printer)
{
this.printer = printer;
}
public void Log(string message)
{
printer.Print($"Log: {message}");
}
}
The Logger
class uses composition to delegate printing. You can pass different implementations of
IPrinter
to change how logging works - without changing the Logger
class.
Extending Behavior with Inheritance
Inheritance allows you to extend behavior by overriding methods. This is useful when you want to specialize a base class.
public class Printer
{
public virtual void Print(string message)
{
Console.WriteLine(message);
}
}
public class TimestampPrinter : Printer
{
public override void Print(string message)
{
Console.WriteLine($"[{DateTime.Now}] {message}");
}
}
The TimestampPrinter
class overrides Print()
to add a timestamp. This is a clean way to
extend behavior when the base class is designed for inheritance.
Avoiding Inheritance Pitfalls
Inheritance can lead to problems if used carelessly:
- Base classes may expose internal details that should be hidden.
- Changes to the base class can break derived classes.
- Deep inheritance hierarchies are hard to understand and maintain.
To avoid these issues:
- Keep base classes small and focused.
- Use
protected
access for members intended for inheritance. - Document which methods are safe to override.
If you find yourself overriding many methods just to disable or change behavior, consider switching to composition.
Combining Composition and Inheritance
You don’t have to choose one technique exclusively. Many designs use both inheritance and composition together. For example, you might use inheritance for polymorphism and composition for behavior reuse.
public abstract class Shape
{
public abstract double Area();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
public class ShapePrinter
{
public void PrintArea(Shape shape)
{
Console.WriteLine($"Area: {shape.Area()}");
}
}
Here, Circle
uses inheritance to implement Shape
, and ShapePrinter
uses
composition to work with any Shape
. This design is flexible and extensible.
Summary
Composition and inheritance are two powerful techniques for structuring relationships between classes in C#. Inheritance expresses an “is-a” relationship and enables polymorphism. Composition expresses a “has-a” relationship and promotes flexibility and reuse.
We explored how to use inheritance to share and override behavior, and how to use composition to delegate behavior and build modular systems. We discussed when to choose each technique, how to avoid inheritance pitfalls, and how to combine both approaches effectively.
As you continue designing classes, favor composition for flexibility and maintainability. Use inheritance when there is a clear conceptual hierarchy and polymorphism is needed. Thoughtful use of these techniques leads to cleaner, more robust designs.
In the next article, we’ll explore Value Types vs Reference Types - how C# handles memory and identity for different kinds of data.