Skip to content

Over-Engineering Anti-Patterns — Interview Q&A

Category: Development Anti-PatternsOver-Engineeringeffort spent solving problems you don't have. Covers (collectively): Premature Optimization · Speculative Generality · Gold Plating · Yo-yo Problem · Lasagna Code · Accidental Complexity · Soft Coding · Bikeshedding

A bank of 65+ interview questions and answers spanning recognition, calibration, architecture-scale judgment, and the runtime/toolchain cost of abstraction. Each answer models the reasoning a strong candidate gives — including the trade-offs and the other side of the argument. Use the <details> toggles to self-quiz: read the question, answer out loud, then expand.


Table of Contents

  1. Fundamentals / Junior
  2. Intermediate / Middle
  3. Senior — Architecture-Scale & Leading Teams
  4. Professional / Deep — The Performance Irony
  5. Code-Reading — Diagnose the Snippet
  6. Curveballs
  7. Rapid-Fire / One-Liners
  8. How to Talk About Over-Engineering in Interviews
  9. Summary
  10. Related Topics

Fundamentals / Junior

Definitions, YAGNI/KISS, and recognition.

Q1. Name the eight over-engineering anti-patterns and give a one-line symptom for each.

Answer - **Premature Optimization** — tuning code for speed before any profile proves it's slow. - **Speculative Generality** — abstractions (interfaces, hooks, parameters) built for an imagined future with one real use case. - **Gold Plating** — features and polish added beyond what the ticket asked for. - **Yo-yo Problem** — behavior scattered across a deep inheritance chain; you bounce up and down to find it. - **Lasagna Code** — too many thin layers, each just forwarding the call. - **Accidental Complexity** — difficulty our solution adds, not the problem's own difficulty. - **Soft Coding** — business logic pushed into config/data until nobody can read the program. - **Bikeshedding** — disproportionate time spent on a trivial decision. The common thread: each is *doing more than the problem needs*. They're failures of restraint, not of effort.

Q2. What do YAGNI and KISS stand for, and how does each fight over-engineering?

Answer **YAGNI** — "You Aren't Gonna Need It": build for the requirement in front of you, not the one you imagine. It directly kills Speculative Generality and Gold Plating — both are bets on a future that usually never arrives. **KISS** — "Keep It Simple, Stupid": prefer the simplest solution that fully solves the real problem. It kills Accidental Complexity, Lasagna, and Soft Coding. The two are complementary: YAGNI is about *scope* (don't build what's not needed), KISS is about *form* (build what's needed in the plainest way). Neither means "do less than the problem requires" — that's under-engineering, the opposite failure.

Q3. Over-engineering feels like diligence. Why is it actually a problem?

Answer Because every line you write is a line someone must read, test, and maintain forever — and code added "just in case" is a *permanent* cost paid for a benefit that usually never arrives. Over-engineering wears the costume of craftsmanship: a plugin system, a clever optimization, an extra layer "for separation of concerns" all *look* like good work. But unused flexibility is debt, not an asset: it widens the surface area for bugs, slows comprehension, and gets in the way when the real requirement (which rarely matches the guess) finally shows up. The cost is real and immediate; the payoff is hypothetical and usually never collected.

Q4. What's the difference between essential and accidental complexity?

Answer The distinction is Fred Brooks' (*No Silver Bullet*). **Essential** complexity is inherent to the problem itself — correctly handling time zones, money rounding, or distributed consensus is genuinely hard, and no clever code removes that difficulty. **Accidental** complexity is what *our solution* adds on top: a five-layer abstraction to format a date, a framework imported to do a `map()`, a config DSL for a constant. Our job is to minimize the accidental kind while faithfully serving the essential. Over-engineering is, almost by definition, the manufacture of accidental complexity — every one of the other seven anti-patterns is a specific way to add it.

Q5. Your colleague rewrites a readable loop into a cryptic "optimized" version. What's the first question to ask?

Answer *"Where's the measurement showing the original was a bottleneck?"* Without a profile or benchmark, it's premature optimization — you've traded clarity (a permanent cost) for a speed-up that may be irrelevant, and you've likely guessed wrong about where the time goes (it's usually I/O, not arithmetic). Even if it *is* a proven hotspot, the follow-up is *"where's the benchmark showing the new version is actually faster, on representative data?"* Optimization without before/after numbers is just a hunch dressed up as performance work.

Q6. When is adding an interface not Speculative Generality?

Answer When it earns value *today*, not for a hypothetical future. Legitimate reasons for a single-implementation interface: it's a **test seam** (you inject a fake — a `Clock`, a `PaymentGateway` — to test callers in isolation); it's a **published API / module boundary** other code depends on; or there's a **confirmed, imminent second implementation** with an actual ticket. It's speculative only when the sole justification is "we might swap it later" with no current consumer. The discriminator is present-day value, not future possibility.

Q7. What is Soft Coding, and how is it related to Hard Coding?

Answer Soft Coding is the *over-correction* of Hard Coding. Hard Coding bakes values (and worse, business rules) into source where they don't belong. The naive fix — "make it configurable" — taken too far produces Soft Coding: *everything* becomes configurable, until business logic lives in JSON, a database, or a rules engine instead of code. You've built a worse programming language inside your config, with no type-checking, no tests, and no debugger. The right middle is to keep logic in code and configure only what genuinely varies by environment or deployment (URLs, timeouts, pool sizes).

Q8. What's the Yo-yo Problem and what's the junior-level fix?

Answer The Yo-yo Problem is when understanding a piece of behavior forces you to bounce up and down a deep inheritance hierarchy — the method is overridden three levels up, calls `super` two levels down, and no single class tells the whole story. It's painful to trace and fragile: a change in a base class silently alters every subclass (the fragile-base-class problem). The fix is **composition over inheritance** — give a class the behavior it needs as an injected collaborator instead of inheriting a tower of it — and keeping any inheritance shallow (one level is usually plenty).

Q9. A teammate proposes moving all discount rules into a database table "so the business team can edit them." What's the risk and what do you ask?

Answer The risk is **Soft Coding**: encoding business logic as data strips away type-checking, tests, version control, code review, and a debugger — and the rules table tends to grow operators (`if`, `>=`, `and`) until it's an ad-hoc programming language nobody can read. The questions to ask: *Do non-developers actually need to change these without a developer, and how often? What happens the first time a rule needs an `if/else` or a date range?* Frequently a tested constant or small config plus a quick code deploy is simpler and safer. If self-serve editing is a genuine, proven requirement, that's a deliberate, narrow, validated feature — not a reflexive "make it configurable."

