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.