Composing objects: building complex behavior from simple parts
Apply the composing objects principle to assemble behavior from small, focused collaborators rather than inheriting from deep class hierarchies.
You need a Robot that can walk, shoot lasers, and detect obstacles. The tempting move: build an inheritance chain. Robot extends MovingEntity extends ShootingEntity. Then the requirements change. A new robot flies instead of walking, and another one detects heat instead of obstacles. Now you need FlyingShootingHeatRobot and the hierarchy is a tangled mess that nobody wants to touch.
The composing objects principle says: stop building complex things by extending other things. Instead, assemble complex behavior from small, focused collaborators. Each collaborator does one job. The composed object wires them together. Swap a collaborator, and the behavior changes without rewriting any class.
I use this principle as the default starting point in every LLD interview. If someone reaches for inheritance first, I ask: "Could you compose this instead?" The answer is almost always yes.
What Is the Composing Objects Principle
The composing objects principle states: build complex behavior by combining simple, focused objects rather than layering behavior through inheritance. Each small object handles exactly one capability. The composed object holds references to these collaborators and delegates to them.
Think of it like LEGO. Each brick is simple and single-purpose (a 2x4 block, a wheel, a hinge). You build a car not by making a "CarBrick" that inherits from "WheelBrick," but by snapping independent bricks together. Want to change the wheels? Pop them off and snap on different ones. The chassis does not care.
For your interview: say "I prefer to compose behavior from small collaborators rather than inheriting it" and then demonstrate it in your class design. That single sentence signals maturity.
On the left, adding a new capability means inserting a new layer into the hierarchy. On the right, adding a capability means plugging in one more collaborator. The composed approach scales linearly; the inheritance approach scales combinatorially.
Composition Over Inheritance
This is not a theoretical debate. It is a practical observation: inheritance hierarchies break the moment requirements combine in unexpected ways.
With inheritance, you get one axis of variation. Robot extends MovingEntity locks in movement behavior for every robot. If you need a flying robot and a walking robot, you need two separate class trees or you resort to ugly workarounds (empty method overrides, boolean flags, multiple inheritance hacks).
With composition, you get independent axes of variation. Movement is one axis. Weapon is another. Sensor is a third. Mix and match freely. A robot with WalkingMovement, LaserWeapon, and RadarSensor is just three constructor arguments. Swap WalkingMovement for FlyingMovement and nothing else changes.
Inheritance needs a class per combination: 2 movements Γ 2 weapons Γ 2 sensors = 8 classes, each with its own name and hierarchy position. Composition needs 6 small implementations and one Robot class that accepts any combination. Add a third weapon type and inheritance needs 12 classes; composition needs 7.
The math alone makes the case. But the real payoff is testing. Each collaborator is testable in isolation. WalkingMovement does not need a weapon to be tested. LaserWeapon does not need a sensor. And Robot can be tested with mocks for all three.
Implementation
// Capability interface: how the robot moves.
// Each implementation is a small, focused class.
// Robot never knows whether it walks, flies, or rolls.
public interface Movement {
void move(double x, double y);
String type();
}Notice what the Robot class does not contain: no if (type == FLYING) branches, no @Override of parent methods, no casting. It holds three interfaces and calls them. Adding a MissileWeapon means writing one new class that implements Weapon. Zero existing files change. That is the open/closed principle in action, powered by composition.
Strategy + Composition: Pluggable Behavior at Runtime
Composing objects becomes even more powerful when you allow swapping collaborators after construction. This is where the Strategy pattern meets composition: the composed object holds a strategy that can be replaced at runtime.
To enable runtime swapping, make the collaborator field non-final and expose a setter:
public class Robot {
private final String name;
private Movement movement; // no longer final
private final Weapon weapon;
private final Sensor sensor;
// ... constructor as before ...
// Swap movement strategy at runtime.
// The robot starts walking and switches to flying mid-mission.
public void setMovement(Movement movement) {
this.movement = movement;
}
}
This is exactly what the Strategy pattern does: encapsulate a family of algorithms behind an interface and make them interchangeable. The difference in framing is that Strategy focuses on one swappable behavior, while the composing objects principle applies the same idea across all of a class's capabilities simultaneously.
My recommendation: start with final fields (construction-time composition). Only add setters when the requirements genuinely need runtime swapping. Immutability by default, mutability by necessity.
Interview tip: name the pattern
When you compose an object with swappable collaborators, say: "I am using the Strategy pattern here so the movement behavior is pluggable." Naming the pattern earns points. It signals you know the GoF vocabulary and can apply it to real designs. Interviewers notice.
When Inheritance Is Still Fine
Composition is the default, not the only tool. Inheritance is appropriate when all three conditions hold:
- True IS-A relationship. A
SavingsAccountgenuinely IS-ABankAccount. It shares identity, not just behavior. - Liskov substitutability. Every place that accepts a
BankAccountworks correctly with aSavingsAccount. No surprises, no exceptions thrown from overridden methods. - Shared state and behavior. The subclass needs the parent's fields and the parent's method implementations, not just its interface.
| Criterion | Inheritance fits | Composition fits |
|---|---|---|
| Relationship | True IS-A (Dog IS-A Animal) | HAS-A or USES-A (Robot HAS-A Weapon) |
| Substitutability | Subclass passes Liskov check | Not required |
| Variation axes | Single axis (one thing varies) | Multiple independent axes |
| Runtime flexibility | Fixed at compile time | Swappable at runtime |
| Framework requirement | extends AbstractList, extends HttpServlet | No framework constraint |
Concrete examples where inheritance is correct:
ArrayList extends AbstractList(genuine IS-A, shared state, framework contract)HttpServletsubclasses (servlet container expects this hierarchy)- Domain models with strict taxonomy (
CheckingAccount extends BankAccountwhen the bank truly models it this way)
The honest rule: if you are inheriting to reuse three lines of code, use composition. If you are inheriting because the domain relationship is genuinely hierarchical and substitutable, inheritance is fine. When in doubt, compose.
Inheritance is a one-way door
Once you publish an inheritance hierarchy, clients depend on it. Changing a superclass method signature breaks every subclass in every downstream team. Composition, by contrast, lets you change a collaborator's internals without affecting the composed class's API. Prefer the reversible choice.
Common Mistakes
1. Premature abstraction
You write Movement, Weapon, and Sensor interfaces before you have a single concrete implementation. Then you discover the abstraction is wrong, and you refactor the interface, the one implementation, and every test. Wait until you have two concrete implementations before extracting an interface. The second implementation reveals the right abstraction naturally.
2. Over-composition (too many collaborators)
A Robot with 12 injected collaborators is not well-composed. It is a class with 12 responsibilities being masked by delegation. If the constructor has more than 5-6 parameters, the class is likely doing too much. Split the class first, then compose the pieces.
3. Composing what should be a method
Not every behavior needs its own class. If the "collaborator" is a single method with no state and no alternative implementations, it belongs as a private method on the composed class, not as a separate object with an interface. Composition is for genuine behavioral variation, not for turning every method into a class.
4. Forgetting to define clear interfaces
Composing concrete classes instead of interfaces gives you composition without flexibility. If Robot holds a WalkingMovement field instead of a Movement field, you cannot swap implementations. Always compose through interfaces or abstract types.
| Mistake | Symptom | Fix |
|---|---|---|
| Premature abstraction | Interface with one implementation, changes frequently | Wait for 2+ implementations, then extract |
| Over-composition | Constructor with 8+ parameters | Split the class into smaller composed units |
| Trivial composition | Single-method stateless "strategy" class | Use a private method instead |
| Concrete composition | Robot holds WalkingMovement not Movement | Depend on the abstraction, not the implementation |
Test Your Understanding
Quick Recap
- The composing objects principle says: build complex behavior by assembling small, focused collaborators rather than stacking behavior through inheritance hierarchies.
- Each collaborator implements a single-capability interface (
Movement,Weapon,Sensor), and the composed class delegates to them without knowing the concrete types. - Composition gives you M + N + 1 classes for M Γ N combinations of behavior; inheritance gives you M Γ N classes. The gap widens with every new axis of variation.
- Combine composition with the Strategy pattern to swap collaborators at runtime, but default to final fields for thread safety and simplicity.
- Inheritance is still correct for genuine IS-A relationships with shared state, Liskov substitutability, and framework contracts.
- The most common composition mistake is over-composition: injecting 8+ collaborators into one constructor, which masks a class that has too many responsibilities.
- In interviews, say "I compose behavior from small collaborators" and then draw the interfaces. That single sentence plus a clean diagram signals design maturity.