Q10. What is Lasagna Code and how do you recognize it?

Answer Lasagna Code is the inverse of Spaghetti: too many thin **horizontal layers**, each adding almost nothing. The tell is a call chain like `Controller → Service → Manager → Handler → Helper → Repository → DAO → Mapper` where each class's method just calls the next and renames the arguments. To follow one operation you open eight files and find that seven of them do no real work. The fix is to collapse pass-through layers and keep a layer only when it owns a genuine, distinct responsibility — validation, DTO↔domain mapping, a transaction or auth boundary.

Q11. What is Bikeshedding and why does it happen?

Answer Bikeshedding (Parkinson's Law of Triviality) is spending disproportionate time on a *trivial* decision while the important, hard decisions get little attention — a PR thread with 47 comments about tabs-vs-spaces and zero comments about the concurrency bug. It happens because trivial problems are *accessible*: everyone has an opinion on naming or formatting, while few feel qualified to weigh in on the locking model. The fix is to remove the accessible problems via automation (formatters, linters, style guides) so they're never debated, and consciously redirect human attention to the decisions that carry real risk.

Q12. Is more abstraction always cleaner code?

Answer No — and believing so is the core junior misconception. Abstraction has a cost: every layer is something to read, name, learn, and maintain, and every indirection is a hop the reader must follow. A *good* abstraction pays for that cost by hiding real, repeated, essential complexity behind a simple interface. A *speculative* abstraction hides nothing real — it's pure overhead, a tax with no service. The honest measure isn't "how abstract is this?" but "does this abstraction hide more complexity than it adds?" Often the cleanest code is the most direct.

Intermediate / Middle

When it creeps in, the calibration, and the trade-offs.

Q13. Give an example of optimization that is not premature, and one that is. What distinguishes them?

Answer **Not premature:** choosing a hash map over a linear scan for a lookup you know will run on large input, or picking O(n log n) over O(n²) at design time given the expected data shape. These are *algorithmic / design-time* decisions made on known facts about the problem. **Premature:** hand-unrolling a loop, caching a value, or rewriting clear code into cryptic code *without a profile showing it's hot*. The distinction is **design-time complexity choice vs. micro-tuning correct code on a hunch**. Knuth's "premature optimization is the root of all evil" targets the second; it was never an argument against choosing the right algorithm up front.

Q14. Walk me through a disciplined performance workflow — how do you avoid premature optimization in practice?

Answer 1. **Establish it matters.** Is there a latency/throughput requirement this code is actually failing? If not, "slow" is hypothetical. 2. **Profile** to find where time genuinely goes. It's almost never where you guessed — usually I/O, serialization, N+1 queries, or lock contention, not your arithmetic. 3. **Benchmark the change** on representative data, comparing before and after with real numbers (e.g. `benchstat`). 4. **Keep the clear version if the gain is trivial.** A 2% win that triples the reading cost is a net loss. The benchmark is the *entry ticket* for the optimization: no number, no change. This converts "I think this is faster" into "I measured that this is faster," which is the difference between engineering and superstition.

Q15. What is the Rule of Three and how does it relate to Speculative Generality?

Answer The Rule of Three is a heuristic for *when* to abstract: one use case → write it concretely; two → tolerate a little duplication and watch the shape; three → now you understand the variation well enough to abstract correctly. It's the direct antidote to Speculative Generality, which abstracts at *one* use case based on an imagined future. Abstracting too early means designing against a guess; by the third real case the true shared shape has revealed itself, so the abstraction fits reality instead of fiction. The rule isn't sacred arithmetic — it's a reminder that good abstractions are *discovered* from concrete cases, not *predicted*.

Q16. You wrote an interface six months ago expecting a second implementation that never came, and the code is now awkward. What does Sandi Metz's advice tell you to do?

Answer *"Duplication is far cheaper than the wrong abstraction."* **Inline it back to duplication.** Re-introduce the concrete code at each call site, let the real cases re-emerge, and only abstract again once you can see the genuine shared shape. The trap is *sunk-cost loyalty* to a premature interface — bending each new requirement to fit an abstraction that was a guess. A wrong abstraction is more expensive than copy-paste because every future change must be threaded through a contract that doesn't match the problem. Backing out to duplication is a legitimate, often correct, refactoring move — not an admission of failure.

Q17. How do you decide whether a layer "earns its keep"?

Answer A layer is justified only if it adds a **distinct responsibility** — something the call would otherwise lack. Earns its keep: validates/sanitizes input, maps between representations (DTO ↔ domain), owns a transaction boundary, enforces auth or a security boundary. Doesn't earn it: renames arguments and forwards 1:1, exists "because the pattern has this layer," or wraps a single call "for symmetry." The test: if you deleted the layer and inlined it, would anything of value be lost? If not, it's a Lasagna layer — collapse it. The goal is *cohesive* layers with real jobs, not the maximum number of layers.

Q18. Ousterhout distinguishes "deep" from "shallow" modules. How does that frame the Lasagna problem?

Answer A **deep module** has a simple interface hiding a substantial implementation — high value per unit of interface, because it hides a lot of complexity behind a little surface. A **shallow module** adds interface without hiding much — its signature is nearly as complex as what it does. Lasagna Code is a *stack of shallow modules*: each layer's interface is as wide as the call it forwards, so it hides nothing and is pure overhead. The cure isn't "fewer layers" mechanically — it's preferring depth: each surviving module should hide real implementation behind a narrow interface. One deep module beats five shallow pass-throughs.

Q19. What single variable most helps you decide between "do the simple thing (YAGNI)" and "invest up front"?

Answer **Reversibility** — the cost to change the decision later. If a decision is cheap to change (most internal code), defer it and do the simple thing; a simpler design actually *preserves* more options because it's cheaper to modify. If a decision is expensive or irreversible — a public API, a persisted data schema, a wire format, a database choice — invest in getting it right up front, because retrofitting is brutal. There, "do the simple thing" can be *under*-engineering. This maps to Bezos' "Type 1 vs Type 2" (one-way vs two-way door) decisions: irreversible decisions deserve deliberation; reversible ones deserve speed.

Q20. Why is over-engineering harder to catch in code review than under-engineering?

Answer Because over-engineering **looks like competence**. More abstraction, more layers, more flexibility, more handled edge cases all *read* as thoroughness — a reviewer feels churlish saying "this is too much engineering." Under-engineering, by contrast, looks like a gap: a missing test, an unhandled error, a hard-coded value — concrete things easy to point at and request. So speculative interfaces and gratuitous layers sail through review while a missing null-check gets three comments. The countermove is to make reviewers ask the right question of every abstraction: *"what real, present need does this serve?"* — and treat "future flexibility" as a red flag, not a virtue.

Q21. How do you keep gold plating out of your own work?

Answer It's process, not willpower (gold plating creeps in through good intentions and through boredom — the assigned task is dull, the extra feature is fun). Concrete moves: **define "done" before starting** as acceptance criteria scoped to the ticket — if it's not in the criteria, it's not in this PR. **Capture ideas, don't build them** — a genuinely good idea becomes a backlog item the team can prioritize, not a surprise in your diff. **Keep PRs small** — a 40-line PR scoped to one criterion rarely hides gold plating; a 1,500-line one routinely does. And apply YAGNI to features, not just code: "users might want XML export" is the feature-level version of speculative generality.

Q22. Where exactly is the line between healthy configuration and Soft Coding?

Answer The question isn't "config or code?" but "**what kind of thing** is this?" | The thing | Where it goes | Why | |---|---|---| | Environment value (URL, pool size, timeout) | Config | Genuinely varies per deploy | | Simple tunable (page size, retry count) | Config with a sane default | Varies, but the *logic* stays in code | | A business rule (how a discount is computed) | **Code** | Needs tests, types, review, a debugger | | Frequently-changing rules edited by non-devs | Narrow, validated DSL/table — last resort | Only if the need is *proven* | The failure is pushing the third row into config. The rule of thumb: **if your config grows operators (`if`, `and`, `>=`), you've started writing a programming language in JSON — stop and move the logic back to code.**

Q23. How do you tell over-engineering apart from under-engineering when you're in the middle of a design?

Answer Run the decision through two questions. First: *does a real need exist today?* If yes, build it — that's not over-engineering. If no, second: *is the simple version cheap to change later?* If yes, do the simple thing (YAGNI). If no — the future need is both highly likely *and* expensive to retrofit — add a minimal seam, a justified bet. The honest framing: **over-engineering bets on a future you're guessing at; under-engineering ignores a future you can clearly see.** Most of the time you're in the "reversible, no present need" quadrant, where simple wins; the art is recognizing the rarer irreversible case and not under-building it.

Q24. Diagnose and simplify this Python code.

# Task: uppercase a list of names.
class Transformer:
    def __init__(self, steps): self.steps = steps
    def run(self, data):
        for s in self.steps: data = s.apply(data)
        return data
class UpperStep:
    def apply(self, data): return [x.upper() for x in data]

result = Transformer([UpperStep()]).run(names)
Answer **Accidental Complexity** (with a dose of Speculative Generality): a "pipeline framework" built to run a single `map()`. The essential problem is one line; the machinery — a step protocol, a transformer that loops over steps, a class per operation — hides that truth under ceremony, for a flexibility (arbitrary pluggable steps) that has exactly one step and no second use case.
result = [name.upper() for name in names]
The simpler form *is* the design. If a real multi-step pipeline with three+ varied stages ever appears, the abstraction can be derived from those real cases (Rule of Three) — and it'll fit better than this guess.

Q25. Diagnose this Java code and give the simpler form.

interface GreetingStrategy { String greet(String name); }
class DefaultGreetingStrategy implements GreetingStrategy {
    public String greet(String name) { return "Hello, " + name; }
}
class GreetingEngine {
    private final GreetingStrategy strategy;
    GreetingEngine(GreetingStrategy s) { this.strategy = s; }
    String run(String name) { return strategy.greet(name); }
}
// new GreetingEngine(new DefaultGreetingStrategy()).run("Sam");
Answer **Speculative Generality.** A Strategy interface, a concrete strategy, and an engine wrapper — three types and an injection — to produce one fixed greeting with one caller and no test seam needed. The flexibility (swap the greeting strategy) is real cost for a use case that doesn't exist.
String greet(String name) { return "Hello, " + name; }
One concrete function. If a second greeting style with a real requirement appears, *then* introduce the seam — designed against two real cases, not one imagined one. Note this is *not* the same as a justified seam: there's no second impl, no mock target, no published contract.

Senior — Architecture-Scale & Leading Teams

Over-engineering at system scale, and steering teams away from it.

Q26. What does over-engineering look like at architecture scale rather than code scale?

Answer The same instinct, larger blast radius: **premature microservices** (splitting a system that has neither the team size nor the scale to justify distributed-system tax); **frameworks built in-house** to abstract over a library that has one caller; **event-driven everything** where a function call would do; a **generic platform** built for the three internal teams that exist, designed to support thirty that don't; **CQRS / event sourcing** adopted for a CRUD app; a **Kubernetes cluster** for a service that fits on one box. Each imports enormous accidental complexity — network failures, eventual consistency, deployment orchestration, distributed tracing — to solve problems the system doesn't yet have. Architecture-scale over-engineering is the most expensive kind because it's the hardest to reverse.

Q27. When is premature microservices a real anti-pattern, and when is splitting justified?

Answer It's an anti-pattern when you adopt microservices for *imagined* scale or *aesthetic* decoupling while paying the full distributed-systems tax now: network partitions, partial failure, distributed transactions, eventual consistency, harder local dev, deployment orchestration, and cross-service debugging. A monolith with clean module boundaries gives you most of the decoupling with none of that tax — and is far easier to split *later* when a real seam emerges. Splitting is justified when there's a **present, concrete force**: a component with genuinely different scaling needs, an independent deployment cadence demanded by team structure (Conway's Law), a hard isolation/security boundary, or a fault-isolation requirement. The rule mirrors the small-scale one: split on a *real, present* need, not a forecast — and prefer the reversible path (modular monolith) until the irreversible one is forced.

Q28. A senior engineer on your team is over-engineering — building a plugin system for a feature with one variant. How do you push back?

Answer Push back on the *reasoning*, not the person, and lead with cost and reversibility. Concretely: (1) **Ask the disarming question** — "what real, present need does the plugin system serve? Is there a second variant with a ticket?" Make them articulate the requirement; often there isn't one. (2) **Name the cost explicitly** — "this adds a registration mechanism, a lifecycle, and an SPI we'll all maintain forever, for a future we're guessing at." (3) **Offer the reversible alternative** — "the concrete version is cheap to generalize *when* the second variant lands; the plugin system is expensive to remove if it doesn't. Let's keep options open." (4) **Defer to data / a decision record** — write down the assumption ("we expect N plugins by Q3") so it can be checked, not just asserted. The senior signal is framing it as *risk management* (we're betting on an unknown future) rather than a taste argument.

Q29. How do you build a team culture that resists over-engineering without tipping into under-engineering?

Answer Make *restraint* the default and *investment* a deliberate, justified exception. Practical levers: **automate trivia** (formatters, linters, a style guide) so bikeshedding has nothing to feed on; **require a present-day justification** for every abstraction in review ("what does this hide that's real?"); **use lightweight ADRs** for the genuinely irreversible decisions (schema, API, data store) so those *do* get up-front thought; **prioritize refactoring by change-frequency × pain**, not by aesthetics; and **reward deletion** — celebrate the PR that removes a speculative abstraction as much as one that adds a feature. The guardrail against over-correcting into under-engineering is the reversibility test: simple-by-default everywhere, but invest hard on the one-way doors. Culture is set by what reviewers praise and what they let slide.

Q30. How do you prioritize removing over-engineering in a codebase that's drowning in it?

Answer Same discipline as any refactoring: **change-frequency × pain**, not disgust. Pull commit history for high-churn files — the speculative abstractions and gratuitous layers there are taxed on every edit, so removing them pays back fastest. A gold-plated module nobody touches has near-zero change-cost; leave it. Within hot spots, target what *blocks the upcoming roadmap*. Remove safely and incrementally: pin behavior with characterization tests, inline one speculative seam at a time, keep structural commits separate from behavioral ones, ship small. And resist the urge to launch a "great simplification project" — that's its own over-engineering. Simplify opportunistically, in the code you're already touching.

Q31. What organizational forces breed over-engineering? It's not just individual ego.

Answer Several systemic forces: **resume-driven development** (engineers adopt fashionable tech — microservices, Kafka, k8s — for career capital, not the problem); **fear of being caught under-prepared** (over-build "to be safe" after a past rewrite burned someone); **no clear owner of simplicity** (everyone can *add*, nobody is accountable for *restraint*); **promotion incentives that reward visible complexity** ("led the platform rewrite" reads better than "deleted 4k lines"); **cargo-culted best practices** (adding a service layer because "real apps have one"); and **idle senior capacity** (a strong engineer with no hard problem invents one). Conway's Law also bites in reverse — speculative service boundaries calcify into team boundaries. Durable fixes address incentives and ownership, not just the code.

Q32. Can over-correcting an under-engineered (Spaghetti / God Object) codebase create over-engineering? Which patterns?

Answer Yes — this is the classic pendulum swing. Flatten a 2,000-line God Object into 30 one-line pass-through classes and you've created **Lasagna Code** (following one call now spans eight files). Burned by a brittle inheritance hierarchy, an engineer may "fix" it by adding *more* indirection — strategies, factories, abstract bases — re-creating the **Yo-yo Problem** in delegation form. Over-react to a hard-coded value and you get **Soft Coding**. The goal was never "more structure"; it's **cohesion** — each unit owns one real responsibility. Both Spaghetti (too little structure) and Lasagna (too much) miss the target; the cure for one must not overshoot into the other.

Q33. "Design it twice" — how does that prevent over-engineering rather than cause more design work?

Answer Ousterhout's "design it twice" means sketching two genuinely different approaches before committing — and it *prevents* over-engineering because the comparison surfaces which complexity is essential and which is accidental. When you only have one design, you can't tell whether its complexity is inherent to the problem or an artifact of your first idea; you tend to defend and elaborate it. A second, simpler alternative reveals that half the machinery in the first was avoidable. It's cheap (it's sketching, not building) and it front-loads judgment onto the *reversible* design phase rather than the expensive implementation phase. It's the opposite of analysis paralysis: a quick, deliberate comparison, then commit.

