Skip to content

14. Honest Admission — even the neatest floor plan has grey corners

~13 min read. Good design advice helps, but pretending certainty everywhere is dangerous.

Built on the ELI5 in 00-eli5.md. The floor plan — class diagram or schema design — still has uncertain edges when one room grows faster than the hallway contracts around it.


1) Patterns can become over-engineering when the problem is still tiny

See. Students learn patterns and then want to use all of them. Factory here, Strategy there, Builder somewhere else. Soon a login form has twelve classes and zero real complexity. That is not maturity. That is decoration. Good LLD matches abstraction depth to problem depth. Use this contrast. ┌──────────────┐ ┌──────────────┐ │ small CRUD │ │ overdesigned │ ├──────────────┤ ├──────────────┤ │ UserService │ │ UserFacade │ │ UserRepo │ │ UserFactory │ │ UserController│ │ UserStrategy │ └──────────────┘ │ UserAdapter │ │ UserProxy │ └──────────────┘ Worked example. A profile update flow validates name and saves one row. Traffic is 20 requests per minute. No alternate providers exist. No complex workflow exists. A simple service plus repository may be enough. Adding five abstractions only raises reading cost. Concrete code-level contrast.

class ProfileService {
  async updateName(userId: string, name: string) {
    if (name.length < 2) throw new Error('invalid')
    await this.repo.updateName(userId, name)
  }
}
class ProfileUpdateStrategyFactoryResolverFacade {
  build() { /* everybody cries */ }
}
Simple, no? The hard part is not knowing patterns. The hard part is resisting them when the problem does not demand them.

2) SOLID is useful, but pragmatism decides where to stop splitting

Single Responsibility sounds obvious until real code arrives. Should validation, formatting, persistence, and telemetry all live separately? Sometimes yes. Sometimes no. If you split too early, navigation becomes painful. If you split too late, classes become sticky and fragile. So what to do? Ask what kind of change you expect most. Design around probable change, not theoretical purity. Diagram. ┌──────────────┐ │ InvoiceService│ ├──────────────┤ │ validate │ │ tax compute │ │ persist │ │ emit event │ └──────┬───────┘ │ split only when reasons diverge ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │Validator │ │TaxPolicy │ │Repo/Event│ └──────────┘ └──────────┘ └──────────┘ Worked example. A tax rule changes every quarter. Persistence schema changes twice a year. Telemetry naming changes rarely. That tells you TaxPolicy likely deserves isolation before telemetry does. Concrete code-level hint.

class InvoiceService {
  Money finalize(Invoice invoice) {
    validator.check(invoice);
    invoice.apply(taxPolicy.forRegion(invoice.region()));
    repo.save(invoice);
    return invoice.total();
  }
}
See the point. SOLID gives questions, not divine answers. Interviewers like candidates who can state tradeoffs without worshipping slogans.

3) DDD is powerful, but not every team can afford the full ceremony

Domain-Driven Design gives rich language and boundaries. Very good. But full DDD can demand heavy modeling discipline. It wants aggregates, value objects, bounded contexts, events, repositories, and ubiquitous language discipline. That can be excellent in payments or insurance. It can also overwhelm a four-person startup shipping simple workflows. Diagram of the tension. ┌──────────────┐ ┌──────────────┐ │ rich domain │ │ simple app │ ├──────────────┤ ├──────────────┤ │ many rules │ │ few rules │ │ many teams │ │ one team │ │ long lifespan│ │ fast pivot │ └──────┬───────┘ └──────┬───────┘ │ use more DDD │ use lighter model ▼ ▼ aggregates/events services + tables Worked example. Loan approval has dozens of rules, states, exceptions, and compliance steps. DDD investment may pay back there. A basic internal asset tracker with CRUD screens may not need aggregate factories and domain events on day one. Concrete code-level smell.

class AssetAggregateRootFactoryBuilder:
    pass
If the team cannot explain why that class exists, it probably should not. Simple, no? Rich modeling is a tool. Not a badge.

