Object-Oriented Design Interview: Patterns, Problems and How to Approach Them
A practical guide to OOD interviews covering the approach framework, essential design patterns, classic problems like Parking Lot and Library System, and what interviewers actually look for.
Object-oriented design interviews are the underrated sibling of system design interviews. While everyone preps for "design Twitter," fewer people prepare for "design a parking lot" or "design an elevator system." That's a mistake. OOD rounds show up at Amazon, Google, Microsoft, and most mid-to-senior level interviews. And unlike system design, there are more objectively right and wrong answers.
Here's the thing: OOD interviews aren't testing whether you've memorized the Gang of Four book. They're testing whether you can decompose a real-world problem into clean classes, relationships, and interfaces. The patterns are tools, not goals.
OOD vs System Design
These get confused constantly, so let's be clear:
System Design is architecture-level. You're drawing boxes for services, databases, load balancers, and message queues. You're thinking about scale, availability, and network calls. "Design a URL shortener" is system design. Object-Oriented Design is class-level. You're defining classes, interfaces, inheritance hierarchies, and method signatures. You're thinking about encapsulation, abstraction, and SOLID principles. "Design a parking lot" is OOD.Some interviews blend both, but most OOD rounds stay at the class level. You'll write actual class skeletons, not architecture diagrams.
The Five-Step Approach
Every OOD problem can be tackled with the same framework. Internalize this and you'll never freeze when handed an unfamiliar prompt.
Step 1: Clarify Requirements (2-3 min). Don't start coding. Ask questions. For a parking lot: How many levels? What vehicle types? Payment system? Reserved spots? This isn't stalling — it's scoping. Step 2: Identify Core Objects (3-5 min). List the nouns in the problem. Those are candidate classes. For a parking lot: Vehicle, Car, Truck, ParkingSpot, ParkingLot, Ticket. Not every noun becomes a class — some are just attributes. Step 3: Define Relationships (3-5 min). Inheritance (Car IS-A Vehicle), composition (ParkingLot HAS spots), or association (Ticket references Vehicle)? Draw this before writing code. Step 4: Apply Design Patterns where they fit naturally. Don't force them. But recognize when the problem calls for Strategy, Factory, or State. Step 5: Write Class Skeletons for the remaining time. Method signatures, key attributes, important logic. Not full implementations.Essential Patterns for OOD Interviews
You don't need all 23 Gang of Four patterns. These six cover 90% of what comes up:
Strategy — Swap Behavior at Runtime
Classic interview scenario: payment processing with multiple methods.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> bool: pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def pay(self, amount: float) -> bool:
print(f"Charging ${amount} to card {self.card_number[-4:]}")
return True
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> bool:
print(f"Charging ${amount} to PayPal {self.email}")
return True
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def process(self, amount: float) -> bool:
return self.strategy.pay(amount)
Adding a new payment method means adding a new class, not modifying existing code. That's Open/Closed in action.
Observer — Notify Without Coupling
When one object changes and others need to know. The publisher doesn't know about its subscribers:
class EventBus:
def __init__(self):
self._subscribers: dict[str, list] = {}
def subscribe(self, event: str, callback):
self._subscribers.setdefault(event, []).append(callback)
def publish(self, event: str, data):
for cb in self._subscribers.get(event, []):
cb(data)
# OrderService publishes events, doesn't know who listens
order_events.publish("order_placed", order)
Factory — Encapsulate Object Creation
class DocumentFactory:
@staticmethod
def create(file_type: str) -> Document:
factories = {"pdf": PDFDocument, "docx": WordDocument}
if file_type not in factories:
raise ValueError(f"Unsupported: {file_type}")
return factories[file_type]()
doc = DocumentFactory.create("pdf") # Caller doesn't know construction details
State — Object Behavior Changes with Internal State
This one shows up in vending machine and elevator problems constantly. The idea: instead of massive if/else blocks checking the current state, each state is its own class with its own behavior.
class VendingMachineState(ABC):
@abstractmethod
def insert_coin(self, machine, amount: float): pass
@abstractmethod
def select_item(self, machine, item: str): pass
class IdleState(VendingMachineState):
def insert_coin(self, machine, amount):
machine.balance += amount
machine.state = HasMoneyState()
def select_item(self, machine, item):
print("Please insert coins first.")
class HasMoneyState(VendingMachineState):
def insert_coin(self, machine, amount):
machine.balance += amount
def select_item(self, machine, item):
price = machine.inventory[item]["price"]
if machine.balance >= price:
machine.balance -= price
print(f"Dispensing {item}")
machine.state = IdleState()
else:
print(f"Need ${price - machine.balance:.2f} more.")
The machine delegates to its current state object. Adding a new state (e.g., MaintenanceState) means adding a class, not modifying existing logic.
Classic OOD Problem: Parking Lot
This is the most common OOD interview question. Here's a clean solution:
from enum import Enum
from datetime import datetime
class VehicleSize(Enum):
MOTORCYCLE = 1
COMPACT = 2
LARGE = 3
class Vehicle:
def __init__(self, license_plate: str, size: VehicleSize):
self.license_plate = license_plate
self.size = size
class Car(Vehicle):
def __init__(self, license_plate: str):
super().__init__(license_plate, VehicleSize.COMPACT)
class ParkingSpot:
def __init__(self, spot_id: str, size: VehicleSize, level: int):
self.spot_id = spot_id
self.size = size
self.level = level
self.vehicle: Vehicle | None = None
def can_fit(self, vehicle: Vehicle) -> bool:
return self.vehicle is None and vehicle.size.value <= self.size.value
def park(self, vehicle: Vehicle):
if not self.can_fit(vehicle):
raise ValueError("Vehicle cannot fit in this spot.")
self.vehicle = vehicle
def remove_vehicle(self):
self.vehicle = None
class ParkingLot:
def __init__(self, spots: list[ParkingSpot]):
self.spots = spots
self.active_tickets: dict[str, tuple[Vehicle, ParkingSpot, datetime]] = {}
def park(self, vehicle: Vehicle) -> bool:
spot = next((s for s in self.spots if s.can_fit(vehicle)), None)
if not spot:
return False
spot.park(vehicle)
self.active_tickets[vehicle.license_plate] = (vehicle, spot, datetime.now())
return True
def unpark(self, license_plate: str):
if license_plate in self.active_tickets:
_, spot, _ = self.active_tickets.pop(license_plate)
spot.remove_vehicle()
Key design decisions to discuss: Vehicle hierarchy uses inheritance (IS-A). ParkingLot uses composition (HAS spots). can_fit allows a motorcycle in a compact spot (smaller fits in larger). Entry time tracking enables payment calculation.
Classic OOD Problem: Library Management System
public class Book {
private String isbn, title, author;
private boolean isAvailable = true;
// constructor, getters, setters
}
public class Member {
private String memberId, name;
private List<Loan> activeLoans = new ArrayList<>();
private static final int MAX_LOANS = 5;
public boolean canBorrow() { return activeLoans.size() < MAX_LOANS; }
}
public class Loan {
private Book book;
private Member member;
private LocalDate borrowDate, dueDate, returnDate;
public boolean isOverdue() {
return returnDate == null && LocalDate.now().isAfter(dueDate);
}
}
public class Library {
private Map<String, Book> catalog; // ISBN -> Book
private Map<String, Member> members; // ID -> Member
public Loan borrowBook(String memberId, String isbn) {
Member member = members.get(memberId);
Book book = catalog.get(isbn);
if (!member.canBorrow()) throw new IllegalStateException("Loan limit");
if (!book.isAvailable()) throw new IllegalStateException("Unavailable");
book.setAvailable(false);
Loan loan = new Loan(book, member, LocalDate.now(), LocalDate.now().plusDays(14));
member.getActiveLoans().add(loan);
return loan;
}
public void returnBook(Loan loan) {
loan.setReturnDate(LocalDate.now());
loan.getBook().setAvailable(true);
loan.getMember().getActiveLoans().remove(loan);
}
}
SOLID Principles (What Interviewers Actually Check)
Interviewers don't ask you to recite SOLID. They watch whether your design naturally follows these principles:
Single Responsibility: Each class does one thing. Don't put payment logic inParkingSpot.
Open/Closed: Adding a new vehicle type shouldn't require modifying ParkingLot.
Liskov Substitution: Any Vehicle subclass should work wherever Vehicle is expected, without surprises.
Interface Segregation: Don't force classes to implement methods they don't need. No charge_battery() on non-electric vehicles.
Dependency Inversion: PaymentProcessor depends on PaymentStrategy (abstraction), not CreditCardPayment (implementation).
Common Mistakes
Jumping to code immediately. Spend 5 minutes discussing objects and relationships first. Shows maturity. Over-engineering. You don't need every pattern for a parking lot. Start simple. The interviewer will ask for more if they want it. Forgetting edge cases. What if the lot is full? Truck in a motorcycle spot? Sixth book borrowed? Handle these. Not discussing trade-offs. "I chose composition over inheritance here because..." is exactly what interviewers want to hear.The best way to prepare is to practice designing real systems, writing the class skeletons, and explaining your decisions out loud. On CodeUp you can work through OOD problems with real code and structured exercises — which is how these patterns actually stick.