Object-Oriented Design Interviews: How to Crack Low-Level Design Questions at Zoho, Freshworks, and More
Low-Level Design (LLD) questions test your ability to model real-world systems with classes, relationships, and design patterns. This guide teaches the framework and walks through 3 complete designs from scratch.
If you're targeting Zoho, Freshworks, Chargebee, or any mid-tier product company, you'll face Low-Level Design (LLD) questions. These are different from the system design questions asked at FAANG β they're about writing actual class structures, defining relationships, and applying OOP principles.
Most students haven't prepared for this at all. This guide closes that gap.
What LLD Questions Actually Test
Interviewers want to know:
- Can you translate a real-world system into classes and relationships?
- Do you apply OOP principles (encapsulation, inheritance, polymorphism) correctly?
- Do you know when to use design patterns?
- Is your design extensible β can it handle new requirements without major rewrites?
What they don't want: a class with 20 methods on it, or a flat procedural design wrapped in a class.
The 5-Step LLD Framework
Step 1: Identify the core entities (nouns)
Read the requirements and underline every noun. Each significant noun is a candidate for a class.
For a Library Management System: Book, User, Library, Librarian, Member, Loan, Reservation, Catalog.
Step 2: Identify relationships (verbs)
- Has-a (Composition): Library has Books. Member has Loans.
- Uses (Association): Librarian uses Catalog. Member uses Loan.
- Is-a (Inheritance): Librarian is a User. Member is a User.
- Can-do (Interface): Anything that can be searched implements Searchable.
Step 3: Define attributes and methods for each class
For each class, ask: what does it know (attributes) and what can it do (methods)?
Step 4: Apply relevant design patterns
Patterns you should know for LLD interviews: Singleton, Factory, Observer, Strategy, Decorator.
Step 5: Discuss extensibility
"How would you add [new feature]?" β always ask yourself this. If the answer requires changing 5 classes, your design is too coupled.
Complete Design 1: Parking Lot System
Requirements: Design a parking lot that can hold cars, bikes, and buses. Track occupied/available spots. Generate a ticket on entry, calculate fee on exit.
Step 1 β Entities
ParkingLot, Floor, Spot, Vehicle, Ticket, PaymentSystem
Step 2 β Relationships
- ParkingLot has multiple Floors
- Floor has multiple Spots
- Spot accommodates one Vehicle
- Entry creates a Ticket, exit closes it
Step 3 β Implementation
from enum import Enum
from datetime import datetime
from typing import Optional
class VehicleType(Enum):
CAR = "car"
BIKE = "bike"
BUS = "bus"
class SpotType(Enum):
COMPACT = "compact" # bikes
MEDIUM = "medium" # cars
LARGE = "large" # buses
class Vehicle:
def __init__(self, license_plate: str, vehicle_type: VehicleType):
self.license_plate = license_plate
self.vehicle_type = vehicle_type
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: SpotType):
self.spot_id = spot_id
self.spot_type = spot_type
self.vehicle: Optional[Vehicle] = None
def is_available(self) -> bool:
return self.vehicle is None
def can_fit(self, vehicle: Vehicle) -> bool:
compatibility = {
VehicleType.BIKE: [SpotType.COMPACT, SpotType.MEDIUM, SpotType.LARGE],
VehicleType.CAR: [SpotType.MEDIUM, SpotType.LARGE],
VehicleType.BUS: [SpotType.LARGE],
}
return self.spot_type in compatibility[vehicle.vehicle_type]
def park(self, vehicle: Vehicle):
if not self.is_available() or not self.can_fit(vehicle):
raise ValueError("Cannot park here")
self.vehicle = vehicle
def unpark(self) -> Vehicle:
vehicle = self.vehicle
self.vehicle = None
return vehicle
class Ticket:
def __init__(self, ticket_id: str, vehicle: Vehicle, spot: ParkingSpot):
self.ticket_id = ticket_id
self.vehicle = vehicle
self.spot = spot
self.entry_time = datetime.now()
self.exit_time: Optional[datetime] = None
def close(self):
self.exit_time = datetime.now()
def duration_hours(self) -> float:
end = self.exit_time or datetime.now()
delta = end - self.entry_time
return delta.total_seconds() / 3600
HOURLY_RATES = {
VehicleType.BIKE: 10,
VehicleType.CAR: 30,
VehicleType.BUS: 100,
}
class ParkingLot:
def __init__(self, name: str):
self.name = name
self.spots: list[ParkingSpot] = []
self.active_tickets: dict[str, Ticket] = {} # license_plate β Ticket
self._ticket_counter = 0
def add_spot(self, spot: ParkingSpot):
self.spots.append(spot)
def _find_spot(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
return next(
(s for s in self.spots if s.is_available() and s.can_fit(vehicle)),
None
)
def enter(self, vehicle: Vehicle) -> Ticket:
if vehicle.license_plate in self.active_tickets:
raise ValueError("Vehicle already parked")
spot = self._find_spot(vehicle)
if not spot:
raise ValueError("No available spot")
spot.park(vehicle)
self._ticket_counter += 1
ticket = Ticket(f"T{self._ticket_counter:04d}", vehicle, spot)
self.active_tickets[vehicle.license_plate] = ticket
print(f"Parked {vehicle.license_plate} at spot {spot.spot_id}")
return ticket
def exit(self, license_plate: str) -> float:
if license_plate not in self.active_tickets:
raise ValueError("Vehicle not found")
ticket = self.active_tickets.pop(license_plate)
ticket.close()
ticket.spot.unpark()
rate = HOURLY_RATES[ticket.vehicle.vehicle_type]
fee = ticket.duration_hours() * rate
print(f"Fee for {license_plate}: βΉ{fee:.2f}")
return fee
def available_spots(self) -> int:
return sum(1 for s in self.spots if s.is_available())
Extension discussion: "How would you add reservations?" β Add a Reservation class, modify _find_spot to exclude reserved spots. No existing class changes needed β just additions. That's good design.
Complete Design 2: Library Management System
Requirements: Members can borrow and return books. Fines for overdue returns. Librarians manage the catalog. Search by title, author, or ISBN.
from datetime import datetime, timedelta
from typing import Optional
from enum import Enum
class BookStatus(Enum):
AVAILABLE = "available"
BORROWED = "borrowed"
RESERVED = "reserved"
class Book:
def __init__(self, isbn: str, title: str, author: str, copies: int = 1):
self.isbn = isbn
self.title = title
self.author = author
self.total_copies = copies
self.available_copies = copies
def is_available(self) -> bool:
return self.available_copies > 0
def borrow(self):
if not self.is_available():
raise ValueError("No copies available")
self.available_copies -= 1
def return_book(self):
self.available_copies += 1
class Loan:
BORROW_DAYS = 14
FINE_PER_DAY = 5 # βΉ5 per day overdue
def __init__(self, book: 'Book', member_id: str):
self.book = book
self.member_id = member_id
self.borrow_date = datetime.now()
self.due_date = self.borrow_date + timedelta(days=self.BORROW_DAYS)
self.return_date: Optional[datetime] = None
def is_overdue(self) -> bool:
check_date = self.return_date or datetime.now()
return check_date > self.due_date
def calculate_fine(self) -> int:
if not self.is_overdue():
return 0
check_date = self.return_date or datetime.now()
overdue_days = (check_date - self.due_date).days
return overdue_days * self.FINE_PER_DAY
class Member:
MAX_BOOKS = 3
def __init__(self, member_id: str, name: str):
self.member_id = member_id
self.name = name
self.active_loans: list[Loan] = []
self.loan_history: list[Loan] = []
def can_borrow(self) -> bool:
return len(self.active_loans) < self.MAX_BOOKS
def borrow(self, book: Book) -> Loan:
if not self.can_borrow():
raise ValueError(f"Maximum {self.MAX_BOOKS} books allowed")
book.borrow()
loan = Loan(book, self.member_id)
self.active_loans.append(loan)
return loan
def return_book(self, isbn: str) -> int:
loan = next((l for l in self.active_loans if l.book.isbn == isbn), None)
if not loan:
raise ValueError("Book not found in active loans")
loan.return_date = datetime.now()
fine = loan.calculate_fine()
self.active_loans.remove(loan)
self.loan_history.append(loan)
loan.book.return_book()
return fine
class Catalog:
def __init__(self):
self._books: dict[str, Book] = {} # isbn β Book
def add_book(self, book: Book):
self._books[book.isbn] = book
def search_by_title(self, query: str) -> list[Book]:
q = query.lower()
return [b for b in self._books.values() if q in b.title.lower()]
def search_by_author(self, query: str) -> list[Book]:
q = query.lower()
return [b for b in self._books.values() if q in b.author.lower()]
def get_by_isbn(self, isbn: str) -> Optional[Book]:
return self._books.get(isbn)
Complete Design 3: Elevator System
Requirements: Multiple elevators in a building. Handle floor requests. Minimise waiting time.
from enum import Enum
from typing import Optional
class Direction(Enum):
UP = "up"
DOWN = "down"
IDLE = "idle"
class Elevator:
def __init__(self, elevator_id: int, total_floors: int):
self.id = elevator_id
self.current_floor = 1
self.direction = Direction.IDLE
self.requests: set[int] = set() # floors to stop at
def add_request(self, floor: int):
self.requests.add(floor)
def step(self):
"""Move one floor toward next request."""
if not self.requests:
self.direction = Direction.IDLE
return
if self.direction == Direction.UP or (
self.direction == Direction.IDLE and
any(f > self.current_floor for f in self.requests)
):
target = min(f for f in self.requests if f >= self.current_floor)
self.direction = Direction.UP
else:
target = max(f for f in self.requests if f <= self.current_floor)
self.direction = Direction.DOWN
# Move toward target
if self.current_floor < target:
self.current_floor += 1
elif self.current_floor > target:
self.current_floor -= 1
# Stop if reached
if self.current_floor == target:
self.requests.discard(target)
if not self.requests:
self.direction = Direction.IDLE
class ElevatorController:
def __init__(self, num_elevators: int, num_floors: int):
self.elevators = [Elevator(i, num_floors) for i in range(num_elevators)]
def _nearest_elevator(self, floor: int) -> Elevator:
"""Assign request to closest idle/same-direction elevator."""
return min(
self.elevators,
key=lambda e: abs(e.current_floor - floor)
)
def request(self, floor: int):
elevator = self._nearest_elevator(floor)
elevator.add_request(floor)
print(f"Elevator {elevator.id} assigned to floor {floor}")
Design Patterns to Know for LLD Interviews
| Pattern | When to Use | Example |
|---|---|---|
| Singleton | Only one instance should exist | Database connection, Configuration |
| Factory | Creating objects without specifying class | VehicleFactory.create("car") |
| Observer | Notify multiple objects of state changes | Elevator notifying floor panels |
| Strategy | Swap algorithms at runtime | Different fee calculation strategies |
| Decorator | Add behavior without modifying class | Adding logging to Catalog search |
How to Answer in an Interview
Minutes 0-2: Ask clarifying questions. What are the core use cases? Any scale constraints? What can I ignore for now?
Minutes 2-5: Identify entities, draw a rough class diagram on the whiteboard/paper.
Minutes 5-20: Write the core classes. Start with the most central class (Parking Lot, Library Catalog), then expand.
Minutes 20-25: Discuss extensibility. "If we needed to add X, I'd change Y here without touching the rest."
One thing interviewers love: Saying "I'm using the Strategy pattern here for fee calculation so we can add different pricing tiers later without modifying the ParkingLot class."
Practice by designing: Tic-Tac-Toe, Snake Game, Chess (just pieces and board, not AI), Hotel Booking System, Movie Ticket Booking. Use AI Tutor to get feedback on your class diagrams.
Ready to practice what you just learned?
Apply these concepts with AI-powered tools built for CS students.