Trade-offs and over-engineering
When a pattern is a cost, and how to say so.
01The intuition
The problem it solves
Asked to write a function that checks whether a string has duplicate characters, a candidate who has just learned design patterns produces this.
from typing import Protocol
class DuplicateStrategy(Protocol):
def has_duplicates(self, s: str) -> bool: ...
class SetStrategy:
def has_duplicates(self, s: str) -> bool:
return len(set(s)) != len(s)
class DuplicateCheckerFactory:
@staticmethod
def create(kind: str = "set") -> DuplicateStrategy:
return SetStrategy()
class DuplicateChecker:
def __init__(self, strategy: DuplicateStrategy) -> None:
self.strategy = strategy
def check(self, s: str) -> bool:
return self.strategy.has_duplicates(s)
Four classes, one line of logic, and no second strategy anywhere in sight. The interviewer learns that the candidate can spell the patterns and cannot tell when not to use them. Over-engineering is a design fault like any other: it costs reading time, test surface and change effort, and it hides the one decision that mattered.
Build it one piece at a time
Step one: write the simplest thing that meets the stated requirement, and say its cost. Here that is one function and one sentence about it.
def has_duplicates(s: str) -> bool:
return len(set(s)) != len(s) # O(n) time, O(n) extra space
Step two: name the pressure that would justify more, and wait for it. If the interviewer adds "the strings are huge and memory is tight", the trade-off changes: sort in place or use a bitmap for a known alphabet. The second version is chosen by a stated constraint, not by habit.
def has_duplicates_low_memory(s: str) -> bool:
seen = 0 # one bit per lowercase letter
for ch in s:
bit = 1 << (ord(ch) - ord("a"))
if seen & bit:
return True
seen |= bit
return False # O(n) time, O(1) space, a–z only
Step three: when two real options exist, lay them side by side with the quantity that decides between them, choose, and say what would change your mind. That is the whole skill; the pattern vocabulary is just how the options are named.
| Option | Time | Extra space | Works for | Pick when |
|---|---|---|---|---|
| set of seen characters | O(n) | O(k) distinct chars | any text | default |
| sort then scan | O(n log n) | O(1) if in place | mutable input | memory is the limit |
| bitmap | O(n) | O(1) | small known alphabet | alphabet is fixed and memory matters |
The same discipline applies to class design. A strategy interface earns its place when a second strategy exists or is named in the requirements; a factory earns its place when creation has rules; a singleton almost never earns its place. The question to ask before adding a layer is "what change does this make cheaper, and has anyone asked for that change".
Why it works
Every abstraction is a bet that a certain kind of change will come. A bet that pays is an extensibility point; a bet that does not is a tax on every reader forever. Saying the trade-off out loud turns the bet into a decision the interviewer can evaluate, which is what the round is for. The simplest version first also gives you working code early, which is what a change request will be measured against.
Where it breaks: under-engineering has a cost too. Hard-coding the second rule into the first, or a God class that does everything, fails the same test from the other side. The signal for adding structure is concrete: a second variant exists, a dependency reaches outside the process, or two parts change at different rates. Until then, a function and a clear sentence beat a hierarchy.
value of an abstraction ≈ (probability the change comes) × (effort it saves) − (effort every reader spends on the indirection)
The trade-off you must say out loud
Say the simple design and its limits in the same breath: "a set, O(n) space; if memory is the constraint I would switch to a bitmap for a fixed alphabet." Then stop. Interviewers rate the ability to choose over the ability to list, and a candidate who names the pressure that would justify more complexity shows both. The follow-up that usually comes next is the very pressure you named, and now the second design is a short step rather than a rewrite.
Where it shows up in the 100 Google problems
- Hash map and hash set from scratch: chaining versus open addressing, chosen by load factor and deletion needs, with the simple one first.
- Lazy tee iterator: one shared buffer versus a buffer per consumer, decided by how far apart consumers drift.
- Duplicate detection over sliding windows: exact set versus precomputed summaries, switched when queries outnumber writes.
- Tic-tac-toe: the three-line win check is right for 3 by 3; counters are right when n grows.