Free lesson · Low Level Design Interviews
Composition over Inheritance in Practice
Composition Over Inheritance in Practice
by Nikhil Joshi - Senior Software Engineer, Google Cloud Spanner.
Product 1 covers "prefer composition over inheritance" as a design principle. The principle is correct. The issue I want to address in this topic is that the principle is also widely known - every candidate knows the phrase. The difference between candidates is not whether they know it. It's whether they can apply it deliberately and visibly when a real design fork arrives.
When I was at Razorpay building payment processing, this principle came up constantly. A new payment provider needed to be added - UPI, NetBanking, credit card, wallet, BNPL. The first instinct was to inherit: AbstractPaymentProvider, then UPIProvider, CardProvider, and so on. We did this initially. Within six months, we'd refactored to composition - each provider was a separate concrete class implementing a common interface, with shared behavior pulled into helper classes. The inheritance hierarchy had been wrong; composition was right; we paid for the lesson.
This topic is about applying that lesson in an LLD interview. The Bar Raiser is watching specifically for whether you reach for inheritance reflexively (and what that signals) versus whether you make the composition-vs-inheritance choice visibly, with reasoning. The choice itself often matters less than how you make it. Reflexive inheritance is the anti-signal; deliberate composition is the senior signal.
The interview-specific framing
In production, the composition-vs-inheritance choice plays out over months. You write some code, see it evolve, refactor when the structure doesn't fit. The mistakes are recoverable because you have time.
In an interview, the choice plays out in seconds. You have to commit to one or the other, often without enough information to be certain. The interviewer is watching your choice and your reasoning. If you reach for inheritance reflexively - "I'll have an AbstractPiece base class with subclasses" - without articulating why, you've signaled less taste than if you made the same choice with reasoning ("I'll use an abstract base class here because all pieces share concrete state - color and position - and only the legal-moves logic varies").
The deeper signal: candidates who default to inheritance are usually doing so because that's what textbooks taught them. Candidates who default to composition are usually doing so because they've worked on production code that suffered from over-inheritance. The default reveals where the candidate's mental model comes from.
Important: The choice isn't right or wrong universally. The reasoning is what's scored.
Inheritance is sometimes correct. Composition is sometimes correct. What separates senior candidates is that they articulate which forces favor which choice for the specific situation. The choice without reasoning is what flags as inexperienced.
Where inheritance genuinely fits
I want to be clear: composition isn't always right. Some situations genuinely fit inheritance. Let me name them.
Is-a relationships with truly shared state and behavior. Chess pieces all have a color and a position; the King is genuinely a Piece. The shared fields exist. Inheritance models this naturally.
public sealed abstract class Piece permits King, Queen, Rook, Bishop, Knight, Pawn {
protected final Color color;
protected Position position;
protected Piece(Color color, Position position) {
this.color = color;
this.position = position;
}
public Color color() { return color; }
public Position position() { return position; }
public abstract Set<Position> legalMoves(Board board);
}
Here, inheritance carries weight: shared state (color, position), shared accessor methods, and an abstract method for the variation. The hierarchy fits.
Frameworks designed for inheritance. Spring's JpaRepository, JUnit's test classes, Android's Activity. These frameworks expect inheritance and provide significant value through the hierarchy. Using inheritance with them is correct because the framework was designed for it.
When the parent contract is stable. If you control both the parent and the children, and the parent is unlikely to change, inheritance can be cleaner than composition's boilerplate.
In each of these, inheritance is the right call. The pattern "favor composition" doesn't mean "never inherit." It means "default to composition unless inheritance demonstrates clear benefit."
Where composition fits
The cases where composition is clearly right:
When the relationship is "has-a" not "is-a." A Car has-an Engine. A Car is not an Engine. Modeling Car as a subclass of Engine would be silly. Composition fits naturally.
When you want to swap implementations. A PaymentProcessor needs to work with Stripe, Adyen, or a test stub. Composition with an interface makes the swap easy. Inheritance would force a complex hierarchy.
public interface PaymentGateway {
PaymentResult charge(Money amount, PaymentMethod method);
}
public class StripePaymentGateway implements PaymentGateway { ... }
public class AdyenPaymentGateway implements PaymentGateway { ... }
public class TestPaymentGateway implements PaymentGateway { ... }
public class OrderService {
private final PaymentGateway gateway; // composition
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
The OrderService composes a PaymentGateway. Any implementation works. The relationship is "OrderService uses a PaymentGateway" - composition reflects this naturally.
When you'd otherwise need multiple inheritance. Java doesn't support multiple inheritance of classes. If an entity needs to share behavior with two different "parents," composition is the only option. Multiple interfaces with default methods can sometimes simulate multiple inheritance, but for shared state, composition is cleaner.
When the parent might evolve in breaking ways. Inheritance creates structural coupling - changes to the parent affect all subclasses. If the parent is stable, this is fine. If the parent might change in ways that affect subclasses, composition's looser coupling is safer.
When the relationship is conceptually one of delegation. "I do some of my work by asking another object to do something." That's composition with delegation.
In practice, most relationships in production systems are composition. The default-to-composition heuristic is statistically right because most situations don't have the specific properties that make inheritance better.
The visible decision
Here's where the interview-specific point comes in. When you face the composition-vs-inheritance choice, make the choice visibly. Don't just write the structure that came to mind. Articulate why.
Weak (silent default):
"I'll have an AbstractPaymentProvider class, with UPIProvider, CardProvider, and WalletProvider as subclasses."
Strong (visible reasoning):
"I'm looking at PaymentProvider. The variations - UPI, Card, Wallet - could be subclasses, but each one is going to have a fairly different implementation. The 'shared behavior' is mostly the contract, not actual code. I'll use an interface - PaymentProvider - with separate concrete classes for each provider. If we find shared helper logic later, we can pull it into a utility or a small base class, but I don't want to commit to inheritance until I see the duplication."
Same structural choice (no inheritance). Different signal. The strong version names the alternatives, articulates the reasoning, and acknowledges flexibility for future evolution. The weak version commits without explanation.
The interviewer is watching for moments like this. The composition-vs-inheritance decision is a recurring fork - most LLD problems have at least one. Each fork is an opportunity to demonstrate engineering taste. Strong candidates use the opportunity. Weak candidates pass through without remark.
A common mistake: inheritance for code reuse
The most common reason candidates reach for inheritance is to reuse code. They have two classes that share some methods, so they extract a common parent with the shared methods.
This is usually wrong.
When inheritance is used for code reuse rather than for genuine is-a relationships, several problems follow:
-
Inappropriate hierarchies. The parent ends up holding methods that don't fit a clean abstract type. It's a "bag of shared methods" rather than a meaningful concept.
-
Fragile base class. Changes to the parent break subclasses in subtle ways. Adding a new method to the parent might conflict with an existing method in a subclass.
-
Hard to refactor. Once a hierarchy is in place, removing it requires touching all the subclasses. The hierarchy becomes path-dependent.
-
Tests get harder. Mocking a parent class is harder than mocking an interface. Tests of subclasses inherit complexity from the parent.
The cleaner alternative for code reuse: pull the shared logic into a helper class (a utility, a separate object, a function library). Each "subclass" becomes a separate class that uses the helper via composition.
// Inheritance for code reuse (anti-pattern)
public abstract class PaymentProcessor {
protected boolean validateAmount(Money amount) { ... } // shared
protected void recordTransaction(Transaction t) { ... } // shared
public abstract PaymentResult process(...);
}
// Composition for code reuse (cleaner)
public class PaymentValidator {
public boolean validateAmount(Money amount) { ... }
}
public class TransactionLogger {
public void recordTransaction(Transaction t) { ... }
}
public class StripePaymentProcessor implements PaymentProcessor {
private final PaymentValidator validator;
private final TransactionLogger logger;
public StripePaymentProcessor(PaymentValidator validator, TransactionLogger logger) {
this.validator = validator;
this.logger = logger;
}
}
The composition version is more boilerplate up front, but it's more flexible - different processors can use different validators or loggers. The shared code is reusable across non-Payment contexts too. The is-a relationship isn't forced where it doesn't fit.
When the model needs interfaces
Closely related to composition: interfaces. When should an entity be an interface with multiple implementations versus a concrete class?
The rule I use: interfaces appear when multiple implementations exist or are clearly anticipated. Single-implementation interfaces are speculative abstraction - they add indirection without enabling anything.
For PaymentGateway: multiple implementations are real (Stripe, Adyen, test stub). Interface is justified.
For OrderRepository: multiple implementations are common (JPA, in-memory for tests, possibly Cassandra later). Interface is justified.
For Board in chess: probably only one implementation. Don't make it an interface.
For Game in chess: probably only one implementation. Don't make it an interface.
The "interface for testability" argument is weak in 2026. Modern mocking tools (Mockito) can mock concrete classes. The need for interfaces purely to enable testing has decreased. Use interfaces when there are real alternative implementations, not as a speculative concession to test infrastructure.
Important: Default to concrete classes. Promote to interface when multiple implementations are real or clearly anticipated.
Single-implementation interfaces are an anti-pattern in interview design. They signal that the candidate is reaching for abstraction without need.
Type erasure and generic considerations
A subtle point that comes up in modern Java interviews: when composition involves generic types, type erasure can constrain your choices.
For a Repository pattern:
public interface Repository<T, ID> {
Optional<T> findById(ID id);
void save(T entity);
void deleteById(ID id);
}
public interface OrderRepository extends Repository<Order, OrderId> {
// additional Order-specific methods
}
The generic Repository handles common patterns; OrderRepository specializes. This is composition (the interface) plus specialization (the typed extension). It works.
The trap to watch: trying to add behavior to the generic Repository that depends on the concrete type. Type erasure means you don't have the concrete type at runtime within the generic interface. You'd need to either pass the type explicitly or move the behavior down to the typed subclass.
In interviews, you usually don't need to dive deep into generic subtleties. Awareness is enough. If the interviewer asks about generics, you should know enough to navigate the question. Reaching for fancy generic tricks without need is the anti-signal.
What I learned at Razorpay
The payment-provider hierarchy at Razorpay taught me one specific thing: the abstractions you imagine at the start of a project are almost always wrong. The clean hierarchies, the perfect taxonomies, the elegant inheritance trees - they look right in design documents and fail in production code.
What works instead is loose, replaceable, composed structures. Each piece does one thing. Pieces are wired together by composition. When a new requirement appears, you add a new piece or replace an existing one. The structure doesn't fight you because it never committed to a clean hierarchy.
In interviews, I bring this perspective directly. When I face a design fork, my default is loose-and-composed, not tight-and-inherited. The interviewer is usually senior; they recognize the pattern from their own production experience. Reaching for composition deliberately signals shared experience.
Important: Default to composition. Use inheritance only when it demonstrably fits. Make the choice visibly in interviews - articulate the alternatives, name the reasoning, acknowledge flexibility for future evolution.
The deliberate decision is the signal, more than the structural choice itself.
What I tell candidates
When candidates ask me about composition vs inheritance, the advice is:
Important: Build the muscle of visible decision-making at composition-vs-inheritance forks.
Practice articulating: "I'm choosing X here because [reason]. The alternative would have been Y, which would [trade-off]." This articulation, repeated at every relevant fork, is what produces the senior signal. It's the technique I drilled before my third Google attempt.
The principle is widely known. The articulation discipline is the differentiator. Many candidates know "favor composition." Few articulate the choice visibly when it arrives.
Common mistakes
Warning: Composition-vs-inheritance failures:
Reflexive inheritance. AbstractFoo for everything. Default to hierarchies because that's what textbooks taught.
Inheritance for code reuse. Pulling shared methods into a parent that has no conceptual coherence.
Silent default to composition. Using composition because you "should" without articulating why.
Single-implementation interfaces. Speculative abstraction; indirection without benefit.
Refusing inheritance when it fits. Going to extremes with composition for situations where inheritance is genuinely cleaner.
Generic trickery. Reaching for advanced generic features when simple types would do.
Try It
Exercise: Build visible-decision discipline:
Take five LLD problems. For each design fork involving composition or inheritance, write out the alternatives and your reasoning. Time-box to 30 seconds per fork.
Practice the articulation. In your next mock interview, every composition-vs-inheritance choice gets verbal reasoning: "I'm choosing X because the alternative Y would..."
Audit your reflexes. When you reach for inheritance, ask: is this really is-a, or am I reaching for code reuse? When you reach for composition, ask: am I adding indirection for no benefit?
Find an inheritance hierarchy in code you maintain. Could it have been composition? What would change?
Find a composition pattern in code you maintain. Could it have been inheritance? What's the trade-off?
The next topic (4.4) is from Brandon - when to reach for a design pattern. Brandon's Bar Raiser perspective on patterns: when applying them genuinely helps, when pattern-dropping signals weakness, and how to handle the temptation to name-drop patterns for impression.