The If-Else Chain Hiding Inside a Single Bird Class

Why one Bird class trying to hold every kind of flying turns into an if-else chain, and the five real problems that chain causes in production.

September 14, 20265 min read1 / 20

"Design a bird" is a real interview question at Amazon. It sounds like a joke the first time you hear it. It isn't.

Picture a zoo management system. It needs to store every kind of bird it holds: sparrows, crows, pigeons, peacocks, penguins. It needs their details, and it needs their behavior. How do you structure the classes for that?

The One-Class Answer Everyone Starts With

The first instinct almost everyone has: one Bird class, done. Give it a few attributes, give it a few methods, ship it.

class Bird { public: double weight; std::string color; std::string type; std::string name; bool canFly; void fly() { /* ... */ } void eat() { /* ... */ } void walk() { /* ... */ } };

You'd use it like this:

Bird b1; b1.color = "blue"; b1.type = "sparrow"; b1.fly();

This compiles. It runs. It is not what the interview is testing.

Every Bird Doesn't Fly the Same Way

Look closer at fly(). Not every bird flies the same way. A sparrow's flight has nothing in common with a penguin's. So what actually goes inside that method?

void fly() { if (type == "sparrow") { // sparrow-specific flying logic } else if (type == "crow") { // crow-specific flying logic } else if (type == "pigeon") { // pigeon-specific flying logic } // ...and one more branch for every bird that gets added }

This is the actual shape of the "one Bird class" solution. One method, branching on a string, forever.

Five Problems Hiding in One Method

Nobody needs a formal definition to feel that this is bad. Here's what's actually wrong with it, spelled out.

It's hard to understand
A method that's a hundred lines of nested branches can't be read in one pass. You have to trace which branch applies before you can trust what the code does.
It's hard to test
Every branch is a separate path through the code, and a real test suite has to cover every one of them. More branches packed into one method just means more paths to keep covered.
It causes regressions
Adding a new bird means editing a method that every other bird's flying already depends on. Touch that method, and there's now a real chance you break a branch you never meant to touch.
It blocks parallel work
If one engineer is writing the sparrow branch and another is writing the crow branch, they're editing the exact same method at the exact same time. That's a merge conflict waiting to happen, not a possibility.
It duplicates code
Some birds do share real flying logic, takeoff, stabilizing in the air, whatever it is. But once that logic lives inside separate if-branches, sharing it means copying it into each branch by hand.

What "Responsibility" Actually Means

All five of these problems trace back to one root cause, and that root cause has a name: the Single Responsibility Principle, the S in SOLID. It states it plainly: every code unit, a class, a method, a package, should have exactly one responsibility.

"Responsibility" has a specific meaning here, and it isn't obvious on first read.

Responsibility means reason to change.

Look at what Bird is actually responsible for. It's not just "holding bird data." It's responsible for how a sparrow flies, how a crow flies, how a pigeon flies, and every bird added after that. Adding a new bird, or changing how an existing one flies, both mean opening the exact same class. That's not one responsibility. That's one responsibility per bird type, all crammed into a single class.

LLD Has No Single Right Answer

One thing worth saying before going further: low-level design questions rarely have one correct answer. Whether a class has "one responsibility" or three is a judgment call, not a formula, and reasonable engineers can define that boundary differently depending on context.

What actually matters in an interview isn't landing on the one true design. It's naming the trade-off you're making and explaining why you're making it. Every design decision trades something for something else, and being able to say what you gave up and what you got in return is the actual skill being tested.

There's a practical question that makes "responsibility" easy to spot in your own code, instead of just a definition to memorize: why would I need to open this class again? Ask that about Bird, and the answer keeps landing on the same class, no matter which bird changes. The next post turns that question into concrete signs you can check for in any codebase, not just this one.