The decision in one sentence
Use composition when behavior should vary independently; use inheritance when a subtype can honestly preserve the promises of its base type. “Prefer composition” is a design preference, not a ban on inheritance.
A notification example
An order service needs to notify customers. At first it sends email. Later, some customers want SMS, and the business wants retry rules and audit records.
A hierarchy such as EmailNotifier, RetryingEmailNotifier, AuditedRetryingEmailNotifier and AuditedSmsNotifier mixes several independent decisions. Every new combination encourages another subclass.
With composition, a notification service delegates delivery to a sender. Retry and audit behavior can be separate collaborators or wrappers. The service chooses its collaborators at construction time; it does not need a subclass for every combination.
The key distinction is responsibility: delivery knows how to send, retry knows whether another attempt is allowed, and audit records what happened. Do not put business decisions about customer consent inside a generic retry wrapper.
A five-question checklist
- What varies? If delivery channel and retry policy vary separately, represent them separately.
- What is promised? A subtype must preserve the base contract, including error behavior and side effects callers rely on.
- Who owns lifetime? Decide who creates and disposes collaborators; sharing a stateful object accidentally can introduce bugs.
- How will it be tested? A small delivery interface lets tests simulate success, temporary failure and permanent rejection without sending messages.
- Is the abstraction earned? Two identical lines do not justify a hierarchy. Start with a clear function when no meaningful variation exists.
Costs you should acknowledge
Composition adds indirection and wiring. A chain of wrappers can be harder to debug than a single class. Order matters: auditing each retry attempt differs from recording only the final outcome. Name the policy and test that ordering.
Inheritance can be useful for a stable framework extension point or a genuine shared contract. The danger is inheriting implementation merely to reuse a method, then overriding unrelated behavior to make the subclass fit.
Interview check
Question: A base document offers Save, but a read-only subclass throws every time Save is called. Is inheritance helping?
Answer: Callers expecting any base document to be saveable now receive a subtype that cannot keep that promise. Consider a readable document contract and a separate writable capability, rather than advertising an operation the object cannot perform.
Question: Does composition mean an interface for every class?
Answer: No. Introduce a boundary where variation, ownership or testing warrants it. Concrete collaborators and small functions are often enough.
Try it yourself
Sketch a report generator with a data source, formatter and destination. Add a second formatter without changing the data source. Then explain where errors are handled and whether the destination may be called twice. The quality of the boundaries matters more than the number of patterns used.
Takeaway
Name the changing responsibility first. Choose the smallest design that keeps that responsibility explicit, testable and honest about its contract.