Home › CS Fundamentals › Object-Oriented Programming
O

Object-Oriented Programming

If you can explain OOP with one good example and know when NOT to use inheritance, you beat most candidates. That is the whole game for this topic. OOP shows up in almost every interview that is not pure algorithms: the warm-up in a system design round, the "model this real-world thing" coding question, and the answer that makes you sound junior if you fumble it.

CS FundamentalsEasy11 min read

If you remember five things

Everything below is the defense of these five calls.

  1. 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.
  2. Four pillars, each with a why. Encapsulation and abstraction hide; inheritance and polymorphism reuse and extend. Say the why, never just the name.
  3. Composition by default. Prefer "has a" over "is a." Reach for inheritance only when a real is-a holds and you want polymorphism.
  4. Polymorphism kills the type switch. A list of shapes answering area() their own way means new types never touch old callers (Open/Closed).
  5. 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 new keywords. 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 BankAccount that way and you are already ahead of the person who just says "it has fields and methods."
The actual idea

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 BankAccount never exposes balance as a public field anyone can set to negative. It exposes deposit() and withdraw(), 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 SavingsAccount is a BankAccount with 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 SavingsAccount must work anywhere a BankAccount is expected.

Polymorphism. Different objects respond to the same call in their own way.

  • You hold a list of Shape objects and call area() 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).
From the interviewer's chair

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.

graph TD OOP[OOP Four Pillars] --> HIDE[Hiding] OOP --> REUSE[Reuse and Extend] HIDE --> ENC[Encapsulation protects data] HIDE --> ABS[Abstraction hides complexity] REUSE --> INH[Inheritance is-a type hierarchy] REUSE --> POLY[Polymorphism same call different behavior]
The clean split to remember. The left branch (encapsulation, abstraction) is about hiding; the right branch (inheritance, polymorphism) is about reusing and extending. Group them this way and you never blank on which pillar does what.

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 through deposit and withdraw, 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 _balance is a convention, not enforced. A double underscore __balance triggers 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_interest calls self.deposit rather than touching self._balance directly. 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 Triangle tomorrow 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_fit lives on Spot, not ParkingLot. The spot owns the rule about what fits in it. That is encapsulation: the knowledge sits next to the data.
  • ParkingLot.park does 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.
Pause before you read on: the lot has 10,000 spots and `park` runs on every car entering. What is the complexity, and what would the interviewer push you to fix?
Reveal the answer
The linear scan is O(n) over every spot, on every single entry. The fix is a free-spot index per size: a queue of open spots for each size class, so finding a spot is O(1). Volunteer this before they ask. Naming the complexity and the fix unprompted is exactly the follow-up they were loading up, and beating them to it reads as someone who has scaled this for real.

Then call out the next move before they ask.

  • "Right now park is 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 Ticket class: 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 Car should have an Engine, not be an Engine.
  • 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 B extends A, then anywhere the code expects an A, a B must work without surprises.
  • The classic violation: Square extends Rectangle. 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 Bird and a Plane can both be Flyable). 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.
Red flag that fails you here

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.
Reach for OOP
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.
Skip the classes
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. Stack extending ArrayList to get the methods is the classic anti-pattern. Now a stack exposes get(index) and callers can violate LIFO. Use composition: hold an ArrayList as a private field, expose only push and pop.
  • God objects. One class that does everything. A class name ending in Manager with 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 BankAccount pattern 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/elif disappear.

9Your practice path#

Do these in order. Each one adds a new muscle.

  1. Model a deck of cards. Easy warm-up. Card, Deck, with shuffle() and deal(). Gets you comfortable with encapsulation and a clean public interface.
  2. 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.
  3. Build a library or elevator system. Multiple interacting objects and state transitions. Elevators force state machines inside an object: Idle, MovingUp, MovingDown, or DoorsOpen, each state deciding which actions are legal.
  4. Implement a shape hierarchy with area() and perimeter(). Drill polymorphism until writing an abstract base and overriding feels automatic.
  5. Refactor an if/elif type switch into polymorphism. Take a payment processor with if method == "card"... elif "paypal"... and turn each branch into a class with a pay() method, then call processor.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.

Follow-ups the interviewer will ask (and your answers)
"Make this field private. What does that actually mean in your language?"
Depends on the language, and I will say which I picked. In Java and C#, `private` is enforced by the compiler: only this class sees it. In Python nothing is truly private: a single underscore is a convention, a double underscore triggers name mangling to make accidental access harder but not impossible. If I claim encapsulation in Python, I am leaning on discipline plus mangling, not a hard wall.
"Square extends Rectangle. What goes wrong?"
It violates Liskov. A rectangle lets you set width and height independently, so a function that sets width 4, height 5, and expects area 20 is relying on that. A square cannot honor it without breaking its own definition, so it returns garbage. The is-a is dishonest. I would drop the inheritance and use composition, or model both as a Shape with an area() each computes its own way.
"Why not just inherit from ArrayList to build a Stack?"
Because you inherit the whole surface, not just what you want. The stack now exposes get(index) and add(index, element), so callers can reach into the middle and break LIFO. Inheritance is substitutability, not a shortcut for borrowing methods. I hold an ArrayList as a private field and expose only push and pop. Composition gives me reuse without leaking the parent's API.
"Java has no multiple inheritance of classes. How does Python handle it?"
With a defined Method Resolution Order, the C3 linearization, inspectable via ClassName.__mro__. When two parents share an ancestor (the diamond), the MRO gives a single deterministic order for which method wins, so there is no ambiguity. Java sidesteps the problem entirely by allowing only single class inheritance plus multiple interfaces.
"When would you NOT use OOP at all?"
When there is no state worth protecting. A data pipeline or a math-heavy routine transforms data straight through, so functions are clearer than classes. The tell: if my class is just static methods with no private fields, it wanted to be a module of functions. I pick based on whether there are invariants to guard, not out of habit.

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.

Thanks. Check your inbox, your Object-Oriented Programming prep plan is on the way.