What it is
Object-oriented design, also called low-level or code design, is about how you split a program into classes. In these rounds you build something small but complete, like a parking lot, a snake game or a rate limiter. They care less about clever algorithms and more about whether your code is clear and easy to change.
How it works
- List the nouns in the problem. They're your candidate classes.
- List the actions. They're your methods, and each one goes on whichever class owns the data it changes.
- Build the simplest version that works end to end.
- When the interviewer adds a requirement, change as little as you can.
class Spot:
def __init__(self, spot_id, size):
self.spot_id = spot_id
self.size = size
self.car = None
class ParkingLot:
SIZES = ["small", "medium", "large"]
def __init__(self, spots):
self.free = {size: [] for size in self.SIZES}
for spot in spots:
self.free[spot.size].append(spot)
self.taken = {}
def park(self, car_id, size):
# A car fits in its own size of spot or anything bigger.
for fit in self.SIZES[self.SIZES.index(size):]:
if self.free[fit]:
spot = self.free[fit].pop()
spot.car = car_id
self.taken[car_id] = spot
return spot.spot_id
return None
def leave(self, car_id):
spot = self.taken.pop(car_id)
spot.car = None
self.free[spot.size].append(spot)Notice that adding a fourth size only means changing the list of sizes, and the lot never needs to know how spots are laid out. That's the kind of thing interviewers look for.
Trade-offs
| Choice | Good | Watch out for |
|---|---|---|
| Lots of small classes | Each one is easy to understand | Too many layers for a 45-minute round |
| One big class | Fast to write | Hard to change when requirements are added |
| Inheritance | Shares behavior between similar things | Deep hierarchies get brittle, so prefer combining small pieces |
In an interview
- Agree on the requirements and the main operations before writing code.
- Get something working early, then improve it.
- Name things well. It's one of the easiest ways to look organized.
Worked example
A rate limiter class, then a change
Build a rate limiter where allow(user_id) returns true if the user is under 5 requests in the last 10 seconds. Halfway through, the interviewer asks for a different algorithm for premium users.
- Start with the interface. RateLimiter has one method, allow(user_id) -> bool. Write a quick usage example to agree on how it's called.
- First version: a SlidingWindowLimiter that keeps a deque of timestamps per user. allow drops timestamps older than 10 seconds, then checks if fewer than 5 are left.
- Pass the clock in, like a now() function, instead of calling time.time() directly. Tests can control time without sleeping.
- The change arrives: premium users get a token bucket. Because callers only know about allow(), you write a TokenBucketLimiter with the same method and don't touch the old class.
- A small TieredLimiter holds one limiter per tier, looks up the user's tier, and calls the right one. It's a limiter too, so callers still don't change.
- Mention what you'd do next if asked: memory cleanup for users who've gone quiet, and a lock if it's used from several threads.
Follow-up questions
Things interviewers tend to ask once you've brought this up.
- How would you make it thread-safe?
- Put a lock around each user's check-and-update. One lock per user, or per group of users, avoids making everyone wait on one lock.
- When would you use inheritance?
- When things really share behavior and will keep doing so. For swapping algorithms, like here, give classes a shared interface and pass the one you want in.
- How would you test it?
- With a fake clock. Make 5 calls, check the 6th fails, move the clock forward 10 seconds, and check it passes again.
- What design patterns come up most?
- Strategy for swappable algorithms, like here. Observer for 'notify these things when something happens'. Factory for creating the right type from input. Name them only if they actually fit.
Common mistakes
- Drawing a big class hierarchy before anything works.
- Calling the clock directly, so nothing can be tested without waiting.
- Putting every method in one giant class.
- Rewriting everything when a requirement changes, instead of extending.
- Naming patterns to sound smart when the code doesn't use them.