TL;DR
- Prefer composition over inheritance for flexible, maintainable C# code.
- Use interfaces and dependency injection to compose behaviors instead of deep class hierarchies.
- Composition makes code easier to test, extend, and adapt to changing requirements.
- Use inheritance only for clear “is-a” relationships with shallow, stable hierarchies.
- Refactor rigid inheritance trees by extracting behaviors into separate classes or interfaces.
Imagine you’re building a car in code. You start with a CarWithElectricEngine
class, then create a SportyCarWithElectricEngine
that inherits from it, and finally a LuxurySportyCarWithElectricEngine
subclass. Sounds organized, right? But then your boss asks for a hybrid engine option… or a diesel version. Suddenly your neat inheritance tree becomes a tangled mess.
This scenario plays out in C# projects all the time when we go overboard with inheritance. Let’s look at why composition is often a better choice, and how it can make your code way more flexible.
The Inheritance Trap We’ve All Fallen Into
You’ve probably seen this pattern before:
public abstract class Employee
{
public string Name { get; set; }
public abstract decimal CalculatePay();
}
public class SalariedEmployee : Employee
{
public decimal Salary { get; set; }
public override decimal CalculatePay() => Salary / 12;
}
public class HourlyEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public override decimal CalculatePay() => HourlyRate * HoursWorked;
}
This looks clean at first, but here’s where it gets messy. What if you need a contractor who gets paid hourly but also receives bonuses? Or a salaried employee with overtime? You either end up with multiple inheritance (which C# doesn’t support) or a deep, rigid hierarchy that’s painful to change.
Composition: Building with LEGO Blocks
Instead of saying “a salaried employee is a employee,” composition says “an employee has a pay calculator.” Here’s the same example using composition:
// Define behavior contracts
public interface IPayCalculator
{
decimal CalculatePay();
}
// Implement specific behaviors
public class SalaryCalculator : IPayCalculator
{
private readonly decimal _monthlySalary;
public SalaryCalculator(decimal monthlySalary) => _monthlySalary = monthlySalary;
public decimal CalculatePay() => _monthlySalary;
}
public class HourlyCalculator : IPayCalculator
{
private readonly decimal _hourlyRate;
private readonly int _hoursWorked;
public HourlyCalculator(decimal hourlyRate, int hoursWorked)
{
_hourlyRate = hourlyRate;
_hoursWorked = hoursWorked;
}
public decimal CalculatePay() => _hourlyRate * _hoursWorked;
}
// Compose the employee with the behavior it needs
public class Employee
{
public string Name { get; set; }
private readonly IPayCalculator _payCalculator;
public Employee(string name, IPayCalculator payCalculator)
{
Name = name;
_payCalculator = payCalculator;
}
public decimal CalculatePay() => _payCalculator.CalculatePay();
}
Now creating employees is straightforward:
var salariedEmployee = new Employee("John", new SalaryCalculator(5000));
var hourlyEmployee = new Employee("Jane", new HourlyCalculator(25, 160));
Why This Actually Makes Your Life Easier
Testing is a breeze. Want to test your Employee
class? Just mock the IPayCalculator
and you’re good to go. No more wrestling with complex inheritance chains in your unit tests.
Need a new pay type? No problem. Got salespeople who earn commissions? Just create a CommissionCalculator
without touching any existing code.
Change things on the fly. You can swap out pay calculators at runtime when business rules change. Try doing that with inheritance!
Keep things separate. Your Employee
class doesn’t need to know how pay is calculated, it just passes that job to whatever calculator you give it.
Inheritance vs. Composition: A Side-by-Side Look
If you want to get into the nitty-gritty, here’s how these two approaches stack up:
Aspect | Inheritance | Composition |
---|---|---|
Reusability | Through class hierarchy | Through object collaboration |
Flexibility | Rigid, fixed at compile-time | Highly flexible, can swap at runtime |
Coupling | Tight (parent-child dependency) | Loose (interfaces, delegation) |
Testing | Hard to isolate dependencies | Easy to mock and substitute |
Runtime Behavior Change | Difficult | Easy (pass different implementations) |
Example Pattern | Template Method, Abstract Class | Strategy, Decorator |
Looking at this table, it’s easy to see why composition is usually the better bet when you’re building software that needs to adapt to changing requirements or be thoroughly tested.
C# Features That Make Composition Even Better
The newer versions of C# have some cool features that make composition easier than ever. Here are some of my favorites that work great with composition:
// Records for immutable composed objects
public record Employee(string Name, IPayCalculator PayCalculator);
// Default interface implementations (C# 8+)
public interface ILogger
{
// Default behavior that implementers can override or use as-is
void Log(string message) => Console.WriteLine(message);
}
// Extension methods as an alternative to inheritance
public static class StringExtensions
{
public static bool IsValidEmail(this string email) =>
email?.Contains("@") ?? false;
}
// Func<> delegates for flexible behavior composition
public class NotificationService
{
private readonly Func<string, bool> _emailValidator;
public NotificationService(Func<string, bool> emailValidator)
{
_emailValidator = emailValidator;
}
public void SendNotification(string email, string message)
{
if (_emailValidator(email))
{
// Send notification
}
}
}
When you combine these with ASP.NET Core’s built-in dependency injection, composition feels really natural in modern C# projects.
When Inheritance Actually Makes More Sense
Look, I’m not saying inheritance is evil. There are times when it’s the right tool for the job, though interfaces are usually the better choice:
Dead-simple base classes - When you have common stuff that pretty much never changes, like ID and timestamp fields in a database entity.
Super performance-sensitive code - If you’re counting microseconds and the tiny overhead of delegation matters (honestly, rare in most business apps).
Working with opinionated frameworks - Some frameworks just expect inheritance (like when you’re creating ASP.NET controllers).
Simple type relationships - When you’re just saying “this is a type of that” without sharing complex behavior.
// Simple example where inheritance is reasonable
public abstract class EntityBase
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
// These entities just need the common properties
public class Customer : EntityBase { /* Customer-specific properties */ }
public class Product : EntityBase { /* Product-specific properties */ }
In cases like this with no complex behavior sharing, inheritance can be more straightforward.
How Composition Helps You Follow SOLID Principles
Composition isn’t just a nice-to-have, it actually helps you follow those SOLID principles you’ve heard about. Here’s what I mean:
Single Responsibility Principle (SRP)
With composition, each component class can focus on exactly one responsibility:
// Each class has one job
public class OrderValidator { /* Validates orders */ }
public class OrderProcessor { /* Processes valid orders */ }
public class OrderNotifier { /* Sends notifications */ }
// The OrderService composes these focused components
public class OrderService
{
private readonly OrderValidator _validator;
private readonly OrderProcessor _processor;
private readonly OrderNotifier _notifier;
// Components injected via constructor
public OrderService(
OrderValidator validator,
OrderProcessor processor,
OrderNotifier notifier)
{
_validator = validator;
_processor = processor;
_notifier = notifier;
}
}
Open/Closed Principle (OCP)
Composition allows you to extend behavior without modifying existing code:
// Original system
public interface IPaymentProcessor { void ProcessPayment(Order order); }
public class CreditCardProcessor : IPaymentProcessor { /* implementation */ }
// Extend with new payment method WITHOUT modifying existing code
public class PayPalProcessor : IPaymentProcessor { /* implementation */ }
// The payment service remains unchanged
public class PaymentService
{
private readonly IPaymentProcessor _processor;
public PaymentService(IPaymentProcessor processor) => _processor = processor;
}
Dependency Inversion Principle (DIP)
Composition naturally leads to depending on abstractions rather than concrete implementations:
// High-level module depends on abstraction
public class ShoppingCart
{
private readonly IInventoryChecker _inventory;
public ShoppingCart(IInventoryChecker inventory) => _inventory = inventory;
public bool CanAddItem(string productId, int quantity)
{
return _inventory.IsInStock(productId, quantity);
}
}
// Low-level module implements the abstraction
public class SqlInventoryChecker : IInventoryChecker { /* implementation */ }
Where You’ve Already Seen Composition (Even If You Didn’t Notice)
Composition is all over modern C# codebases. You’ve probably used these examples without even thinking about it:
ASP.NET Core Middleware Pipeline
The entire ASP.NET Core middleware system is built on composition:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
// Each middleware component is composed into the pipeline
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Logging Providers
The .NET logging system uses composition to support multiple logging destinations:
public class Program
{
public static void Main()
{
var host = Host.CreateDefaultBuilder()
.ConfigureLogging(logging =>
{
// Compose multiple logging providers
logging.AddConsole();
logging.AddDebug();
logging.AddApplicationInsights();
logging.AddSeq("http://seq-server");
})
.Build();
host.Run();
}
}
Game Development with Entity Component Systems
Many game engines use composition over inheritance for flexibility:
// Instead of a deep hierarchy like GameObject -> Character -> Player
// Composition allows mixing and matching capabilities
var player = new GameObject("Player");
player.AddComponent<RenderComponent>();
player.AddComponent<PhysicsComponent>();
player.AddComponent<PlayerInputComponent>();
player.AddComponent<HealthComponent>();
// A different entity with different components
var tree = new GameObject("Tree");
tree.AddComponent<RenderComponent>();
tree.AddComponent<PhysicsComponent>(); // Shares some components
// But no input or health components
Converting from Inheritance to Composition: A Real Example
Let’s walk through how you might actually convert some inheritance-based code to use composition instead:
Before: Inheritance-Based Approach
// Base class with flying behavior
public abstract class Bird
{
public string Name { get; set; }
public virtual void MakeSound() => Console.WriteLine("Tweet!");
// Problem: Not all birds can fly, but it's in the base class
public virtual void Fly() => Console.WriteLine("Flying high!");
public void Eat() => Console.WriteLine("Eating seeds");
}
// Works fine for flying birds
public class Sparrow : Bird
{
public override void MakeSound() => Console.WriteLine("Chirp chirp!");
}
// But what about birds that can't fly?
public class Penguin : Bird
{
public override void MakeSound() => Console.WriteLine("Honk!");
// Forced to override with inappropriate behavior
public override void Fly() =>
throw new InvalidOperationException("Penguins can't fly!");
}
// And what about ostriches? And kiwis? You'll need to override Fly() for each...
After: Composition-Based Approach
// Step 1: Extract behavior interfaces
public interface IFlyBehavior
{
void Fly();
}
public interface ISoundBehavior
{
void MakeSound();
}
// Step 2: Create concrete implementations
public class NormalFlight : IFlyBehavior
{
public void Fly() => Console.WriteLine("Flying high!");
}
public class NoFlight : IFlyBehavior
{
public void Fly() => Console.WriteLine("This bird can't fly!");
}
public class Chirping : ISoundBehavior
{
public void MakeSound() => Console.WriteLine("Chirp chirp!");
}
public class Honking : ISoundBehavior
{
public void MakeSound() => Console.WriteLine("Honk!");
}
// Step 3: Compose the behaviors
public class Bird
{
public string Name { get; set; }
private readonly IFlyBehavior _flyBehavior;
private readonly ISoundBehavior _soundBehavior;
public Bird(string name, IFlyBehavior flyBehavior, ISoundBehavior soundBehavior)
{
Name = name;
_flyBehavior = flyBehavior;
_soundBehavior = soundBehavior;
}
public void Fly() => _flyBehavior.Fly();
public void MakeSound() => _soundBehavior.MakeSound();
public void Eat() => Console.WriteLine("Eating seeds");
}
Using the Refactored Code
// Create birds with appropriate behaviors
var sparrow = new Bird(
"Sparrow",
new NormalFlight(),
new Chirping());
var penguin = new Bird(
"Penguin",
new NoFlight(),
new Honking());
// Use them
sparrow.Fly(); // "Flying high!"
penguin.Fly(); // "This bird can't fly!" (No exception!)
// Need a new type of bird? No problem!
var duck = new Bird(
"Duck",
new NormalFlight(),
new ISoundBehavior { MakeSound = () => Console.WriteLine("Quack!") });
What did we gain from this rewrite?
- We got rid of that brittle inheritance structure
- No more worrying about penguins accidentally flying
- We can easily swap out behaviors for testing
- We can change behaviors while the program is running
- Adding a new bird type is super simple
When to Choose What
Use Composition When… | Use Inheritance When… |
---|---|
You need runtime flexibility | You have a true “is-a” relationship |
Behaviors can be mixed and matched | The hierarchy is shallow and stable |
You’re writing unit tests | You’re modeling domain concepts that naturally inherit |
Requirements change frequently |
What I’ve Learned About Composition vs. Inheritance
I’ve been writing C# for years now, and I’ve tried both approaches on lots of projects. Here’s my take: composition usually leads to code that’s easier to maintain. Yes, you spend a bit more time up front creating interfaces and wiring things up, but that investment pays off fast when requirements start changing (and they always do).
I don’t hate inheritance. It has its place. I still use it when:
- I have a genuine “is-a” relationship that I’m pretty sure won’t change
- I need polymorphic behavior but don’t need to swap implementations
- I’m keeping my inheritance tree shallow (no more than 1-2 levels)
But honestly? These situations pop up way less often than I expected in real-world business apps. Most of the time, I find myself reaching for composition because it just gives me fewer headaches down the road.
The bottom line isn’t “always use composition” or “never use inheritance.” It’s about picking the right approach for what you’re building right now.
My advice? Next time you start typing : BaseClass
, pause and ask yourself: “Could I just inject this behavior instead?” Your future self (and your teammates) will probably thank you.
Related Posts
- How Does Composition Support the SOLID Principles? (C# Examples & Best Practices)
- How Polymorphism Makes C# Code Flexible: Real-World Examples and Best Practices
- When Inheritance Still Makes Sense in C#: Polymorphism Without Swapping
- Prefer Interfaces Over Abstract Classes in C#: Build Flexible, Testable, and Maintainable Code
- Polymorphism in C#: How Template Method, Strategy, and Visitor Patterns Make Your Code Flexible