Q34. When is up-front design not over-engineering?

Answer When the decision is **irreversible or expensive to change** — a Type-1 / one-way-door decision. Database choice, persisted schema, public API shape, wire/message formats, security and tenancy boundaries, and data-migration strategy all fall here: getting them wrong costs months and breaks consumers. For these, "do the simplest thing and change it later" is *under*-engineering, because "later" is precisely what's prohibitively expensive. YAGNI targets *speculative flexibility in reversible code*; it was never an argument against thinking hard about the decisions you genuinely can't walk back. The mature stance is asymmetric: deliberate on one-way doors, move fast through two-way doors.

Q35. Is "we'll need to scale to millions of users" a valid reason to build a distributed system now?

Answer Usually not, on its own — it's the architecture-scale version of "we might need it someday." Two honest questions: *is that scale a present, funded requirement or a founder's aspiration?* and *can the simple system be scaled when the load actually arrives?* For the vast majority of products, a well-structured monolith on a single beefy box (with read replicas and a cache) serves far more traffic than people assume, and the company hits other walls first. Building for millions while serving thousands means paying the full distributed tax — operational complexity, cost, slower iteration — for years of phantom load, often dying of slowness-to-ship before scale ever becomes the problem. The exception is when the scale is *known and imminent* and the architecture is genuinely irreversible — then it's justified investment, not speculation.

