OOP for LLD

OOP for LLD interviews

Encapsulation, abstraction, polymorphism, and inheritance — with interview-shaped bad/good examples and a bias toward composition.

Mechanisms, not buzzwords

Four OOP pillars
Encapsulation · abstraction · polymorphism · inheritance.
Design principles tell you how to think about clean code. OOP concepts are the language tools that implement those ideas. This page is a focused refresher on the four that matter in interviews — what they are, why interviewers care, and how they show up in parking lots, payments, and games.

Analogy: kitchen stations

Separation of concerns
Each station owns one job.

SOLID in a restaurant kitchen: the grill doesn’t also run the register (SRP). You can swap the fryer brand without rewriting recipes (OCP via interfaces). Servers depend on “food ready” tickets, not on a specific chef’s nickname (DIP).

Encapsulation

Keep an object’s data private and let the object control how that data changes. Callers use methods; they don’t poke fields. Predictability is the win: deposit / withdraw can enforce non-negative balances, log, and update related state. A public balance field cannot.
Bad — open guts:
class ParkingLot:
    spots: list  # public — callers mutate freely


class ParkingSpot:
    def occupy(self, vehicle): ...
class ParkingLot {
    public List<ParkingSpot> spots;  // public — callers mutate freely
}

class ParkingSpot {
    void occupy(Vehicle vehicle) { /* ... */ }
}
Good — owned mutations:
class ParkingLot:
    def __init__(self):
        self._spots: list[ParkingSpot] = []

    def park(self, vehicle) -> bool:
        spot = self._find_available(vehicle)
        if spot is None:
            return False
        spot.occupy(vehicle)
        return True

    def spots(self) -> list[ParkingSpot]:
        return list(self._spots)  # copy / view, not the live list
class ParkingLot {
    private final List<ParkingSpot> spots = new ArrayList<>();

    boolean park(Vehicle vehicle) {
        ParkingSpot spot = findAvailable(vehicle);
        if (spot == null) return false;
        spot.occupy(vehicle);
        return true;
    }

    List<ParkingSpot> spots() {
        return List.copyOf(spots);  // copy / view, not the live list
    }
}

Abstraction

Expose what’s essential; hide how it works behind a clear contract. Use abstractions where logic has variations, messy rules, or multiple approaches — not everywhere.
Bad — coupled to Stripe:
class OrderService:
    def checkout(self, order):
        stripe = StripeAPI()
        stripe.set_api_key(self.api_key)
        stripe.create_charge(order.total, order.credit_card)
class OrderService {
    void checkout(Order order) {
        StripeAPI stripe = new StripeAPI();
        stripe.setApiKey(this.apiKey);
        stripe.createCharge(order.total, order.creditCard);
    }
}
Good — depend on a payment contract:
class PaymentMethod(Protocol):
    def process(self, amount: float) -> bool: ...


class OrderService:
    def __init__(self, payment: PaymentMethod):
        self._payment = payment

    def checkout(self, order) -> None:
        self._payment.process(order.total)
interface PaymentMethod {
    boolean process(double amount);
}

class OrderService {
    private final PaymentMethod payment;

    OrderService(PaymentMethod payment) {
        this.payment = payment;
    }

    void checkout(Order order) {
        payment.process(order.total);
    }
}

Polymorphism

Replace if type == … with “call the same method; each object handles itself.” Flexibility vs traceability is a real tradeoff — some teams prefer explicit branches. In interviews, explain why you chose polymorphism.
Bad — type switches in the lot:
def park_vehicle(self, v):
    if v.type == "car":
        return self._find("regular") is not None
    if v.type == "motorcycle":
        return self._find("motorcycle") is not None
    if v.type == "truck":
        return self._find("large") is not None
    return False
boolean parkVehicle(Vehicle v) {
    if ("car".equals(v.type)) {
        return find("regular") != null;
    }
    if ("motorcycle".equals(v.type)) {
        return find("motorcycle") != null;
    }
    if ("truck".equals(v.type)) {
        return find("large") != null;
    }
    return false;
}
Good — type carries its requirement:
class Vehicle(Protocol):
    def required_spot_size(self) -> SpotSize: ...


def park_vehicle(self, v: Vehicle) -> bool:
    spot = self._find(v.required_spot_size())
    return spot is not None
interface Vehicle {
    SpotSize requiredSpotSize();
}

boolean parkVehicle(Vehicle v) {
    ParkingSpot spot = find(v.requiredSpotSize());
    return spot != null;
}

Inheritance — use sparingly

Inheritance shares implementation but couples children to the parent (fragile base class). Prefer interfaces + composition for behavior variation. Use inheritance when subclasses genuinely share stable implementation.
Good fit — shared account mechanics:
class BankAccount:
    def __init__(self):
        self._balance = 0.0

    def deposit(self, amount: float) -> None:
        self._balance += amount

    def withdraw(self, amount: float) -> bool:
        if self._balance < amount:
            return False
        self._balance -= amount
        return True


