One Interview Question, Five Versions, Five Principles

The whole Bird design in one timeline, from one overloaded class to a version that respects all five SOLID principles at once, one problem and one fix per stop.

September 14, 20262 min read18 / 20

Every version of Bird across this whole series was solving one real problem at a time. Laid out in a straight line, from the very first attempt to the last, the whole arc actually explains itself.

v0SRP + OCP
One Class, Two Violations
A single `Bird` class held an if-else chain for every kind of flying. One class carried every bird's logic, and adding a new bird meant editing code every other bird already depended on.
Read the first version
v1LSP
One Class Per Bird, a New Crack Appears
Splitting flying into per-bird subclasses fixed both problems above, until a penguin joined the zoo. `fly()` had nothing honest to contain, a subclass keeping its contract on paper while breaking the promise behind it.
Read where the split happened
v1.5Class Explosion
Splitting by Behavior, the Wrong Way
`FlyingBird` and `NotFlyingBird` fixed the penguin problem, but broke down the moment walking became a second independent behavior: classes doubling with every new behavior, and no single type left to mean "every bird that can fly."
Read why it broke down
v2LSP fix + ISP
Interfaces, the Right Tool for Behavior
`Bird` went back to one flat hierarchy. A `Flyable` interface, later joined by `Eater`, marked which birds actually had which capability, `Flyable[]` became the single type the design was missing. Merging the two interfaces into one would have broken the moment their overlap stopped being a coincidence.
Read how interfaces fixed it
v2.5DIP
A New Duplication, a New Violation
`Crow` and `Owl` flew in the exact same way, and so did `Parrot` and `Sparrow`. Pulling that shared code into behavior classes fixed the duplication, but left each bird directly dependent on one specific concrete class.
Read the new violation
v3DIP fix
The Final Shape
A `FlyingBehavior` interface stood between each bird and its flying logic. Then dependency injection removed the last hardcoded `new`: `Crow` just declares what it needs, and whoever builds one decides which behavior to hand over.
Read the final fix

That final version is the one that satisfies all five principles at once, not because it was designed top-down from a checklist, but because each principle showed up to fix one specific problem the previous version actually had.

This same shape shows up constantly outside of birds. A payment service that talks directly to one hardcoded bank integration has the exact same problem Crow had: the day that bank has an outage, the payment service goes down with it, for a decision that was never really its own to make. A PaymentGateway interface, one per bank, turns that into a one-place fix instead of a scramble through every file that ever mentioned that bank by name. The scale is different. The mistake and the fix are identical.