Q36. How do you handle a junior who under-engineers because they took YAGNI too literally?

Answer Recalibrate them on the *reversibility* axis — YAGNI is not "never abstract" or "never think ahead." Show concretely where up-front thought is mandatory: "this is the persisted schema — getting it wrong means a painful migration, so we *do* design it carefully now." Teach the Rule of Three so they know abstraction at the third repetition is *correct*, not a YAGNI violation. The lesson: YAGNI applies to *speculative flexibility in cheap-to-change code*, not to *thoughtful design of expensive-to-change decisions*. A junior who refuses to abstract at the fifth copy-paste, or who skips schema design "to keep it simple," has swung past restraint into negligence. Both edges of the pendulum are failures.

Professional / Deep — The Performance Irony

Where abstraction has real runtime cost, and where the compiler already wins.

Q37. The performance irony: can clean, well-layered code actually be slower than messy code?

Answer Yes — and a strong candidate names *why*. Each abstraction layer can cost: **dispatch overhead** (virtual/interface calls are indirect and can defeat inlining and devirtualization); **allocation pressure** (splitting one object into many small collaborators means more heap objects, more GC work); **cache locality loss** (related data scattered across objects/layers means more cache-line misses, the dominant cost on modern CPUs); and **call-stack depth** (Lasagna's eight hops are eight stack frames per operation). A flat, "messy" loop over a contiguous array can crush a beautifully layered, allocation-heavy, pointer-chasing equivalent — not because clean is bad, but because abstraction isn't free. The nuance: for the overwhelming majority of code this overhead is irrelevant, so you keep the clean version. You only trade clarity for flatness on *proven* hot paths.

Q38. "I should hand-optimize this loop because the compiler won't." When is that wrong?

Answer Almost always wrong for the micro-optimizations people reach for first. Modern optimizing compilers and JITs already do loop unrolling, strength reduction, constant folding, common-subexpression elimination, inlining, autovectorization (SIMD), and bounds-check elimination — frequently *better* than hand-rolled code, and they keep the source readable. Hand-unrolling a loop often *defeats* the optimizer (it pattern-matches idiomatic code; your "clever" version it can't recognize) and pins you to assumptions that break on the next CPU. The compiler does *not* fix algorithmic complexity (O(n²) stays O(n²)), data-structure choice, I/O patterns, or cache layout — that's where human effort belongs. So: let the compiler do the micro work; spend your effort on algorithm, data structures, and memory access patterns, guided by a profiler.

Q39. What is the concrete runtime cost of Soft Coding — a rules engine instead of code?

Answer Soft Coding moves logic from compiled, optimized code into *interpreted data*, and you pay for it at runtime. A JSON/DB rules engine typically: **interprets** the rule tree on every evaluation (walking a data structure and dispatching on `op` strings) instead of running straight-line compiled code; **allocates** boxed values and intermediate objects the compiler would have kept in registers; **defeats the JIT/inliner**, which can't optimize across the generic `evaluate(rule, context)` dispatch the way it would inline a concrete method; and **loses branch prediction** because the "branches" are data-driven, not in the instruction stream. A discount computed by walking a config tree can be orders of magnitude slower than the equivalent `if` in code — and that's *on top* of losing types, tests, and the debugger. The runtime cost is usually the *least* of Soft Coding's problems, but it's real and worth naming.

Q40. How does Lasagna Code interact with the JIT and inlining?

Answer Thin pass-through layers are individually trivial, so a good JIT (HotSpot, V8) will often inline them away on hot paths — the eight-hop chain can collapse into roughly the cost of the one real call once it's warmed up. So at steady state the *dispatch* cost of Lasagna can approach zero. But there are real edges: inlining has budgets (HotSpot's `MaxInlineSize` / `FreqInlineSize`, the megamorphic call-site limit), so deep or polymorphic chains can blow the budget and stay un-inlined; **megamorphic** interface call sites (many implementations seen) can't be devirtualized and cost an indirect dispatch each time; and code *not yet hot* runs interpreted, so layer count costs in startup/cold paths and short-lived processes (serverless, CLIs). AOT-compiled or non-JIT runtimes (and Go, which inlines conservatively) get less of this rescue. The honest answer: the JIT usually saves you on hot paths, but "the optimizer will inline it" is not a license for gratuitous layers — comprehension cost remains regardless.

Q41. Does Speculative Generality have a runtime cost, or is it purely maintainability?

Answer Mostly maintainability, but there are real runtime edges worth naming. A speculative interface forces **indirect dispatch** where a concrete call would be a direct, inlinable one — and an interface with a single implementation that the compiler *can't prove* is the only one (e.g. it's public, loaded via DI, or reachable by reflection) won't be devirtualized, so you pay the indirection forever. Generic, parameterized "engines" tend to **box values, allocate strategy objects, and chase pointers** where concrete code would stay flat and on-stack. In languages with type erasure (Java generics) you also pay boxing/casting. None of this matters off the hot path — but on it, "we made it pluggable just in case" can show up in a flame graph as dispatch and allocation that buys nothing.

