If you remember five things
Everything below is the defense of these five calls.
- Bundle data with behavior. An object owns its data and exposes the only legal ways to change it, so it keeps itself in a valid state.
- Four pillars, each with a why. Encapsulation and abstraction hide; inheritance and polymorphism reuse and extend. Say the why, never just the name.
- Composition by default. Prefer "has a" over "is a." Reach for inheritance only when a real is-a holds and you want polymorphism.
- Polymorphism kills the type switch. A list of shapes answering area() their own way means new types never touch old callers (Open/Closed).
- Judgment beats class count. Knowing when NOT to make a class, and when NOT to inherit, is the staff-level signal.
1The mental model (what this really is)#
OOP organizes code around things instead of steps. Procedural code is a recipe: do this, then this. Object-oriented code is more like a company.
- You have employees (objects). Each holds some private information (data) and knows how to do certain jobs (methods).
- You get work done by asking objects to do their jobs, not by reaching into their desks and shuffling their papers yourself.
- The part most people miss: OOP is not about classes and
newkeywords. It is about who owns what data, and who is allowed to change it. - A good object is a small, trustworthy unit. It protects its own data and exposes a clean set of operations. You can use it without knowing how it works inside.
The one-liner to say in the room.
- "OOP bundles data together with the behavior that operates on that data, so each object is responsible for keeping itself in a valid state."
- Say that and the interviewer knows you get it.
A framing that lands even better.
- An object is a tiny state machine with a guard rail around it.
- The data is the state. The public methods are the only legal transitions. Everything else, the outside world cannot touch.
- Describe your
BankAccountthat way and you are already ahead of the person who just says "it has fields and methods."
Access control is the whole point. If only two guarded methods can change a balance, then a "balance went negative" bug lives in exactly those two methods, not in the 200 files that touch the account. OOP is a strategy for shrinking where bugs can hide.
2The four pillars (and what each is actually for)#
Every interviewer expects the four pillars. Reciting definitions is a trap. Say why each one exists.
Encapsulation. Keep data private, touch it only through methods.
- The point: nothing outside can put the object into a broken state.
- A
BankAccountnever exposesbalanceas a public field anyone can set to negative. It exposesdeposit()andwithdraw(), and those guard the rules. - This is the pillar senior engineers care about most, because it is what keeps large codebases from rotting.
- Concrete payoff: if balance only changes through two guarded methods, the "went negative" bug has exactly two places to live.
Abstraction. Hide the messy details behind a simple interface.
- When you call
list.sort(), you do not care if it is quicksort or merge sort. - Abstraction is about what an object does, not how.
- The sharp distinction to say out loud: encapsulation is about access (who can touch this field), abstraction is about design (what surface do I expose, what do I keep off the menu).
Inheritance. Let one class reuse and extend another.
- A
SavingsAccountis aBankAccountwith extra interest logic. You get code reuse and a type hierarchy. - It is the pillar people overuse (more in gotchas).
- The real reason it exists is not reuse, it is substitutability: a
SavingsAccountmust work anywhere aBankAccountis expected.
Polymorphism. Different objects respond to the same call in their own way.
- You hold a list of
Shapeobjects and callarea()on each. A circle computes one way, a square another. The caller does not branch on type. - This is where OOP earns its keep: add new types without touching old code.
- For depth, name the two flavors: runtime polymorphism (overriding, resolved by the object's actual type at call time) and compile-time polymorphism (overloading, resolved by the compiler from the argument types).
I am not listening for the four words. I can read those off a flashcard. I am listening for the second clause: encapsulation so the object cannot break, polymorphism so I add types without editing callers. The candidate who gives me the why on each one just told me they have actually used these to keep a real codebase sane.
3The template (how it works, with pseudocode)#
Here is the shape of a well-formed class: private data, a constructor that sets up a valid state, and public methods that guard the rules.
class BankAccount:
def __init__(self, owner, opening_balance=0):
if opening_balance < 0:
raise ValueError("balance cannot start negative")
self._owner = owner # leading underscore = "private"
self._balance = opening_balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
def withdraw(self, amount):
if amount > self._balance:
raise ValueError("insufficient funds")
self._balance -= amount
def balance(self): # read-only access, no setter
return self._balance
What makes this correct.
- There is no
set_balance(). The balance only changes throughdepositandwithdraw, and both enforce rules. That is encapsulation doing its job. - The invariant "balance is never negative and never set arbitrarily" holds because the constructor checks the opening value and both mutators check their inputs.
- There is no fourth door into this object.
Language mechanics, because interviewers probe it.
- Python's single underscore
_balanceis a convention, not enforced. A double underscore__balancetriggers name mangling to make accidental access harder. - Java and C# have real keywords:
private(this class only),protected(this class plus subclasses),public(everyone). - If you claim a field is "private," be ready to say what that means in the language you picked.
Now inheritance and polymorphism:
class SavingsAccount(BankAccount):
def __init__(self, owner, opening_balance=0, rate=0.04):
super().__init__(owner, opening_balance)
self._rate = rate
def add_interest(self):
self.deposit(self._balance * self._rate) # reuses the guarded method
- Notice
add_interestcallsself.depositrather than touchingself._balancedirectly. Even the subclass respects the guard rail. - That is the discipline that keeps the invariant true as the hierarchy grows.
And polymorphism through a shared interface:
class Shape:
def area(self):
raise NotImplementedError # subclasses must define this
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r * self.r
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s * self.s
def total_area(shapes):
return sum(s.area() for s in shapes) # no type-checking, no if/else
Why total_area is the payoff.
- Add a
Triangletomorrow and this function never changes. That is what to point at when someone asks "why bother with polymorphism." - The deeper name is the Open/Closed Principle: open for extension (new shapes), closed for modification (you never reopen
total_area). - Say that phrase and you connect the toy example to a real design principle.
A type switch is a function that has to be reopened every time the world grows. A polymorphic call is one that never does.
4A worked example (walk through one)#
The classic OOP coding question: "Design a parking lot." Think out loud the way you should in the room.
Start from nouns and verbs.
- Nouns are candidate classes: parking lot, parking spot, vehicle, ticket.
- Verbs are the methods: park a car, remove a car, find an open spot.
Then find what varies.
- Vehicles come in sizes: motorcycle, car, bus. Spots come in sizes too.
- A bus cannot fit a motorcycle spot. That size relationship is the heart of the problem, so model it explicitly.
class Vehicle:
def __init__(self, size): # size: 1=moto, 2=car, 3=bus
self.size = size
class Spot:
def __init__(self, size):
self.size = size
self.vehicle = None
def can_fit(self, vehicle):
return self.vehicle is None and vehicle.size <= self.size
def park(self, vehicle):
if not self.can_fit(vehicle):
raise ValueError("vehicle does not fit here")
self.vehicle = vehicle
def free(self):
self.vehicle = None
class ParkingLot:
def __init__(self, spots):
self._spots = spots
def park(self, vehicle):
for spot in self._spots:
if spot.can_fit(vehicle):
spot.park(vehicle)
return spot
return None # lot full for this size
Walk through the reasoning out loud.
can_fitlives onSpot, notParkingLot. The spot owns the rule about what fits in it. That is encapsulation: the knowledge sits next to the data.ParkingLot.parkdoes not know how fitting works. It just asks each spot. Change the fitting rule later and you touch one place.- I used a size integer instead of three subclasses. In the room: "I could make Motorcycle, Car, Bus subclasses, but there is no behavior difference, only a size, so a field is simpler." Choosing not to use inheritance is a strong signal.
Reveal the answer
Then call out the next move before they ask.
- "Right now
parkis a linear scan, O(n) over every spot. With 10,000 spots on every entry, I would keep a free-spot index per size, a queue of open spots per size class, so finding a spot is O(1)." - Same with the
Ticketclass: it holds the spot reference and entry time, and pricing by duration lives on the ticket, not smeared across the lot. - That judgment is what separates a pass from a strong pass. Interviewers want to see judgment, not just the ability to make classes.
5Variations and gotchas#
Composition vs inheritance. The single most important OOP opinion to have.
- Inheritance says "is a." Composition says "has a." If you find yourself inheriting just to borrow a method, stop.
- Hold the other object as a field and call it. A
Carshould have anEngine, not be anEngine. - The rule for juniors: prefer composition by default, reach for inheritance only when there is a genuine "is a" and you want polymorphism.
- Why composition wins is coupling. Inheritance depends on the parent's internals, so a base-class change can break you silently. Composition depends only on the other object's public methods, a smaller, safer contract.
Liskov Substitution Principle. The test for whether an "is a" is honest.
- If
BextendsA, then anywhere the code expects anA, aBmust work without surprises. - The classic violation:
SquareextendsRectangle. A rectangle lets you set width and height independently; a square cannot without breaking its own definition. - A function that sets width to 4, height to 5, and expects area 20 gets garbage from a square.
- If a subclass has to weaken or contradict the parent's promises, it is not really a subtype. That is your cue to use composition.
Abstract classes vs interfaces.
- An interface is a pure contract: a list of methods with no implementation.
- An abstract class can carry shared code plus methods left for subclasses to fill in.
- Interface when unrelated classes need to share a capability (a
Birdand aPlanecan both beFlyable). Abstract class when related classes share real code. - One-line rule: a class can implement many interfaces but inherit from only one class, so interfaces are "can do this" and abstract classes are "is this kind of thing."
Overloading vs overriding. Easy to confuse, often asked.
- Overriding: a subclass replacing a parent method with the same signature, resolved at runtime based on the object's real type.
- Overloading: multiple methods, same name, different parameters, resolved at compile time from the argument types.
- Different mechanisms: one is about type hierarchies, the other is convenience in a single class.
The diamond problem.
- A class inherits from two parents that share a common ancestor. Which version of a method wins?
- This is why Java bans multiple inheritance of classes and uses interfaces instead.
- Python allows it and resolves the ambiguity with a defined Method Resolution Order (C3 linearization), which you can inspect with
ClassName.__mro__. - Name-drop this if multiple inheritance comes up.
Making Stack extend ArrayList so you get the methods "for free," then not seeing the problem when I point it out. Now your stack exposes get(index) and add(index, element), so any caller can poke into the middle and break LIFO. Inheriting for reuse instead of substitutability is a textbook tell that you have not felt the pain of a brittle hierarchy.
6Trade-offs#
OOP is not free, and saying so makes you sound senior.
What you gain.
- Code that maps to the real domain.
- Encapsulated state that is hard to corrupt.
- The ability to add new types without rewriting old logic.
What it costs.
- Indirection. Following one request through five objects and three layers of inheritance is painful.
- Brittleness. Deep hierarchies break quietly: change the base class and you can break subclasses you forgot existed.
- Overhead. For small scripts or heavy data-crunching, plain functions are often clearer than ceremony around classes.
Many entities, evolving rules
- Shines when
- You model interacting things with state worth protecting.
- Buys you
- Guarded invariants and new types without touching old code.
- Example
- Parking lot, order system, game entities.
Data crunching, math routines
- Shines when
- You transform data straight through with no state to guard.
- Buys you
- Less indirection, easier to read top to bottom.
- Example
- A data pipeline, a numeric kernel, a one-off script.
My honest take.
- OOP shines for systems with many interacting entities and evolving rules. For a data pipeline or a math-heavy routine, functional or procedural is usually cleaner.
- Good engineers mix both and do not treat OOP as a religion.
- A concrete tell: if your "class" has no private state to protect and is just a bag of static methods, it wanted to be a module of functions all along.
7Mistakes that cost people the round#
- Reciting definitions with no "why." Anyone can memorize "encapsulation means data hiding." Say what it protects you from.
- Public fields everywhere. Raw mutable data with no guards throws away encapsulation. Interviewers notice immediately.
- Inheriting for code reuse.
StackextendingArrayListto get the methods is the classic anti-pattern. Now a stack exposesget(index)and callers can violate LIFO. Use composition: hold anArrayListas a private field, expose onlypushandpop. - God objects. One class that does everything. A class name ending in
Managerwith twenty methods is a smell. Each class should have one reason to change (Single Responsibility Principle). - Forgetting polymorphism. Writing
if type == "circle": ... elif type == "square": ...instead of a method call is the exact thing OOP exists to kill. See that chain and the interviewer will ask you to refactor it. - No judgment on when to use it. "I would make a class for everything" is as bad as never using classes. Show you pick the right tool.
8Questions interviewers actually ask (with strong answers)#
The exact prompts, phrased the way they come up, with the answer I would give.
"What are the four pillars of OOP?"
- Do not just list them. "Encapsulation and abstraction are both about hiding: encapsulation protects an object's data so it cannot be put in a bad state, abstraction exposes a simple interface and hides the complexity behind it. Inheritance and polymorphism are about reuse and extension: inheritance builds an is-a type hierarchy, polymorphism lets different types answer the same call their own way, so you add new types without editing old callers."
- Then offer one example each if they want depth.
"Composition or inheritance, which do you prefer and why?"
- "Composition by default. Inheritance couples me to a parent's internals, so a base-class change can break subclasses silently, and it only makes sense when there is a true is-a I will use polymorphically. Composition couples me only to another object's public methods, a smaller contract. I reach for inheritance when substitutability actually holds, Liskov holds, and I want polymorphism."
- That answer shows an opinion and the reasoning behind it.
"What is the difference between an abstract class and an interface?"
- "An interface is a pure contract, no implementation, and a class can implement many of them, so it is for a capability like
Flyable. An abstract class can carry shared code plus abstract methods, and you can inherit only one, so it is for a family of related types that share real behavior." - Mention that modern Java interfaces allow default methods if they want to split hairs.
"Overloading vs overriding?"
- "Overriding is a subclass redefining a parent method with the same signature, resolved at runtime by the object's actual type. Overloading is same method name, different parameters, resolved at compile time by the arguments. One is polymorphism across a hierarchy, the other is naming convenience in a class."
"Can you give a real example of encapsulation from code you have written?"
- This separates people who have shipped from people who have read. Have a real one ready.
- Mine is the
BankAccountpattern above: no public setter for balance, every mutation goes through a guarded method, so the "never negative" invariant lives in two places instead of everywhere.
"Why is OOP better than procedural code?"
- Trap question. Reject the premise. "It is not universally better. OOP wins when I am modeling many interacting entities with evolving rules. For a data pipeline or a math-heavy routine, procedural or functional is cleaner. I pick based on whether there is state worth protecting."
- Refusing the false binary is the senior answer.
"Refactor this if/elif type switch."
- Covered in the practice path below. Turn each branch into a class that implements a shared method, then call the method polymorphically. They want to see the
if/elifdisappear.
9Your practice path#
Do these in order. Each one adds a new muscle.
- Model a deck of cards. Easy warm-up.
Card,Deck, withshuffle()anddeal(). Gets you comfortable with encapsulation and a clean public interface. - Design a parking lot. The one above. Practice choosing field-vs-subclass and placing rules on the right object, then volunteer the O(1) free-spot index as the scaling follow-up.
- Build a library or elevator system. Multiple interacting objects and state transitions. Elevators force state machines inside an object:
Idle,MovingUp,MovingDown, orDoorsOpen, each state deciding which actions are legal. - Implement a shape hierarchy with
area()andperimeter(). Drill polymorphism until writing an abstract base and overriding feels automatic. - Refactor an
if/eliftype switch into polymorphism. Take a payment processor withif method == "card"... elif "paypal"...and turn each branch into a class with apay()method, then callprocessor.pay()with no branching. This is the single most common "improve this code" prompt.
For each, say your noun/verb reasoning out loud and justify every inheritance choice. The talking matters as much as the code.
"Make this field private. What does that actually mean in your language?"
"Square extends Rectangle. What goes wrong?"
"Why not just inherit from ArrayList to build a Stack?"
"Java has no multiple inheritance of classes. How does Python handle it?"
"When would you NOT use OOP at all?"
10Where this shows up#
OOP design questions ("design X") are a staple in object-design and low-level-design rounds.
- Amazon, Microsoft, and most product companies run them. Amazon in particular is known for prompts like parking lot, elevator, and library in the loop, and they push on your class boundaries and complexity.
- The four-pillars and composition-vs-inheritance conceptual questions show up in phone screens almost everywhere, especially for Java and C# shops.
- Startups lean on it in take-homes where they watch how you structure a small app.
- Even in system design rounds, your class modeling of the core entities is often the first thing you sketch.
What you can now explain in the room
- Why OOP is really about who owns data and who may change it, not about classes and `new`.
- All four pillars with the reason each exists, not just the definitions.
- Why composition beats inheritance by default, and the coupling argument behind it.
- How polymorphism kills a type switch and satisfies the Open/Closed Principle.
- When an "is a" is dishonest (Liskov), using the Square-Rectangle trap.
- When to skip OOP entirely, and how to spot a class that wanted to be functions.
Want to see exactly which rounds each company runs this in? Check the company guides to map this pattern to the interviews you are actually taking.
Get a prep plan built around your Object-Oriented Programming interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.