Concepts / SOLID and design principles / DRY, KISS and YAGNI

DRY, KISS and YAGNI

Remove duplication, keep it simple, build what is asked.

01The intuition

The problem it solves

Three slogans show up in code reviews and in the last ten minutes of design interviews. Each guards against a different way of wasting effort, and they pull against each other, so you need all three at once.

Here is a hit counter written by someone who has heard none of them. It counts hits in the last 5 minutes, and it is ready for features nobody asked for.

class HitCounter:
    def __init__(self, window_sec=300, shards=16, persist_path=None, metrics=None):
        self.window_sec = window_sec; self.shards = shards
        self.persist_path = persist_path; self.metrics = metrics
        self.hits = []; self.hits_backup = []
    def hit(self, t):
        self.hits.append(t); self.hits_backup.append(t)
        if self.metrics: self.metrics.incr("hit")
    def get_hits(self, t):
        return len([h for h in self.hits if t - h < self.window_sec])
    def get_hits_backup(self, t):
        return len([h for h in self.hits_backup if t - h < self.window_sec])

There is duplicated logic (two identical list scans), speculative complexity (shards, persistence and metrics with no caller), and the simple version is buried. The interviewer asked for hit and get_hits.

Build it one piece at a time

Step 1: YAGNI, "you aren't gonna need it". Delete every parameter and field that no requirement mentions. Sharding and persistence come back the day someone asks, with a real requirement to design against.

class HitCounter:
    def __init__(self, window_sec: int = 300):
        self.window_sec = window_sec
        self.hits: list[int] = []
    def hit(self, t: int) -> None:
        self.hits.append(t)
    def get_hits(self, t: int) -> int:
        return len([h for h in self.hits if t - h < self.window_sec])

Step 2: KISS, "keep it simple". Before optimising, check that the simple version is correct and readable. It is. Only now ask whether it is fast enough: the list grows forever, and get_hits scans everything. That is a real problem, so fix it with the simplest structure that works, a queue that drops old entries.

from collections import deque

class HitCounter:
    def __init__(self, window_sec: int = 300):
        self.window_sec = window_sec
        self.hits: deque[int] = deque()
    def _evict(self, t: int) -> None:
        while self.hits and t - self.hits[0] >= self.window_sec:
            self.hits.popleft()
    def hit(self, t: int) -> None:
        self._evict(t)
        self.hits.append(t)
    def get_hits(self, t: int) -> int:
        self._evict(t)
        return len(self.hits)

Step 3: DRY, "don't repeat yourself". The eviction rule now exists once, in _evict, and both public methods call it. If the window becomes inclusive instead of exclusive, one line changes. The earlier version had the rule in two places and would have drifted.

Step 4: know which slogan wins when they fight. If a second feature needs hits per second instead of per window, do not generalise HitCounter into a "TimeSeriesAggregator" on speculation (YAGNI). Write the second class simply (KISS), and only when the two share real logic, extract it (DRY). Duplication you can see is cheaper than an abstraction you guessed wrong.

StepLinesFeatures without a requirementCopies of the eviction ruleget_hits cost
Start1442O(all hits ever)
1701 (inline)O(all hits ever)
2 and 31201 (named)amortised O(1)

Why it works

Each slogan protects a different invariant. YAGNI keeps every line traceable to a requirement, so the design can be explained in the time available and changed without unpicking guesses. KISS keeps the first version obviously correct, which is the only base from which optimisation is safe. DRY keeps each rule in one place, so a change is one edit and tests cannot pass on one copy while the other is wrong.

Where they break: DRY applied to coincidental similarity. Two functions that look alike today but serve different masters, say a customer discount and an employee discount, will diverge, and a shared helper then grows flags. The test for DRY is "would these always change together". If not, the duplication is honest.

The trade-off you must say out loud

In an interview the risk is the opposite of real projects: candidates over-build to show range. Say the requirement you are solving, solve it simply, then name the extension point you would add if the interviewer asked, without adding it. The follow-up is usually that very extension ("now make it per client", "now scale it"), and because the simple version is small, you can change it on the spot. A sentence that lands well: "I kept this a deque because the requirement is one window; if we need many, the eviction rule is already in one method to lift out."

Where it shows up in the 100 Google problems

  • Design a hit counter: the deque version above is the whole answer until the interviewer asks for billions of hits, at which point you move to buckets, not before.
  • Design a set with O(1) insert, delete and getRandom: one list and one dictionary; a candidate who adds thread safety and persistence unprompted runs out of time.
  • Design tic-tac-toe: a 3×3 board with counters per row, column and diagonal; the n×n and infinite-grid variants are follow-ups, and the simple version adapts because the counting rule is in one place.
  • Design a URL shortener (class level): a counter and a base-62 encoder solve the stated problem; the strategy interface arrives when a second encoding is actually requested.

02Used in these problems