Q42. A teammate says "deep abstraction is fine, the compiler optimizes it away." How do you respond?

Answer Agree on the narrow truth, then reframe. Yes — on hot, monomorphic paths a good JIT inlines and devirtualizes, so the *dispatch* cost of abstraction often does vanish. But three caveats: (1) the optimizer's help is *conditional* — it evaporates with megamorphic call sites, inlining-budget overflows, cold/startup paths, reflection, and non-JIT runtimes; (2) the optimizer flattens *dispatch*, not *allocation and cache layout* — scattered small objects still miss cache and pressure GC; and (3) **the cost of abstraction was never primarily CPU** — it's human comprehension and change-cost. Even where the compiler makes it free at runtime, the reader still pays to trace eight layers. So "the compiler optimizes it" answers a question nobody important was asking. We add abstraction to *hide real complexity*, not because it's cheap.

Q43. How can you measure whether an abstraction is costing you, rather than guessing?

Answer Treat performance as a behavior to verify, not assume. **Profile** the hot path (a sampling profiler / flame graph) and look for time in dispatch glue, allocation, and GC rather than real work — Lasagna and speculative generality show up as wide, shallow frames doing nothing but forwarding. **Benchmark** the abstracted vs. a flattened version on representative data with a proper harness (`go test -bench`, JMH, `pytest-benchmark`) and compare with statistics, not single runs. **Inspect allocations** (`-benchmem`, allocation profilers) — extra small objects per operation is the smoking gun. **Read the compiler's output** when it matters: Go's `-gcflags=-m` shows inlining and escape-analysis decisions; you can confirm whether your layer was inlined or whether a value escaped to the heap. The principle mirrors premature optimization: don't *assume* the abstraction is cheap *or* expensive — measure, then decide whether to flatten.

Q44. Why is "the compiler will tree-shake / dead-code-eliminate the unused generality" only sometimes true?

Answer Because dead-code elimination and tree-shaking are *conservative*: they only remove what they can *prove* unreachable, and the very mechanisms speculative generality uses defeat that proof. Reflection, dependency-injection containers, plugin registries, dynamic dispatch, exported public symbols, and `init`-style side effects all force the tool to assume the code might be called. So a speculative `ExportXML` reachable via a DI-registered interface, or a plugin SPI loaded by name, *ships in the binary* and *runs through the dispatch path* even with zero real callers. The implication: you can't outsource the deletion of speculative generality to the optimizer — it bloats the bundle/binary, enlarges attack surface and the dependency graph (CVE patching, license compliance), and slows tooling, none of which the compiler reliably reclaims. Human deletion, backed by git, is what actually removes the cost.

Q45. When should you deliberately keep flatter, "less clean" code for performance?

