Facebook Pixel

Failure Modes: Over-Engineering & Pattern-Forcing

A small set of mistakes accounts for most weak LLD answers, and they tend to appear in a consistent order. This lesson describes the six most common failure modes, explains why each one lowers the score, and shows a corrected approach for each.

Diving in before clarifying requirements

Opening an editor before asking a single question is a common early mistake. A candidate who begins with "I'll define a Notification class" the moment the prompt lands has not yet established what they are building. Interviewers provide one-sentence prompts intentionally — the prompt is incomplete, and the questions a candidate asks reveal engineering judgment.

Clarifying requirements is itself design work. Whether notifications are sent via email, SMS, or push; whether the caller cares about delivery confirmation; and whether retries are in scope — each answer either removes a class that would otherwise be built unnecessarily, or surfaces a constraint that changes the design.

Before naming any entity, restate the prompt in your own words and ask two or three targeted questions. Write an in-scope list and an out-of-scope list. Only then begin writing classes.

Pattern-forcing

The second failure mode is reaching for a design pattern when a plain function or a plain field would do. Candidates who have studied patterns sometimes feel pressure to demonstrate them, so they install a NotificationFactory because the problem involves creating objects, or a Singleton because there is one notification service, or an Observer because events are mentioned. Each of these can be right in context. None of them is right by default. (Factory, Singleton, Observer, and the rest are previewed here only as cautionary examples; they are taught properly in Module 4 — you do not need to know them yet.)

Adding an unnecessary pattern usually costs more than omitting one. A Factory wrapping a two-line constructor adds a class and a layer of indirection without removing any duplication. A pattern is justified only when it removes a specific problem: duplicated creation logic, the need for a single shared instance, or a publisher that should not know its subscribers. If that problem cannot be named, the pattern does not belong.

Writing the simplest code that satisfies the requirements listed in Step 1 is the correct baseline. If a natural smell surfaces during implementation, name it and introduce the pattern at that point. Most clean designs use zero or one pattern.

A notification service that dispatches messages by channel — email, SMS, push — illustrates the difference concretely. The code below shows pattern-forcing alongside a plain alternative.

1# Bad: Factory added with no smell to remove.
2# There is one creation site, one line of logic per channel,
3# and no duplication. The factory is pure ceremony.
4class NotificationSenderFactory:
5    @staticmethod
6    def create(channel: str) -> "NotificationSender":
7        if channel == "email":
8            return EmailSender()
9        elif channel == "sms":
10            return SmsSender()
11        elif channel == "push":
12            return PushSender()
13        raise ValueError(f"unknown channel: {channel}")
14
15class NotificationService:
16    def send(self, user_id: str, message: str, channel: str) -> None:
17        sender = NotificationSenderFactory.create(channel)
18        sender.send(user_id, message)
1# Good: a plain dict maps channel to sender — zero pattern overhead.
2# If creation logic grows complex or senders need pooling,
3# that is the moment to consider a factory.
4class NotificationService:
5    def __init__(self) -> None:
6        self._senders: dict[str, "NotificationSender"] = {
7            "email": EmailSender(),
8            "sms": SmsSender(),
9            "push": PushSender(),
10        }
11
12    def send(self, user_id: str, message: str, channel: str) -> None:
13        sender = self._senders.get(channel)
14        if sender is None:
15            raise ValueError(f"unsupported channel: {channel}")
16        sender.send(user_id, message)

The god class

The third failure mode is the god class: one class that owns every piece of state in the system, with public mutable fields that any caller can reach into and change. It usually appears when a candidate skips Step 2 (entities and relationships) and starts writing directly in a single file. Everything ends up on the same object because there was never a moment to ask which noun should own which piece of state.

A god class is a maintenance problem in production and a design problem in an interview. It tells the interviewer that the candidate does not think in terms of encapsulation or single responsibility. The fix is mechanical: at Step 2, identify the nouns in the requirements, assign one responsibility to each noun, and keep the state for that responsibility private behind methods.

Premature abstraction

