Courses › Low-Level Design › Foundations

OOP & SOLID

Encapsulation, composition over inheritance and the five SOLID principles.

Low-level design interviews are judged on whether your classes are easy to understand, change and test. Object-oriented principles and SOLID are the vocabulary for that — use them to explain your decisions, not as a checklist to recite.

Core OOP ideas

  • Encapsulation — keep state private and expose behavior. A Spot should offer assign(vehicle) and release(), not a public occupied flag anyone can flip.
  • Abstraction — expose *what* something does, hide *how*. Callers use PricingStrategy.price(ticket) without knowing the formula.
  • Polymorphism — treat different implementations uniformly through a shared interface.
  • Composition over inheritance — build objects from smaller parts ("has-a") rather than deep class hierarchies ("is-a"). Inheritance couples subclasses tightly to parents; composition lets you swap parts at runtime.
# Inheritance explosion: GasCar, ElectricCar, GasTruck, ElectricTruck, ...
# Composition: a vehicle has a size and an engine.
class Vehicle:
    def __init__(self, plate: str, size: "VehicleSize", engine: "Engine"):
        self.plate = plate
        self.size = size
        self.engine = engine

SOLID

S — Single Responsibility

A class should have one reason to change. A Ticket holds ticket data; computing the fee belongs to a pricing component; printing it belongs elsewhere.

O — Open/Closed

Open for extension, closed for modification. Add new behavior by adding new classes, not by editing a growing if/elif chain.

class PricingStrategy:
    def price(self, ticket) -> int: ...

class HourlyPricing(PricingStrategy):
    def __init__(self, cents_per_hour): self.rate = cents_per_hour
    def price(self, ticket):
        hours = max(1, ceil(ticket.duration_minutes() / 60))
        return hours * self.rate

class FlatRatePricing(PricingStrategy):
    def __init__(self, cents): self.cents = cents
    def price(self, ticket): return self.cents

L — Liskov Substitution

A subclass must be usable anywhere its parent is expected, without surprises. The classic violation: a Square subclass of Rectangle whose set_width also changes the height. If a subtype has to throw "not supported" for a parent method, the hierarchy is wrong.

I — Interface Segregation

Prefer several small, focused interfaces over one large one. A Printer and a Scanner interface, rather than MultiFunctionDevice forcing every class to implement fax().

D — Dependency Inversion

Depend on abstractions, not concrete classes, and inject dependencies. A ParkingLot receives a SpotAllocationStrategy in its constructor instead of creating NearestSpotStrategy itself. This is also what makes classes testable — pass in a fake clock, a fake repository.

Other habits interviewers notice

  • Enums for fixed sets of values (VehicleSize.SMALL), not strings
  • Immutable value objects for things like money and coordinates
  • Clear error handling — raise specific exceptions or return explicit results, rather than silently failing
  • Thread safety where shared state is mutated

[!INTERVIEW] Name the principle when you use it "I'm making allocation a strategy interface so we can add new allocation rules without touching ParkingLot — open/closed." One sentence like that shows you design intentionally.