Designing method signatures
Inputs, outputs, errors and names that make misuse hard.
01The intuition
The problem it solves
A classmate writes a hit counter for a website. The method is record(x, y, z=None, flag=False): x is a timestamp sometimes in seconds and sometimes in milliseconds, y is a user id or a page name depending on the flag, and it returns -1 for errors, 0 for "nothing counted" and otherwise the count, except on Sundays. Every caller has to read the body to use it, and a wrong call is accepted without a sound and gives a wrong number.
class Counter:
def record(self, x, y, z=None, flag=False):
# x: time (s or ms?), y: user or page, z: weight?, flag: which mode
if flag: self._pages[y] = self._pages.get(y, 0) + (z or 1)
else: self._users[y] = self._users.get(y, 0) + (z or 1)
return -1 if x < 0 else len(self._users)
c = Counter()
c.record(1700000000000, "home", 1, True) # is that ms? did it count a user or a page?
Build it one piece at a time
Step one: one method, one job. Split the two modes into two methods with names that say what they do and parameters that say what they are, with types. A caller can now be wrong only in ways the type checker sees.
class HitCounter:
def hit(self, page: str, at: int) -> None:
"""Count one hit on page at time at, in whole seconds."""
def hits(self, page: str, now: int) -> int:
"""Hits on page in the 300 seconds ending at now."""
Step two: make wrong values impossible or loud. A unit that can be confused gets its own type; a precondition that fails raises a specific exception instead of returning a magic number.
from dataclasses import dataclass
@dataclass(frozen=True)
class Seconds:
value: int
def __post_init__(self) -> None:
if self.value < 0: raise ValueError("time cannot be negative")
class HitCounter:
WINDOW = 300
def hit(self, page: str, at: Seconds) -> None:
if not page: raise ValueError("page must be non-empty")
self._events.append((at.value, page))
def hits(self, page: str, now: Seconds) -> int:
lo = now.value - self.WINDOW
return sum(1 for t, p in self._events if p == page and lo < t <= now.value)
Step three: decide the shape of results and of time once. Return plain values for queries, nothing for commands, and never both; inject the clock so tests do not depend on the wall. The signature now reads as a sentence: hit this page at this time; how many hits in the last five minutes as of now.
from typing import Callable
class HitCounter:
def __init__(self, clock: Callable[[], int]) -> None:
self._clock = clock
self._events: list[tuple[int, str]] = []
def hit(self, page: str) -> None:
self._events.append((self._clock(), page))
def hits(self, page: str) -> int:
now = self._clock(); lo = now - self.WINDOW
return sum(1 for t, p in self._events if p == page and lo < t <= now)
fake_time = [1000]
c = HitCounter(clock=lambda: fake_time[0])
c.hit("home"); fake_time[0] += 301; assert c.hits("home") == 0
Why it works
The principle: a method signature is a promise, and a good one makes the right call easy and the wrong call hard or impossible. The invariant is that every argument has one meaning and one unit, every error is an exception or a typed result, and the method's name is a verb that matches what it changes. Where it breaks: boolean flags that switch behaviour (split the method), optional parameters that change the meaning of others, returning None sometimes and a value other times, and mutable default arguments. Interviewers test by calling your API wrongly on purpose; the good design refuses with a clear message at the boundary.
The trade-off you must say out loud
Small types such as Seconds add classes and ceremony, and a ten-line script does not need them. The cost is worth it when a value crosses a boundary between people or modules, which is where units get confused. Say that you keep the public surface small (three or four methods) and that anything the caller does not need is private. The follow-up is often about pagination or bulk calls: whether hits should accept many pages at once, and what the result shape becomes.
Where it shows up in the 100 Google problems
- Hit counter: the example above; the interviewer pushes on the window boundary and the unit of time.
- LRU cache: get returns a sentinel or raises; put returns nothing; the choice must be stated.
- Time-based key-value store: set(key, value, t) and get(key, t) with the inclusive boundary written in the docstring.
- Cursor-based pagination API: the cursor is an opaque token, not a page number, and the signature says so.
- Restaurant waitlist: join, leave and seat with the identity of a party chosen so misuse is impossible.
02Used in these problems
- Design an LRU cache
- Design a key-value store with TTL
- Design a time-based key-value store
- Design a hash map and hash set from scratch
- Design a map with constant-time putAll
- Design a set with O(1) insert, delete and getRandom
- Design a phone book with concurrent access
- Design a frequency-ordered container with weighted random pick
- Track the user with the most distinct contacts
- Design a peeking iterator
- Design a zigzag (cyclic) iterator over k lists
- Design a flattening iterator over nested lists
- Design independent level-order iterators over a shared tree
- Design a cursor-based pagination API
- Design a running-median container
- Design a hit counter
- Design a logger rate limiter
- Design a playlist shuffler with a no-repeat window
- Design a timer that supports many concurrent timers
- Design a text editor with undo and redo
- Design a variable store with placeholder substitution
- Design an HTTP router with path parameters
- Design an in-memory file system
- Design a path-value file system API
- Design a meeting room reservation system
- Assign appointments to rooms
- Design a restaurant waitlist
- Design tic-tac-toe
- Design the Snake game
- Design Minesweeper
- Design a game matchmaking system
- Design a leaderboard class
- Design a logger that returns its top-K messages
- Design a Twitter feed (class level)
- Design an employee hierarchy with is_manager queries
- Design a role and permission system
- Design an in-process search index