Design Principle
Composition Over Inheritance
Learn when to prefer composition over inheritance. Model behavior with combinable units for explicit, testable changes. Avoid fragile base classes and coupling.
Composition Over Inheritance
Why This Matters
Think of inheritance like a family tree—you inherit traits from your parents, and your children inherit from you. But if you want to change something, you have to change it for the whole family. Composition is like building with LEGO blocks—you combine different pieces to create what you need, and you can easily swap pieces without affecting the whole structure.
This matters because inheritance creates tight coupling. If you change a base class, all subclasses are affected. This makes code hard to change and test. Composition creates loose coupling—you combine objects, and you can swap implementations without changing the whole hierarchy. This makes code more flexible and maintainable.
In interviews, when someone asks "How would you refactor this code?", they're testing whether you understand composition vs inheritance. Do you know when to use each? Can you identify when inheritance is causing problems? Most engineers can't. They use inheritance by default and wonder why the code is hard to change.
What Engineers Usually Get Wrong
Engineers often think "inheritance is the OOP way, so I should use it." But inheritance isn't always the best choice. Inheritance works well when you have a true "is-a" relationship and the subclass truly extends the base class's behavior. But if you're using inheritance just to reuse code, composition is usually better.
Engineers also don't understand the fragile base class problem. When you change a base class, all subclasses are affected. This can break code in unexpected ways. Composition avoids this—if you change one component, only code that uses that component is affected.
How This Breaks Systems in the Real World
A service had a PaymentProcessor base class with subclasses for different payment methods (CreditCard, PayPal, BankTransfer). The base class had a process() method. A developer added a new payment method (Crypto) and overrode process(). But the base class also had a validate() method that called process(). The new subclass's process() method had different behavior, which broke the validation logic. The fix? Use composition. Create a PaymentMethod interface, and compose payment processors with different validation strategies. This avoids the fragile base class problem.
Another story: A team was building a notification system. They used inheritance: EmailNotification extends Notification, SMSNotification extends Notification, etc. When they needed to support multiple channels (email + SMS), they had to create new classes like EmailAndSMSNotification. This created a class explosion. The fix? Use composition. Create NotificationChannel interfaces (EmailChannel, SMSChannel), and compose them. Now you can combine channels without creating new classes.
1. Keep behaviours granular
Break features into capability modules. Compose them where you need them.
1type Policy = {2 before?(input: any): Promise<void> | void;3 after?(result: any): Promise<void> | void;4};56const withAudit = (policy: Policy): Policy => ({7 before: async (input) => {8 console.log("[audit] input", input);9 await policy.before?.input
Result: Capabilities stay opt-in. No fragile base class required.
2. Treat UI as composition-friendly
React hooks and render props exemplify composition. They expose building blocks without enforcing ancestry.
1// Hooks compose behaviour without hierarchy.2function usePolling<T>(fetcher: () => Promise<T>, interval = 5000) {3 const [data, setData] = useState<T | null>(null);4 useEffect(() => {5 let cancelled = false;6 async function poll() {7 const result = await
Result: Any component can reuse usePolling without contorting inheritance chains.
3. Model cross-cutting concerns with decorators
Decorators (or higher-order functions) express orthogonal capabilities neatly.
1type Command = (...args: any[]) => Promise<void>;23const withLock =4 (lockId: string, command: Command): Command =>5 async (...args) => {6 const lock = await acquireLock(lockId);7 try {8 await command(...args);9 } finally {10 await lock.release();11 }
Result: You can assemble features like Lego bricks, detaching capability decisions from inheritance diagrams.
Checklist
- Does extending a base class force you to inherit methods you don’t need? Switch to composition.
- Do you need multiple versions of a behaviour? Compose them around the shared core.
- Are tests struggling with deep hierarchies? Flatten by extracting mixins or helpers.
Reflection: Diagram a core service and its behaviours. Which responsibilities could be optional modules? Refactor one to use composition and measure how tests simplify.
Takeaway: Composition keeps change local. Your system evolves by snapping pieces together, not by balancing a brittle family tree.
Interview Questions
Beginner
Q: What is the difference between composition and inheritance? When should you prefer composition?
A:
Inheritance: Shares behavior by sharing ancestry (class extends another class). Child classes inherit all methods from parent.
Composition: Shares behavior by wiring objects together (object contains other objects). You combine capabilities explicitly.
Prefer composition when:
- You need multiple behaviors (can't inherit from multiple classes in most languages)
- Behaviors are optional (don't want to inherit everything)
- You want to change behavior at runtime
- Inheritance would create a deep, fragile hierarchy
Example: Instead of AdminUser extends User extends BaseEntity, compose: User contains Permissions, AuditLogger, EmailNotifier as separate objects.
Intermediate
Q: Refactor this inheritance hierarchy to use composition: Animal → Dog extends Animal → ServiceDog extends Dog. The classes have methods like eat(), bark(), fetch(), performTask().
A:
Problem with inheritance: ServiceDog inherits everything from Dog, even if it doesn't need it. Adding new animal types creates deep hierarchies.
Composition approach:
1// Define behaviors as composable units2type EatingBehavior = { eat(): void };3type BarkingBehavior = { bark(): void };4type FetchingBehavior = { fetch(): void };5type TaskBehavior = { performTask(): void };67// Implement behaviors8const standardEat: EatingBehavior = { eat() { /* ... */ } };
Benefits: Can swap behaviors (quiet dog, fetch-disabled dog), test behaviors independently, add new behaviors without modifying base classes.
Senior
Q: Design a plugin system for a code editor (like VS Code extensions). How would you use composition to allow plugins to add features (syntax highlighting, autocomplete, linting, formatting) without modifying core editor code?
A:
Core editor (closed for modification):
1interface Editor {2 registerPlugin(plugin: Plugin): void;3 getDocument(): Document;4 insertText(text: string, position: Position): void;5}67// Plugin interface - composition point8interface Plugin {9 name: string;10 capabilities: PluginCapability[];11 activate(editor: Editor): void;12 deactivate(): void;13}1415// Capabilities as composable units
Plugin implementation (composition):
1class TypeScriptPlugin implements Plugin {2 name = 'typescript';3 capabilities: PluginCapability[] = [4 {5 type: 'syntax-highlight',6 handler: this.highlightTypeScript.bind(this)7 },8 {9 type: 'autocomplete',10 handler: this.completeTypeScript.bind(this)11 },12 {13 type: 'lint',14 handler: this.lintTypeScript.bind
Editor extension points (Open/Closed Principle + Composition):
1class CodeEditor implements Editor {2 private highlighters: SyntaxHighlighter[] = [];3 private completers: Autocompleter[] = [];4 private linters: Linter[] = [];56 registerCapability(cap: PluginCapability) {7 switch(cap.type) {8 case 'syntax-highlight':9 this.highlighters.push(cap.handler as SyntaxHighlighter);10 break;11 case
Benefits:
- Plugins compose capabilities (can have highlighting + linting + formatting)
- Editor doesn't know about specific languages
- New plugins don't modify core editor
- Can enable/disable capabilities per plugin
- Test each capability independently
Key insight: Composition allows the editor to be extended (new plugins) without modification (core editor unchanged). Each plugin is a composition of capabilities, not a monolithic extension.
Failure Stories You'll Recognize
The Fragile Base Class: A service had a PaymentProcessor base class with subclasses for different payment methods (CreditCard, PayPal, BankTransfer). The base class had a process() method. A developer added a new payment method (Crypto) and overrode process(). But the base class also had a validate() method that called process(). The new subclass's process() method had different behavior, which broke the validation logic. The fix? Use composition. Create a PaymentMethod interface, and compose payment processors with different validation strategies. This avoids the fragile base class problem.
The Class Explosion: A team was building a notification system. They used inheritance: EmailNotification extends Notification, SMSNotification extends Notification, etc. When they needed to support multiple channels (email + SMS), they had to create new classes like EmailAndSMSNotification. This created a class explosion. The fix? Use composition. Create NotificationChannel interfaces (EmailChannel, SMSChannel), and compose them. Now you can combine channels without creating new classes.
The Deep Hierarchy: A team was using inheritance for everything. They had classes like BaseService extends BaseComponent extends BaseEntity. When they needed to change BaseEntity, it affected all classes in the hierarchy. Testing became difficult—you had to understand the entire hierarchy to test one class. The fix? Use composition. Break behaviors into small, composable pieces. Compose them as needed. This makes code easier to understand, test, and change.
What Interviewers Are Really Testing
They want to hear you talk about composition vs inheritance as a design choice, not a rule. Junior engineers say "use composition over inheritance." Senior engineers say "inheritance works for true 'is-a' relationships. Composition works for 'has-a' relationships and when you need flexibility. Choose based on your use case. Avoid deep hierarchies and fragile base classes."
When they ask "How would you refactor this code?", they're testing:
-
Can you identify when inheritance is causing problems?
-
Do you know when to use composition vs inheritance?
-
Can you refactor code to use composition?
-
Keep behaviors granular - break features into capability modules, compose them where needed
-
Composition shares behavior by wiring objects together - no fragile base classes required
-
Treat UI as composition-friendly - React hooks and render props exemplify composition without hierarchy
-
Model cross-cutting concerns with decorators - higher-order functions express orthogonal capabilities neatly
-
Capabilities stay opt-in - no need to inherit everything, just compose what you need
-
Checklist: Does extending force you to inherit unused methods? Do you need multiple behavior versions? Are tests struggling with deep hierarchies?
-
Composition keeps change local - system evolves by snapping pieces together, not balancing a brittle family tree
-
Prefer composition when behaviors are optional or need to change at runtime
How InterviewCrafted Will Teach This
We'll teach this through real code problems, not abstract principles. Instead of memorizing "prefer composition over inheritance," you'll learn through scenarios like "why is this code hard to change when we modify the base class?"
You'll see how composition helps you write flexible, maintainable code. When an interviewer asks "how would you refactor this code?", you'll think about coupling, flexibility, and testability—not just "use composition."
- SOLID Principles - Liskov Substitution Principle (LSP) relates to composition over inheritance, ensuring subtypes can be substituted without breaking contracts.
- DRY, KISS & YAGNI - Composition aligns with KISS by keeping behaviors simple and explicit, and with YAGNI by avoiding premature inheritance hierarchies.
- Separation of Concerns - Composition helps achieve separation of concerns by keeping behaviors in separate, composable units.
Key Takeaways
Keep behaviors granular - break features into capability modules, compose them where needed
Composition shares behavior by wiring objects together - no fragile base classes required
Treat UI as composition-friendly - React hooks and render props exemplify composition without hierarchy
Model cross-cutting concerns with decorators - higher-order functions express orthogonal capabilities neatly
Capabilities stay opt-in - no need to inherit everything, just compose what you need
Checklist: Does extending force you to inherit unused methods? Do you need multiple behavior versions? Are tests struggling with deep hierarchies?
Composition keeps change local - system evolves by snapping pieces together, not balancing a brittle family tree
Prefer composition when behaviors are optional or need to change at runtime
Related Topics
SOLID Principles
Liskov Substitution Principle (LSP) relates to composition over inheritance, ensuring subtypes can be substituted without breaking contracts.
DRY, KISS & YAGNI
Composition aligns with KISS by keeping behaviors simple and explicit, and with YAGNI by avoiding premature inheritance hierarchies.
Separation of Concerns
Composition helps achieve separation of concerns by keeping behaviors in separate, composable units.
Keep exploring
Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.