Home/Blog/Object-Oriented Design Interviews: How to Crack Low-Level Design Questions at Zoho, Freshworks, and More
InterviewCareerSystem DesignPlacements

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.

S
SCS TeamΒ·25 February 2026Β·13 min read

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:

  1. Can you translate a real-world system into classes and relationships?
  2. Do you apply OOP principles (encapsulation, inheritance, polymorphism) correctly?
  3. Do you know when to use design patterns?
  4. 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.