Hey fellow developers! đź‘‹ Today, we're diving into one of the most important design principles in object-oriented programming: Composition Over Inheritance. If you've been writing C# code for a while, you've probably heard this advice countless times, but do you really understand why composition is preferred and when to use it?
Let me share a story from my early coding days. I was building an e-commerce application and enthusiastically created a beautiful inheritance hierarchy: Product → PhysicalProduct → ElectronicProduct → SmartphoneProduct. It looked elegant on paper! But then business requirements changed (as they always do), and I needed products that could be both physical AND digital (like a book with an ebook bundle). My entire inheritance tree came crashing down. That painful refactoring taught me the value of composition.
What is Composition Over Inheritance?
Composition Over Inheritance is a design principle that suggests you should build complex functionality by combining simple, focused objects rather than creating deep inheritance hierarchies. Instead of saying "A Car is a Vehicle," you say "A Car has an Engine, has a Transmission, and has Wheels."
Think of it like building with LEGO blocks. Inheritance is like creating specialised blocks that can only connect in specific ways. Composition is like having standard blocks you can snap together in any combination to create whatever you need.
Interview-Ready Definition
"Composition Over Inheritance is a design principle in object-oriented programming that recommends achieving code reuse and polymorphism through object composition (has-a relationship) rather than class inheritance (is-a relationship). This approach creates more flexible, maintainable code by assembling functionality from independent components instead of inheriting behaviour from parent classes."
Understanding the Problem with Inheritance
Before we appreciate composition, let's understand why excessive inheritance causes problems. Inheritance isn't inherently bad—it's a fundamental OOP concept—but it's often overused.
The Fragile Base Class Problem: When you change a base class, all derived classes are affected. This tight coupling makes your codebase brittle and hard to maintain.
Deep Inheritance Hierarchies: As your application grows, inheritance trees become deeper and more complex. Understanding the behaviour of a class requires tracing through multiple parent classes.
Limited Flexibility: C# (like Java) doesn't support multiple inheritance. If you need functionality from two different base classes, you're stuck.
Inappropriate Relationships: Sometimes we force inheritance where it doesn't make semantic sense, just to reuse code. This violates the Liskov Substitution Principle.
Let's see a real example of how inheritance can go wrong.
Real-World Example: Employee Management System
Imagine you're building an employee management system. Your first instinct might be to use inheritance to model different employee types.
❌ The Inheritance Approach (Bad Example)
// Base class
public class Employee
{
public string Name { get; set; }
public string Email { get; set; }
public decimal Salary { get; set; }
public virtual decimal CalculateBonus()
{
return Salary * 0.10m; // 10% bonus
}
public virtual void GenerateReport()
{
Console.WriteLine($"Employee Report for {Name}");
Console.WriteLine($"Email: {Email}");
Console.WriteLine($"Salary: {Salary:C}");
}
}
// Full-time employee with benefits
public class FullTimeEmployee : Employee
{
public decimal HealthInsurance { get; set; }
public decimal RetirementContribution { get; set; }
public override decimal CalculateBonus()
{
return Salary * 0.15m; // 15% bonus for full-time
}
public override void GenerateReport()
{
base.GenerateReport();
Console.WriteLine($"Health Insurance: {HealthInsurance:C}");
Console.WriteLine($"Retirement: {RetirementContribution:C}");
}
}
// Part-time employee
public class PartTimeEmployee : Employee
{
public int HoursPerWeek { get; set; }
public override decimal CalculateBonus()
{
return Salary * 0.05m; // 5% bonus for part-time
}
public override void GenerateReport()
{
base.GenerateReport();
Console.WriteLine($"Hours Per Week: {HoursPerWeek}");
}
}
// Contractor - now we have a problem!
public class Contractor : Employee
{
public DateTime ContractEndDate { get; set; }
public override decimal CalculateBonus()
{
// Contractors don't get bonuses, but we're forced to override
return 0;
}
public override void GenerateReport()
{
base.GenerateReport();
Console.WriteLine($"Contract Ends: {ContractEndDate:d}");
}
}
Problems with this approach
Inappropriate inheritance: Contractors inherit Salary property, but they might work on hourly rates instead
Forced implementation: Contractors must override CalculateBonus() even though bonuses don't apply to them
Inflexibility: What if you need an employee who is both full-time AND a contractor (consulting arrangement)? The hierarchy breaks down
Tight coupling: Changes to the Employee base class ripple through all derived classes
Violation of Liskov Substitution Principle: A Contractor can't truly substitute an Employee in all contexts
âś… The Composition Approach (Good Example)
Let's redesign this system using composition. We'll break down employee features into independent, composable components.
// Compensation strategies
public interface ICompensationCalculator
{
decimal CalculateBonus(decimal basePay);
}
public class SalariedCompensation : ICompensationCalculator
{
private readonly decimal _bonusPercentage;
public SalariedCompensation(decimal bonusPercentage)
{
_bonusPercentage = bonusPercentage;
}
public decimal CalculateBonus(decimal basePay)
{
return basePay * _bonusPercentage;
}
}
public class HourlyCompensation : ICompensationCalculator
{
public decimal CalculateBonus(decimal basePay)
{
// Hourly workers get fixed bonus
return 500m;
}
}
public class NoBonus : ICompensationCalculator
{
public decimal CalculateBonus(decimal basePay)
{
return 0;
}
}
// Benefits package
public interface IBenefitsPackage
{
void DisplayBenefits();
}
public class StandardBenefits : IBenefitsPackage
{
public decimal HealthInsurance { get; set; }
public decimal RetirementContribution { get; set; }
public void DisplayBenefits()
{
Console.WriteLine($"Health Insurance: {HealthInsurance:C}");
Console.WriteLine($"Retirement: {RetirementContribution:C}");
}
}
public class NoBenefits : IBenefitsPackage
{
public void DisplayBenefits()
{
Console.WriteLine("No benefits package");
}
}
// Work schedule
public interface IWorkSchedule
{
void DisplaySchedule();
}
public class FullTimeSchedule : IWorkSchedule
{
public void DisplaySchedule()
{
Console.WriteLine("Full-time: 40 hours/week");
}
}
public class PartTimeSchedule : IWorkSchedule
{
public int HoursPerWeek { get; set; }
public void DisplaySchedule()
{
Console.WriteLine($"Part-time: {HoursPerWeek} hours/week");
}
}
public class ContractSchedule : IWorkSchedule
{
public DateTime ContractEndDate { get; set; }
public void DisplaySchedule()
{
Console.WriteLine($"Contract until: {ContractEndDate:d}");
}
}
// Employee class using composition
public class Employee
{
public string Name { get; set; }
public string Email { get; set; }
public decimal BasePay { get; set; }
private readonly ICompensationCalculator _compensationCalculator;
private readonly IBenefitsPackage _benefitsPackage;
private readonly IWorkSchedule _workSchedule;
public Employee(
string name,
string email,
decimal basePay,
ICompensationCalculator compensationCalculator,
IBenefitsPackage benefitsPackage,
IWorkSchedule workSchedule)
{
Name = name;
Email = email;
BasePay = basePay;
_compensationCalculator = compensationCalculator;
_benefitsPackage = benefitsPackage;
_workSchedule = workSchedule;
}
public decimal CalculateBonus()
{
return _compensationCalculator.CalculateBonus(BasePay);
}
public void GenerateReport()
{
Console.WriteLine($"Employee Report for {Name}");
Console.WriteLine($"Email: {Email}");
Console.WriteLine($"Base Pay: {BasePay:C}");
Console.WriteLine($"Bonus: {CalculateBonus():C}");
Console.WriteLine("\nBenefits:");
_benefitsPackage.DisplayBenefits();
Console.WriteLine("\nSchedule:");
_workSchedule.DisplaySchedule();
}
}
// Usage examples
public class Program
{
public static void Main()
{
// Full-time employee
var fullTime = new Employee(
"Alice Johnson",
"[email protected]",
75000m,
new SalariedCompensation(0.15m),
new StandardBenefits { HealthInsurance = 500m, RetirementContribution = 300m },
new FullTimeSchedule()
);
fullTime.GenerateReport();
Console.WriteLine("\n" + new string('-', 50) + "\n");
// Part-time employee
var partTime = new Employee(
"Bob Smith",
"[email protected]",
35000m,
new SalariedCompensation(0.05m),
new NoBenefits(),
new PartTimeSchedule { HoursPerWeek = 20 }
);
partTime.GenerateReport();
Console.WriteLine("\n" + new string('-', 50) + "\n");
// Contractor
var contractor = new Employee(
"Carol Davis",
"[email protected]",
50000m,
new NoBonus(),
new NoBenefits(),
new ContractSchedule { ContractEndDate = DateTime.Now.AddMonths(6) }
);
contractor.GenerateReport();
Console.WriteLine("\n" + new string('-', 50) + "\n");
// Flexible: Full-time employee with hourly compensation (special case)
var hybrid = new Employee(
"David Lee",
"[email protected]",
60000m,
new HourlyCompensation(),
new StandardBenefits { HealthInsurance = 450m, RetirementContribution = 250m },
new FullTimeSchedule()
);
hybrid.GenerateReport();
}
}
Benefits of the composition approach
Flexibility: Mix and match components to create any employee type without modifying existing code
No forced implementations: Each component has a clear, focused responsibility
Easy to extend: Add new compensation types, benefits, or schedules without touching employee class
Loose coupling: Changes to compensation logic don't affect benefits or schedule logic
Better testing: Test each component independently with simple unit tests
Runtime flexibility: You can even change components at runtime if needed
Key Principles of Composition
1. Has-A Instead of Is-A
Composition models "has-a" relationships (Employee has a compensation calculator), while inheritance models "is-a" relationships (Manager is an Employee). "Has-a" relationships are more flexible and realistic for most scenarios.
2. Favour Small, Focused Interfaces
Each component should have a single, well-defined responsibility. This aligns with the Single Responsibility Principle from SOLID.
3. Depend on Abstractions
Use interfaces or abstract classes for component types. This follows the Dependency Inversion Principle and makes testing easier.
4. Compose at Construction Time
Inject dependencies through constructors or properties, making relationships explicit and dependencies clear.
When to Use Inheritance vs Composition
Composition isn't always the answer. Here's how to decide:
Use Inheritance When
True is-a relationship exists: A Dog truly is an Animal
Shared behaviour is fundamental: All derived classes need ALL base class functionality
Liskov Substitution holds: Derived classes can completely substitutethe base class
Extending framework classes: When integrating with frameworks that require inheritance
Creating template methods: When using the Template Method design pattern
Use Composition When
Behaviour can vary independently: Different combinations of features are needed
Runtime flexibility required: Need to change behaviour dynamically
Multiple inheritance needed: C# doesn't support it, so use composition
Avoiding deep hierarchies: The Inheritance tree is getting too complex
Code reuse across unrelated classes: Shared functionality doesn't imply a relationship
Common Composition Patterns in C#
Strategy Pattern
As seen in our compensation calculator example, the Strategy pattern uses composition to encapsulate algorithms and make them interchangeable.
Decorator Pattern
Add responsibilities to objects dynamically by wrapping them with decorator objects.
public interface INotification
{
void Send(string message);
}
public class BasicNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Sending: {message}");
}
}
public class EmailNotificationDecorator : INotification
{
private readonly INotification _notification;
public EmailNotificationDecorator(INotification notification)
{
_notification = notification;
}
public void Send(string message)
{
_notification.Send(message);
Console.WriteLine("Also sending via email...");
}
}
public class SMSNotificationDecorator : INotification
{
private readonly INotification _notification;
public SMSNotificationDecorator(INotification notification)
{
_notification = notification;
}
public void Send(string message)
{
_notification.Send(message);
Console.WriteLine("Also sending via SMS...");
}
}
// Usage: Compose multiple decorators
INotification notification = new BasicNotification();
notification = new EmailNotificationDecorator(notification);
notification = new SMSNotificationDecorator(notification);
notification.Send("Important alert!");
Composite Pattern
Treat individual objects and compositions uniformly, creating tree structures of objects.
Practical Tips for Applying Composition
Start with interfaces: Define what components should do before implementing them
Keep components small: Each component should have one clear responsibility
Use dependency injection: Frameworks like .NET's built-in DI container make composition easier
Don't over-engineer: Start simple and refactor to composition when complexity grows
Document relationships: Make it clear what components work together and why
Real-World Benefits
In my projects, switching from inheritance to composition has resulted in:
50% reduction in code changes when requirements evolve
Easier onboarding for new developers—smaller, focused components are simpler to understand
Better test coverage—testing individual components is straightforward
Fewer bugs—changes are localised to specific components
Faster feature development—new features often just require new components, not refactoring
Common Pitfalls to Avoid
Too many small classes: Don't create a component for everything—balance is key
Over-abstraction: Don't create interfaces for components that will only have one implementation
Circular dependencies: Be careful that components don't depend on each other
Ignoring performance: Composition adds indirection; for performance-critical code, measure first
Conclusion
Composition Over Inheritance isn't about abandoning inheritance entirely—it's about choosing the right tool for the job. Inheritance has its place for modelling true is-a relationships and extending framework classes. But for most application logic, composition provides the flexibility, maintainability, and testability we need.
Think of composition as building with LEGO blocks: small, reusable pieces that can be combined in countless ways. Inheritance is more like Russian nesting dolls: elegant for specific scenarios but rigid and constraining when your needs change.
The next time you're tempted to create a deep inheritance hierarchy, ask yourself: "Can I achieve this with composition instead?" More often than not, the answer will make your code better.
Key Takeaways
Prefer "has-a" relationships over "is-a" relationships for flexibility
Use small, focused components with single responsibilities
Depend on abstractions (interfaces) for loose coupling
Apply composition when behaviour varies independently or needs runtime flexibility
Reserve inheritance for true is-a relationships and framework integration
Complete source code examples: GitHub Repository Link
Happy coding, and may your classes be loosely coupled and highly cohesive! 🚀