Answer On a *measured* hot path where the abstraction's overhead is shown to matter — a tight numeric kernel, an inner loop running billions of times, a latency-critical request path the profiler flags. There, you might justifiably: keep data in a flat struct-of-arrays for cache locality instead of an array of richly-modeled objects; inline a hot call instead of routing it through layers; avoid interface dispatch in the loop body; pre-allocate to dodge per-iteration GC. The discipline that keeps this from becoming premature optimization: it's *evidence-led* (profiler said so), *localized* (you flatten the one hot path, not the whole codebase), *benchmarked* (numbers prove the win), and *documented* (a comment explains why this spot trades clarity for speed). Everywhere else, the clean version wins because the overhead is irrelevant.

Code-Reading — Diagnose the Snippet

You're shown a snippet; identify the anti-pattern(s) and state the simpler form.

Q46. Which anti-pattern, and what's the simpler form?

func sumEven(nums []int) int {
    s, i, n := 0, 0, len(nums)
    for ; i < n-3; i += 4 {
        if nums[i]&1 == 0   { s += nums[i] }
        if nums[i+1]&1 == 0 { s += nums[i+1] }
        if nums[i+2]&1 == 0 { s += nums[i+2] }
        if nums[i+3]&1 == 0 { s += nums[i+3] }
    }
    for ; i < n; i++ { if nums[i]&1 == 0 { s += nums[i] } }
    return s
}
Answer **Premature Optimization.** Manual loop unrolling and bit-twiddling (`&1` instead of `%2`) in code with no profile showing it's hot — clarity traded for a speed-up that's probably irrelevant and that the compiler would do anyway (Go's compiler can unroll and the idiomatic version is autovectorizable).
func sumEven(nums []int) int {
    s := 0
    for _, v := range nums {
        if v%2 == 0 { s += v }
    }
    return s
}
Write this; if a profiler ever flags it, optimize *then*, with a benchmark proving the gain.

Q47. Which anti-pattern, and what's the simpler form?

def save_report(report, path, *, fmt="txt", compress=False, encrypt=False,
                email_to=None, watermark=None, theme="dark", retries=3):
    ...  # 200 lines
# Ticket said: "save the report to a file."
Answer **Gold Plating.** The ticket asked to save a file; the implementation grew encryption, email delivery, watermarks, themes, compression, and retries — none requested. Every option is permanent surface to test, document, and maintain, and it delayed a simple feature.
def save_report(report, path):
    path.write_text(report.render())
Build exactly the ticket. If `compress` or `email_to` is genuinely valuable, raise each as its own backlog item for the team to prioritize — don't smuggle them in.

Q48. Which anti-pattern, and what's the simpler form?

class OrderService { Order get(int id){ return manager.get(id); } }
class OrderManager { Order get(int id){ return handler.get(id); } }
class OrderHandler { Order get(int id){ return repo.get(id); } }
// repo.get is the only line that does work.
Answer **Lasagna Code.** Three layers, none adding a responsibility — each forwards `get(id)` 1:1 and renames nothing. To reach the one real query you open three files. Collapse the pass-throughs; keep only layers with a distinct job (validation, mapping, a transaction boundary):
class OrderService {
    private final OrderRepository repo;
    Order get(int id) { return repo.get(id); } // talk to the repo directly
}
If, say, validation or DTO mapping is genuinely needed, *that* layer earns its place — but a layer that only forwards does not.

Q49. Which anti-pattern, and what's the simpler form?

{
  "rules": [
    {"if": {"field": "age", "op": ">=", "val": 18}, "then": {"set": "adult", "to": true}},
    {"if": {"field": "country", "in": ["US","CA"]}, "then": {"apply": "rule_47"}}
  ]
}
Answer **Soft Coding.** Business logic — conditionals, operators, even a `rule_47` cross-reference — encoded as JSON data. There's no type-checking, no test, no debugger, no readable control flow; it's a worse programming language living in a config file. Move the logic back to code where it can be tested and read:
def classify(user):
    user.adult = user.age >= 18
    if user.country in ("US", "CA"):
        apply_rule_47(user)
Configure only what genuinely varies per deploy. If non-devs *must* edit rules, that's a deliberate, narrow, validated feature — not a reflexive config tree.

Q50. Which anti-pattern, and what's the simpler form?

class Animal { void move(){ step(); } protected void step(){/*...*/} }
class Mammal extends Animal { protected void step(){ super.step(); /*...*/ } }
class Dog extends Mammal    { protected void step(){ /*...*/ super.step(); } }
class Puppy extends Dog     { protected void step(){ /*...*/ } }
// "What does puppy.move() do?" → climb four files.
Answer **Yo-yo Problem.** `step()` is fragmented across four levels, half-overridden and chained through `super`, so answering "what does `puppy.move()` do?" means bouncing up and down the hierarchy. It's also fragile — a change in `Animal.step()` silently alters every descendant. Replace inheritance with composition so behavior lives in one place:
class Dog {
    private final Gait gait;          // behavior is a collaborator, not an ancestor
    Dog(Gait gait) { this.gait = gait; }
    void move() { gait.step(); }
}
Now `Dog`'s behavior is readable from `Dog` plus the `Gait` it's given — no climbing.

Q51. This snippet shows two anti-patterns at once — name both and simplify.

class ConfigurableSummer:
    def __init__(self, ops=None):
        self.ops = ops or {"add": lambda a, b: a + b}
    def reduce(self, items, op="add"):
        acc = items[0]
        for x in items[1:]:
            acc = self.ops[op](acc, x)  # micro-tuned: avoids re-lookup? (it doesn't)
        return acc

total = ConfigurableSummer().reduce(numbers)  # we only ever sum
Answer **Speculative Generality** (a configurable op-registry for a sum that's never anything but a sum) *plus* **Accidental Complexity** (a class, a dict of lambdas, and a manual reduce to express `sum`). There's also a comment hinting at a non-existent micro-optimization. The essential problem is one builtin:
total = sum(numbers)
The pluggable ops, the registry, the hand-rolled fold — all accidental, all serving an imagined future where someone passes `"multiply"`. They don't, so delete them; derive an abstraction from real cases if they ever appear.

Q52. Which anti-pattern does this code review thread illustrate?

PR #482 — "Add rate limiting to the payment webhook"
  47 comments: `limit` vs `maxRequests` vs `rateLimit` naming;
               tabs vs spaces in the new file; whether the comment
               needs a trailing period.
  The actual question — is the limiter correct under concurrent
  requests sharing one bucket? — 0 comments.
Answer **Bikeshedding** (Parkinson's Law of Triviality). The review poured 47 comments into naming, formatting, and punctuation — *accessible* problems everyone can opine on — while the one decision with real risk (a concurrency correctness bug in shared-bucket rate limiting) got zero attention. Fix the *cause*: a formatter and linter should make tabs/spaces and trailing-period debates impossible (nothing left to argue), a naming convention pre-decides the rest, and reviewer attention gets consciously steered to the locking model. Match scrutiny to blast radius: a naming nit on a payment webhook is not where the risk lives.

Curveballs

The questions designed to catch glib answers.

Q53. Isn't more abstraction always cleaner?

Answer No. Abstraction is not free — it's a *trade*: you hide some complexity behind an interface, but you add a layer to read, name, learn, and maintain, plus an indirection to follow. The trade is good only when the hidden complexity is *real, substantial, and essential* (a deep module); it's a loss when the abstraction hides little (a shallow module / Lasagna layer) or hides *nothing real* (Speculative Generality). The right metric is not "how abstract?" but "does this hide more complexity than it adds?" Frequently the cleanest code is the most direct code. "More abstraction = cleaner" is the belief that produces the entire over-engineering family.

Q54. When is up-front design NOT over-engineering?

Answer When the decision is **irreversible or expensive to retrofit** — a Type-1, one-way-door decision. Persisted data schemas, public/published API shapes, wire and message formats, the choice of datastore, security and multi-tenancy boundaries: get these wrong and the fix is a migration that breaks consumers and burns months. For these, designing carefully up front is good engineering, and "do the simple thing and change it later" is *under*-engineering. YAGNI targets *speculative flexibility in reversible code*, never *thoughtful design of expensive-to-change decisions*. The skill is honestly classifying which door you're standing in front of.

Q55. When is optimization NOT premature?

Answer Three legitimate cases. (1) **Algorithmic / design-time choices** — picking O(n log n) over O(n²), a hash map over a scan, an index over a full table scan — made on known facts about data size and access patterns, *before* writing code. (2) **Measured hotspots** — the profiler points at a specific function eating real time on a path that has an actual latency/throughput requirement; you optimize *that*, with a before/after benchmark. (3) **Known-constrained environments** — embedded, real-time, or per-request-budget systems where the constraint is a stated requirement, not a guess. The common factor is *evidence*: a known data shape, a profile, or a hard requirement. Premature optimization is specifically *micro-tuning correct, clear code on a hunch* — Knuth's quote was never an argument against thinking.

Q56. Is duplication ever better than abstraction?

Answer Yes — *"duplication is far cheaper than the wrong abstraction"* (Sandi Metz). Two pieces of code that *look* similar today but are driven by different reasons to change are **false duplication**; abstracting them couples things that should evolve independently, and every future change must be forced through a contract that doesn't fit. The cost of a wrong abstraction compounds: you bend requirements to the abstraction, add flags to it, and eventually it serves no case well. When you don't yet understand the variation (fewer than ~three real cases), a little duplication keeps the cases free to diverge and reveals the true shared shape. The senior move is sometimes to *inline an abstraction back to duplication* and re-derive it from reality.

Q57. Can clean, layered code be slower than messy code?

Answer Yes. Layers and rich object models cost indirect **dispatch** (defeating inlining/devirtualization), **allocation** (many small objects → GC pressure), **cache misses** (data scattered across objects instead of contiguous), and **stack depth** (per-call overhead through the layers). A flat loop over a contiguous array routinely beats an elegant, allocation-heavy, pointer-chasing equivalent. The crucial nuance for a strong answer: this matters only on *proven hot paths* — for the vast majority of code the overhead is noise, so you keep the clean version; you flatten deliberately, locally, and with benchmarks where the profiler shows it pays. "Clean is slower" is true in the small and irrelevant in the large; knowing which regime you're in is the skill.

Q58. How do you push back on a senior who's over-engineering?

Answer Attack the reasoning, not the person, and frame it as risk management. Make them state the *present* requirement ("is there a second use case with a ticket?") — often there isn't. Name the concrete, ongoing cost ("this SPI is something we all maintain forever"). Offer the reversible alternative ("the concrete version is cheap to generalize *when* the need lands; the abstraction is expensive to remove if it doesn't — let's keep options open"). Anchor on reversibility and on data: write the assumption into a decision record so it's testable, not a clash of taste. If they hold rank, you can disagree-and-commit on a *reversible* decision; escalate only the irreversible ones. The goal is a culture where "what real need does this serve today?" is a normal, non-confrontational question.

Q59. "We might need to support multiple databases someday, so let's abstract the data layer now." Good idea?

Answer Usually not — "someday" with no present requirement is textbook Speculative Generality. The cost is paid now (a generic data-access abstraction that hides the features that make each database worth using — its query language, indexes, transactions, JSON ops) for a benefit that rarely arrives, and that, when it does, almost never matches the guess. Most products never switch databases; those that do find the leaky abstraction needs rework anyway. The reversibility angle: a clean repository boundary for *testing* (a real, present need) is fine — but a *multi-vendor* abstraction is a bet you can defer cheaply by keeping data access localized behind a focused module. Build for one database well; if a second becomes a real, ticketed requirement, introduce the seam against two real implementations. The honorable exception is shipping software you *know* must run on customers' choice of DB on day one — then it's a present requirement, not speculation.

Q60. If over-engineering is bad and under-engineering is bad, isn't "right-sized engineering" just a platitude? How is it actionable?

Answer It's actionable through a single discriminating question applied per decision: *does a real need exist today, and how expensive is this to change later?* That collapses into a 2×2 you can actually use — (need now) → build it; (no need, cheap to change) → simple thing, YAGNI; (no need, expensive to change, likely) → minimal seam; (no need, expensive, unlikely) → still simple, accept the risk. The "platitude" becomes concrete once you stop treating engineering effort as a single dial ("more vs. less") and start treating it as *targeted* — restraint by default, investment aimed precisely at the irreversible, high-likelihood decisions. The skill is the honest assessment of those two variables, which is exactly what separates strong engineers from both the gold-platers and the corner-cutters.

Q61. A framework you're using forces ceremony (boilerplate, layers) you'd otherwise avoid. Is that accidental complexity, and what do you do?

Answer Yes — framework-imposed ceremony is accidental complexity *you imported*. The first question is whether the framework earns its keep: does it solve enough essential complexity (routing, ORM, DI, security) to justify the boilerplate it demands? Often it does, and the ceremony is the rent you pay for real value — that's a reasonable trade. The failure mode is **cargo-culting the framework's full layering onto a problem that doesn't need it** (a service + repository + DTO stack for a three-endpoint CRUD app), or **pulling in a heavy framework to solve a one-function problem**, importing its accidental complexity wholesale. The move: use the minimum of the framework that the problem requires, don't add layers it merely *permits*, and for genuinely small problems prefer a library or plain code over a framework. Match the tool's weight to the problem's weight.

Rapid-Fire / One-Liners

Crisp answers; what an interviewer wants in one or two sentences.

Q62. One-line cure for each of the eight?

Answer Premature Optimization → profile first, optimize the proven hotspot with a benchmark. Speculative Generality → concrete now, generalize on the (Rule-of-Three) real cases. Gold Plating → scope to the ticket; propose extras as backlog. Yo-yo → composition over inheritance, shallow hierarchies. Lasagna → collapse layers that add no responsibility; prefer deep modules. Accidental Complexity → KISS; solve the problem directly, prefer boring. Soft Coding → logic in code, config only for what varies. Bikeshedding → automate trivia, spend attention on real risk.

Q63. The two principles that fight the whole family, in one line each?

Answer **YAGNI** — don't build for an imagined future (kills speculative scope). **KISS** — build today's real problem in the plainest form (kills accidental complexity).

Q64. Knuth's full quote, and the half everyone forgets?

Answer "Premature optimization is the root of all evil" — but he prefaced it with *"we should forget about small efficiencies, say about 97% of the time"*, implying the other 3% (the measured hotspots) **do** deserve optimization. It's a call to measure, not to never optimize.

Q65. The deciding variable between "do the simple thing" and "invest up front"?

Answer Reversibility. Cheap to change later → simple now (YAGNI). Expensive/irreversible → invest now (schema, public API, wire format).

Q66. Sandi Metz's rule on duplication in one line?

Answer "Duplication is far cheaper than the wrong abstraction" — when the variation isn't understood yet, prefer copy-paste over a premature, mis-fitting interface.

Q67. Essential vs accidental complexity in one sentence?

Answer Essential is the problem's own difficulty (unavoidable); accidental is what our solution adds (removable) — and over-engineering is the manufacture of the accidental.

Q68. Why is "the compiler will optimize it" a weak defense of deep abstraction?

Answer The optimizer's help is conditional (it evaporates with megamorphic dispatch, cold paths, reflection, inlining budgets) and it flattens *dispatch*, not *allocation, cache layout, or human comprehension cost* — which is the real cost of abstraction.

Q69. The phrase that signals Soft Coding?

Answer "Let's make it configurable so non-devs can change it." Often nobody does, and the config grows into an unreadable, untestable programming language.

Q70. The phrase that signals Speculative Generality?

Answer "We might need to swap this out / support X later." Someday with no present second case and no test seam is a guess, not a requirement.

How to Talk About Over-Engineering in Interviews

A few habits separate a strong answer from a textbook recital:

  • Lead with the cost, not the label. Don't just say "that's speculative generality." Say why it hurts — "it's a permanent maintenance and comprehension cost paid now for a use case that doesn't exist and probably won't match our guess." Interviewers want the reasoning, not the vocabulary.
  • Anchor on reversibility. The single most senior-sounding idea in this topic: do the simple thing for cheap-to-change decisions; invest up front only on the irreversible ones (schema, API, wire format). It dissolves most "isn't this over/under-engineered?" questions cleanly.
  • Always insist on measurement for performance. "Premature optimization" answers should reflexively ask for a profile and a benchmark — and you should distinguish algorithmic/design-time choices (fair game) from micro-tuning correct code (premature).
  • Name the trade-off and the other side. Senior signal is acknowledging that abstraction can be right (test seams, deep modules, irreversible decisions) and that duplication can be better than the wrong abstraction. "It depends, and here's on what" beats absolutism.
  • Go deep when invited. The performance irony — dispatch, allocation, cache locality, conditional JIT inlining, the fact that the compiler already does the micro-optimizations — shows depth beyond the maintainability story. So does naming Soft Coding's interpreter-vs-compiled runtime cost.
  • Avoid the juniorisms. "More abstraction is always cleaner," "always make it configurable," "the compiler optimizes it away," "we might need it later" — each is a trap the curveballs are built to catch. Calibrate instead of absolutize.
  • Use a concrete example. "We almost built a plugin system for one variant; we kept it concrete, and the second variant that finally landed looked nothing like our guess" lands far harder than a definition.

Summary

  • The eight over-engineering anti-patterns share one root — doing more than the problem needs: Premature Optimization (tuning before measuring), Speculative Generality (abstraction for an imagined future), Gold Plating (features beyond the ask), Yo-yo Problem (behavior lost in an inheritance chain), Lasagna Code (empty pass-through layers), Accidental Complexity (self-inflicted difficulty — the umbrella for the rest), Soft Coding (logic hidden in config/data), and Bikeshedding (effort spent on trivia).
  • Recognition plus YAGNI/KISS is the junior bar; the middle bar is calibration — the Rule of Three, justified seams vs. speculation, layers that earn their keep, and reversibility as the deciding variable; the senior bar is architecture-scale judgment (premature microservices, in-house frameworks, generic platforms) and leading teams away from it; the professional bar is the performance irony — abstraction costs dispatch, allocation, and cache locality, the compiler already does the micro-optimizations, and Soft Coding pays an interpreter tax at runtime.
  • The strongest answers lead with cost and reasoning, anchor on reversibility, insist on measurement for performance claims, and name the trade-offs — including when abstraction, up-front design, and even duplication are the right call.
  • The recurring curveball insight: judge by present-day need and reversibility, not by how clever, future-proof, or "thorough" the code looks — because over-engineering is the anti-pattern that disguises itself as craftsmanship.

  • junior.md — recognize each anti-pattern; YAGNI and KISS.
  • middle.md — when each creeps in, the Rule of Three, justified seams, and reversibility.
  • senior.md — architecture-scale over-engineering and leading teams away from it.
  • professional.md — the performance and runtime cost of abstraction in depth.
  • tasks.md · find-bug.md · optimize.md — practice diagnosing and simplifying.
  • Bad Structure — Lasagna is the inverse of Spaghetti; Speculative Generality neighbors the Boat Anchor.
  • Bad Shortcuts — Soft Coding is the over-correction of Hard Coding; the Rule of Three and the wrong-abstraction trap.
  • Clean Code → Classes — composition over inheritance, cohesion, deep modules.
  • Refactoring → Techniques — Remove Speculative Generality, Collapse Hierarchy, Inline Class, Replace Subclass with Delegate.
  • Design Patterns — patterns are tools, not goals; applying them speculatively is over-engineering.