Concepts / Modeling and API shape / Designing method signatures

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