4) AI system patterns are still moving targets

This is the most honest admission. Classic LLD books were not written for prompt routers, vector stores, and eval harnesses. We are adapting old ideas to new workloads. Sometimes the mapping is strong. Strategy for model swap makes sense. Observer for monitoring makes sense. But many AI components are still evolving rapidly. Today one team builds prompt templates as files. Tomorrow another team treats them as versioned policies in a registry. Both may be reasonable. Diagram. ┌──────────────┐ → ┌──────────────┐ → ┌──────────────┐ │ prompt store │ │ model router │ │ eval harness │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ assumptions shift │ models shift │ metrics shift ▼ ▼ ▼ design today redesign soon redesign again Worked example. A routing rule based on token count may work today. Next month latency variance, safety policy, or tool availability may dominate instead. That means AI LLD needs extra humility. Concrete code-level guard.

interface RouterPolicy {
  route(req: PromptRequest, context: RuntimeSignals): RouteDecision
}
Keep policy injectable. Do not freeze volatile knowledge into rigid class trees.

5) The practical answer: design for clarity first, reversibility second

When advice conflicts, use two checkpoints. First, can a new engineer explain the design in five minutes? Second, can you change a likely requirement without surgery across ten files? If both answers are yes, you are probably fine. If clarity is low, simplify names and boundaries. If reversibility is low, extract one seam where change pressure is highest. See this checklist. ┌──────────────────────────────┐ │ Can I explain each class job?│ │ Can I test the hot path fast?│ │ Can one likely change fit well?│ │ Can ops observe failures? │ └──────────────────────────────┘ Worked example. A notification service today sends email only. Roadmap shows SMS next quarter. You do not need a cosmic plugin system today. But you may define a NotificationSender interface now because the next change is likely and concrete. That is balanced design. Concrete code-level sketch.

interface NotificationSender {
  void send(Message msg);
}
Simple, no? Pragmatism is not messy coding. It is disciplined betting under uncertainty.


Where this lives in the wild

At Amazon, a senior engineer on internal tooling may reject extra abstractions because the workflow is stable and tiny. At Stripe, a staff engineer on money movement might embrace heavier domain modeling because rules and audit pressure are intense. At Notion, a product engineer may keep collaboration features lightweight until extension points become clearly valuable. At CRED, a backend engineer can feel the SOLID-versus-speed tension while shipping fast fintech experiments. At OpenAI, an applied AI engineer must accept that prompt routing and evaluation patterns are still changing quickly.


Pause and recall

What signal tells you a pattern is helping rather than decorating? Why is SOLID better used as questions than commandments? When can DDD pay off strongly? Why should AI LLD include extra reversibility?


Interview Q&A

Why not apply every good pattern you know?

Because each abstraction adds reading, debugging, and maintenance cost that must be justified by change pressure. Common wrong answer to avoid: More patterns always mean more scalable design.

Why pragmatism not strict SOLID everywhere?

Pragmatism adapts boundaries to real change frequency, team size, and codebase pressure. Common wrong answer to avoid: If a class has two concerns, it is automatically bad design.

Why lighter design not full DDD for every service?

DDD pays when the domain is rich and long-lived, not when the workflow is mostly CRUD and short-lived. Common wrong answer to avoid: Serious engineers always use aggregates, value objects, and domain events.

Why admit uncertainty in AI LLD?

Because the workloads, tools, and platform constraints are changing fast, so rigid certainty becomes technical debt. Common wrong answer to avoid: Once you pick an AI architecture, best practice is basically settled.


Apply now (5 min)

Take one tiny feature and one complex feature from your current project. Write which patterns you would keep, skip, or delay for each. Then name one seam you would introduce only because a likely future change is visible. Sketch from memory: draw a simple floor plan for a small service, then draw the over-engineered version and circle the unnecessary boxes.


Bridge. LLD taught us how to design software interiors. Next module, we step out of the building and study how systems talk across networks. → ../03_networking_internet_fundamentals/00-eli5.md