class SavingsAccount(BankAccount):
    def __init__(self, rate: float):
        super().__init__()
        self.rate = rate


class CheckingAccount(BankAccount):
    def __init__(self, overdraft: int):
        super().__init__()
        self.overdraft = overdraft
class BankAccount {
    private double balance = 0.0;

    void deposit(double amount) {
        balance += amount;
    }

    boolean withdraw(double amount) {
        if (balance < amount) return false;
        balance -= amount;
        return true;
    }
}

class SavingsAccount extends BankAccount {
    final double rate;
    SavingsAccount(double rate) { this.rate = rate; }
}

class CheckingAccount extends BankAccount {
    final int overdraft;
    CheckingAccount(int overdraft) { this.overdraft = overdraft; }
}
Bad fit — inheritance for different engines:
class Car:
    def start_engine(self): ...  # gasoline story


class ElectricCar(Car):
    def start_engine(self):
        raise NotImplementedError("no engine")  # LSP smell
class Car {
    void startEngine() { /* gasoline story */ }
}

class ElectricCar extends Car {
    @Override
    void startEngine() {
        throw new UnsupportedOperationException("no engine");  // LSP smell
    }
}
Better — compose drivetrain:
class Drivetrain(Protocol):
    def start(self) -> None: ...


class Car:
    def __init__(self, drivetrain: Drivetrain):
        self._drivetrain = drivetrain

    def start(self) -> None:
        self._drivetrain.start()
interface Drivetrain {
    void start();
}

class Car {
    private final Drivetrain drivetrain;

    Car(Drivetrain drivetrain) {
        this.drivetrain = drivetrain;
    }

    void start() {
        drivetrain.start();
    }
}

Apply without reciting

You don’t need to say “polymorphism” out loud. When requirements say “multiple payment methods,” define an interface. Keep fields private. When you reach for type checks, reach for an interface instead.
  • Encapsulation — hide state, expose behavior
  • Abstraction — contracts for variation
  • Polymorphism — objects handle themselves
  • Inheritance — compose first; inherit only when sharing is real

Worked mini-example: Library Book vs BookItem

Encapsulation: BookItem hides state transitions. Polymorphism: FinePolicy. Inheritance: resist BookEbook extends Book until a real behavioral fork appears.

class BookItem:
    def checkout(self, member):
        if self.state != "AVAILABLE":
            raise RuntimeError("unavailable")
        self.state = "LOANED"  # invariant stays inside

class FinePolicy(Protocol):
    def compute(self, loan, returned_at) -> int: ...

class FlatPerDay:
    def compute(self, loan, returned_at):
        days = max(0, (returned_at - loan.due).days)
        return days * 100  # cents
class BookItem {
    void checkout(Member member) {
        if (!"AVAILABLE".equals(state)) {
            throw new IllegalStateException("unavailable");
        }
        state = "LOANED";  // invariant stays inside
    }
}

interface FinePolicy {
    int compute(Loan loan, LocalDateTime returnedAt);
}

class FlatPerDay implements FinePolicy {
    public int compute(Loan loan, LocalDateTime returnedAt) {
        long days = Math.max(0, ChronoUnit.DAYS.between(loan.due, returnedAt));
        return (int) days * 100;  // cents
    }
}

OOP anti-patterns on the whiteboard

  • Inheritance for types that only differ by data (VehicleCar, VehicleBike trees).
  • Public fields mutated from anywhere — invariants leak.
  • Interfaces with one implementation forever “just in case.”
  • SOLID recited as a poem without pointing at a class.

OOP decision checklist

  1. Can this be an enum + data before a subclass?
  2. Who is allowed to mutate this field?
  3. Do I need polymorphism (multiple behaviors) or just configuration?
  4. Does inheritance buy reused behavior — or only reused names?
  5. Which SOLID principle is actually under pressure in this prompt?

Common interview pitfalls

These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.

  • Inheritance as default modeling tool.
  • Getters/setters everywhere — no behavior.
  • SOLID name-drop without an example class.
  • Abstraction that hides the wrong thing (can’t test invariants).
  • Polymorphism via boolean flags instead of types/strategies.

Interview script (say this)

Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.

  1. I’ll use encapsulation so invariants live with the data.
  2. Abstraction shows intent — ParkingLot.enter not spot fiddling.
  3. Polymorphism when behavior varies — PricingStrategy, not if-else sprawl forever.
  4. Inheritance only when it’s clearly “is-a” with shared behavior.
  5. On library: Book vs BookItem is the classic encapsulation split.
  6. I’ll apply SOLID as pressure checks, not a checklist recital.
  7. Example: Open/Closed via new Limiter type without editing orchestrator.
  8. If a principle isn’t under pressure, I won’t force it.

Extra verification traces

Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.

Staff-level follow-ups

At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.

  • When is inheritance OK? — Shared behavior + substitution; else compose.
  • SRP conflict? — Split orchestration from policy objects.
  • DIP in 35m? — One Port interface where a twist is likely (Payment, Sink).

← Lattice