The fourth failure mode is introducing interfaces and configurability for requirements that do not exist. You are not asked to design for every future channel, every future payment gateway, or every future pricing model — you are asked to design for the requirements you listed in Step 1. Adding an IPaymentProcessor interface, a PaymentProcessorFactory, and a configuration layer when there is only one payment processor in scope is YAGNI — You Aren't Gonna Need It — and adds machinery no requirement uses.

Extensibility and over-abstraction are different. Extensibility means the design can absorb a real follow-up without a rewrite; over-abstraction means it pre-builds extension points for requirements that do not exist. Build for what was asked. In Step 6 (Extensibility), show that a natural follow-up fits — but do not implement the follow-up unprompted.

Scope creep

The fifth failure mode is gold-plating instead of finishing the core. Time is fixed at roughly forty minutes. A candidate who spends fifteen minutes on a polished retry mechanism for notifications while the core Checkout flow is unimplemented has misread the problem. The interviewer cannot grade what is not there.

The discipline from Step 4 applies directly: implement the happy path first, add edge-case guards second, sketch the rest as stubs with a brief comment. If time is short, a working skeleton with clear method signatures scores higher than a half-finished tangle of edge cases.

A checkout flow shows this clearly. The following code demonstrates scope creep alongside a focused alternative.

1# Bad: gold-plating retry logic and idempotency keys while
2# the Checkout class itself is still a stub.
3class PaymentGateway:
4    def charge(
5        self,
6        amount: int,
7        card_token: str,
8        idempotency_key: str,
9        max_retries: int = 3,
10        backoff_base: float = 0.5,
11    ) -> "ChargeResult":
12        for attempt in range(max_retries):
13            try:
14                return self._attempt_charge(amount, card_token, idempotency_key)
15            except TransientError:
16                time.sleep(backoff_base * (2 ** attempt))
17        raise PaymentError("all retries exhausted")
18
19class Checkout:
20    pass   # TODO: not started — time ran out
1# Good: happy path for Checkout is complete and correct.
2# PaymentGateway.charge() is a thin stub — the interviewer can see
3# its signature and knows retries can be added in Step 6.
4class PaymentGateway:
5    def charge(self, amount: int, card_token: str) -> "ChargeResult":
6        # stub: calls external processor, returns result
7        ...
8
9class Checkout:
10    def __init__(self, cart: "Cart", gateway: PaymentGateway) -> None:
11        self._cart = cart
12        self._gateway = gateway
13
14    def complete(self, card_token: str) -> "Order":
15        total = self._cart.total()
16        result = self._gateway.charge(total, card_token)
17        if not result.success:
18            raise PaymentError(result.error)
19        return Order(items=self._cart.items(), total=total)

Going silent

Going quiet for several minutes while the interviewer waits is a distinct failure mode, and one that does not appear in the code at all. LLD interviews are collaborative. When the interviewer steers the conversation — "what if we need to support a third payment method?" — that is either filling in a requirement that was missed or checking whether the design can absorb a change without a rewrite. In either case, the appropriate response is to engage immediately: restate what was heard, identify what changes in the design, and narrate the adjustment.

Communication is a graded dimension in every LLD rubric. An interviewer who cannot follow the reasoning cannot award credit for it, regardless of the code quality. Narrating each significant decision in a sentence keeps the reasoning visible. "I'm making PaymentMethod an interface here because the requirements mention two processors — that's the natural extension point" states the reasoning in one sentence, so the interviewer can follow it and credit it.

Keeping the six straight

The six failure modes form a natural arc through the interview timeline.

#Failure modeWhen it strikes
1No requirementsFirst two minutes
2Pattern-forcingEntity step
3God classClass design
4Premature abstractionClass design
5Scope creepImplementation
6Going silentThroughout

The delivery framework from the previous lesson addresses the first five structurally. Following its steps in order — requirements, then entities, then class design, then implementation — makes each of those failure modes harder to fall into. The sixth is addressed through practice: narrating decisions aloud, engaging with every steer from the interviewer, and treating the session as collaborative rather than a solo exercise.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro