Mechanisms, not buzzwords
Analogy: kitchen stations
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
deposit / withdraw can enforce non-negative balances, log, and update related state. A public balance field cannot.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) { /* ... */ }
}
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
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);
}
}
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
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.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;
}
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
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; }
}
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
}
}
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
- 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
- Can this be an enum + data before a subclass?
- Who is allowed to mutate this field?
- Do I need polymorphism (multiple behaviors) or just configuration?
- Does inheritance buy reused behavior — or only reused names?
- 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.
- I’ll use encapsulation so invariants live with the data.
- Abstraction shows intent — ParkingLot.enter not spot fiddling.
- Polymorphism when behavior varies — PricingStrategy, not if-else sprawl forever.
- Inheritance only when it’s clearly “is-a” with shared behavior.
- On library: Book vs BookItem is the classic encapsulation split.
- I’ll apply SOLID as pressure checks, not a checklist recital.
- Example: Open/Closed via new Limiter type without editing orchestrator.
- 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).