Core Design Principles
The language-neutral heuristics every reviewer reaches for: DRY, KISS and YAGNI, separation of concerns, cohesion and coupling, composition, and encapsulation. They are the shared vocabulary for arguing about design without appealing to taste.
part ofSoftware design & architectureoverview, primer and where to startread it →on this pageshowhide
explore
- DRY Principle5 questions
- KISS and YAGNI6 questions
- Separation of Concerns5 questions
- Cohesion and Coupling6 questions
- Composition Over Inheritance6 questions
- Encapsulation and Information Hiding5 questions
- Law of Demeter6 questions
- Tell, Don't Ask6 questions
- Command-Query Separation6 questions
- Abstraction and Indirection6 questions
- Principle of Least Astonishment5 questions
- Fail Fast6 questions
- Single Source of Truth5 questions
questions
73 · 13 sectionsWhat does the DRY principle ("Don't Repeat Yourself") actually require, and why is it stated in terms of knowledge rather than lines of code?
basics
~20 sDRY says every piece of knowledge — a rule, a formula, a fact about the system — should live in exactly one place. So when that rule changes, you edit one spot instead of hunting for copies you might miss.
How do you tell real (knowledge) duplication from coincidental (incidental) duplication, and what happens when you get that judgement wrong in each direction?
basics
~20 sAsk whether the copies must always change together for the same reason. If yes, it's real duplication — unify it. If they could evolve separately, it's coincidence — leave them. Merging coincidence creates a shared thing that soon needs flags.
Explain the "rule of three" and AHA ("Avoid Hasty Abstractions"), and describe how you decide the moment at which extracting an abstraction pays off.
basics
~20 sThe rule of three says wait until you see the same thing a third time before extracting it — two copies aren't enough to reveal what really varies. AHA adds: prefer some duplication over an abstraction you invented too early.
How does applying DRY across service, module, or team boundaries differ from applying it inside a single codebase — and when is deliberate duplication the better architectural choice?
basics
~20 sInside one codebase, sharing costs you a function call. Across services it costs coordination: a shared library of business rules means teams must upgrade and release together. So teams often duplicate small logic on purpose to stay independently deployable.
Beyond application code, where else does knowledge get duplicated in a system, and what techniques give you a single authoritative source for it?
basics
~20 sThe same rule often lives in the database schema, the API contract, client code, config files, infrastructure scripts, and the docs. You single-source it by generating the copies from one definition, or by adding automated checks that fail when they disagree.
What do the design principles KISS ("Keep It Simple, Stupid") and YAGNI ("You Aren't Gonna Need It") each tell a developer to do, and how do the two differ?
basics
~20 sKISS: choose the simplest solution that works — fewer moving parts, easier to read. YAGNI: don't build a feature or extra flexibility until something actually needs it. KISS shapes how you build; YAGNI decides whether to build now.
Fred Brooks distinguished "essential" from "accidental" complexity in software. What is the difference, and how does that distinction guide you when someone asks you to "simplify" a codebase?
basics
~20 sEssential complexity comes from the problem itself — real rules, cases and constraints you cannot delete. Accidental complexity comes from how we built it: tooling, layers, workarounds, duplication. Simplifying means removing accidental complexity; essential complexity can only be moved or made explicit.
Martin Fowler frames YAGNI ("You Aren't Gonna Need It") as an economic trade-off between building a presumptive feature now and carrying it. Walk through those costs, and name the situations where applying YAGNI is the wrong call.
basics
~20 sBuilding early costs the build itself plus a delay to real work, and if you guessed wrong you carry and repair dead complexity forever. YAGNI fails where changing later is expensive or impossible: published APIs, stored/wire data formats, security models, and hard non-functional requirements.
What is the "speculative generality" code smell, and how do the Rule of Three and Sandi Metz's maxim "duplication is far cheaper than the wrong abstraction" help you decide when to introduce an abstraction?
basics
~20 sSpeculative generality is machinery built for a future that never arrives — an interface with one implementation, unused parameters, hooks nobody calls. Rule of Three: wait for the third occurrence before abstracting. Until then, duplication is safer than guessing the wrong shared shape.
How do you build a system that stays cheap to change without violating YAGNI by building speculative extension points? Discuss the mechanisms you would actually use.
basics
~20 sBuy options, not features: keep code well-factored and tested, defer decisions to the last responsible moment, make data and contracts evolvable (versioned, additive-only, expand/contract migrations), and spend up-front design only on decisions that are hard to reverse.
What is Separation of Concerns in software design, and what is a "concern"?
basics
~20 sSeparation of Concerns means splitting a system so each part deals with one distinct topic — for example, storing data, applying business rules, or drawing the screen — instead of mixing them all in one place.
How does Separation of Concerns relate to the Single Responsibility Principle, and where do the two differ?
basics
~20 sThey overlap: SRP says one class should have one reason to change, which is SoC applied to a class. SoC is broader — it applies to functions, files, layers and whole systems, including concerns spread across many classes.
What are cross-cutting concerns (logging, authorization, transactions), why are they hard to separate, and what mechanisms exist to handle them?
basics
~20 sCross-cutting concerns are needs like logging, security checks and transactions that apply almost everywhere. Because they touch every operation, they can't simply live in one module; teams pull them out using wrappers, middleware or framework interceptors instead of copying them into every method.
Layered architecture is the classic way to separate concerns. What are its rules and its failure modes, and when would you separate by feature slice instead of by technical layer?
basics
~20 sLayering splits code into presentation, business logic and data access, with dependencies pointing one way. It fails when every small change touches all layers, when layers just pass data through, or when database concerns leak upward. Splitting by feature keeps related code together instead.
When is applying Separation of Concerns harmful? How do you judge that a boundary costs more than it saves?
basics
~20 sEvery boundary adds indirection: more files, interfaces and hops to read. If splitting doesn't let the parts change, deploy or be tested independently — and instead makes one change touch many modules — the split costs more than it saves.
In software design, what do the terms "cohesion" and "coupling" mean, and what is the standard rule of thumb about them?
basics
~10 sCohesion is how strongly the things inside one module belong together. Coupling is how much one module depends on another. The rule: aim for high cohesion inside modules and low coupling between them.
The classic coupling scale runs from content coupling (worst) to data coupling (best). Walk through the levels and explain why content and common coupling are considered the most dangerous.
basics
~20 sWorst to best: content (one module touches another's internals), common (shared global data), external (shared external format/device), control (a flag tells the callee what to do), stamp (passing a whole record when only a field is needed), data (passing only the simple values needed). Content and common are worst because a change anywhere breaks everything silently.
The classic structured-design cohesion scale ranks a module's internal relatedness from coincidental up to functional. Name the levels in order and give an example of the worst and the best.
basics
~10 sFrom worst to best: coincidental, logical, temporal, procedural, communicational, sequential, functional. Coincidental is a Utils class of unrelated helpers. Functional is a module that does exactly one well-defined job, like calculating sales tax.
Explain fan-in and fan-out, and the metrics afferent coupling (Ca), efferent coupling (Ce), instability (I = Ce / (Ca + Ce)) and abstractness (A). How do you use them to judge a design?
basics
~20 sFan-in is how many modules depend on this one; fan-out is how many it depends on. Afferent coupling Ca counts incoming dependencies, efferent Ce counts outgoing. Instability I = Ce/(Ca+Ce) runs 0 (very depended-on, hard to change) to 1 (depends on everything, easy to change). Stable modules should be abstract.
At architecture scale, how do you decide where module or service boundaries go using cohesion and coupling — and when is accepting tighter coupling the right call?
basics
~20 sPut boundaries where things change together and are owned by one team; keep what changes together inside one boundary. Accept tighter coupling when the parts share an invariant that must hold immediately, when the domain is still uncertain, or when decoupling would cost more than the change it saves.
What does the design guideline "favor composition over inheritance" actually mean, and what is the difference between the two techniques?
basics
~20 sInheritance reuses code by making a new type a subtype of an existing one ("is-a"). Composition reuses code by holding another object as a field and calling it ("has-a"). Prefer composition: it couples types less and can change at runtime.
What is the fragile base class problem, and can you walk through a concrete way a harmless-looking change in a base class breaks its subclasses?
basics
~20 sWhen a subclass overrides methods, it can depend on how the base class calls its own methods internally. If the base later changes those internal calls — even without changing its public behavior — subclasses silently break. That's the fragile base class problem.
When is class inheritance still the right choice over composition? Give concrete criteria, not just "when there's an is-a relationship".
basics
~20 sUse inheritance when the subtype is genuinely substitutable for the base everywhere (Liskov), the base was deliberately designed and documented for extension, and both live under one owner. Closed/sealed hierarchies and framework template-method hooks are good fits.
A hierarchy has grown to dozens of subclasses because behavior varies along several independent axes (e.g. storage backend × serialization format × compression). How would you restructure it, and what is this failure mode called?
basics
~20 sIt's a combinatorial subclass explosion: with inheritance you need one class per combination (2×3×2 = 12). Replace each axis with a composed collaborator chosen by interface — a Strategy per axis — so you write 2+3+2 = 7 parts and assemble them.
Composition usually means delegation. What is delegation in practice, what boilerplate and pitfalls does it bring, and what is the "self-problem"?
basics
~20 sDelegation means an object forwards calls to a collaborator it holds. You get flexibility, but must write a forwarder per method, and the wrapped object's internal calls dispatch to itself — so your wrapper can't intercept them. That's the self-problem.
What is encapsulation in software design, and why is making every field private while adding a public getter and setter for each one not really encapsulation?
basics
~20 sEncapsulation means a component keeps its internal data private and exposes only meaningful operations. A getter and setter for every field still lets outsiders read and change all the state, so nothing is actually hidden and no rules are enforced.
David Parnas argued that modules should be decomposed by information hiding rather than by processing steps. What does that mean in practice, and how do you decide what each module hides?
basics
~20 sInstead of splitting a system into one module per step of the process, split it so each module hides one design decision that is likely to change — a data format, an algorithm, a device, a policy. Everything that would change together lives behind one interface.
What is representation exposure, and how does returning an internal mutable collection or storing a caller-supplied object break a class's invariants?
basics
~20 sRepresentation exposure is handing out a reference to your internal mutable data. If a method returns the live list, or the constructor stores the caller's list directly, the caller can change that data afterwards without going through your validation, so your rules stop holding.
How do you decide which parts of a component belong in its stable public interface and which must stay volatile implementation details? What signals tell you a detail has leaked?
basics
~20 sPublish what callers need to express their intent and what you can promise to keep working; hide anything you expect to change — data layout, algorithms, libraries, protocols. A detail has leaked when a change to it forces callers to change too.
Encapsulation at the class level is enforced by access modifiers. What enforces it at the module or service level, and why is a shared database between two services the classic failure of encapsulation at that scale?
basics
~20 sAt module or service scale there is no private keyword across process boundaries, so encapsulation must be enforced by explicit published interfaces plus tooling and ownership. A shared database breaks it because every table is effectively a public field any service can read and write.
What is the Law of Demeter (also called the "principle of least knowledge") in object-oriented design, and what is a "train wreck" call chain?
basics
~10 sA design guideline: an object should only call methods on its immediate collaborators, not reach through them into strangers. A "train wreck" is a chain like order.getCustomer().getAddress().getCity().getName() that navigates someone else's object graph.
State the formal Law of Demeter rule: which objects may a method M of an object O legitimately send messages to?
basics
~20 sMethod M of object O may call methods on: O itself, O's own fields/components, M's parameters, objects M creates inside itself, and globals in scope. It may not call methods on objects returned by those calls.
Why is "count the dots on the line" a bad test for Law of Demeter violations? Give examples of long chains that are fine and short chains that are not.
basics
~10 sDots don't measure coupling. list.filter(...).map(...).count() has many dots but no new dependencies, while a.getB().doIt() has few dots yet reaches into a stranger. What matters is whether you're walking another object's structure.
What does strict adherence to the Law of Demeter cost, and how do you decide when the wrapper/delegation overhead isn't worth paying?
basics
~20 sEvery hidden hop needs a pass-through method, so classes fill with delegating one-liners (the "middle man" smell), APIs get wider, and changes ripple through several files. It's worth paying when the hidden structure is genuinely likely to change.
How does the Law of Demeter relate to "Tell, Don't Ask" and to the Feature Envy smell, and how do you refactor a train wreck without simply adding a delegating getter?
basics
~20 sA long getter chain usually means the caller is pulling data out to do work that belongs elsewhere (Feature Envy). Instead of adding another getter, move the operation into the object that owns the data — tell it what to do.
What does the design principle "Tell, Don't Ask" mean, and what problem is it meant to prevent?
basics
~20 sInstead of reading an object's data and deciding what to do outside it, ask the object to do the job and let it decide using its own data. Behavior stays next to the data it needs.
How do the "feature envy" code smell and the "anemic domain model" relate to Tell, Don't Ask, and how would you refactor a case of each?
basics
~20 sBoth are what ask-style code looks like at scale. Feature envy is one method using another object's data more than its own; anemic model is data-only classes with all logic in services. Fix by moving the logic onto the class that owns the data.
When is querying an object's state ("asking") the right design rather than a violation of Tell, Don't Ask? Give concrete categories.
basics
~20 sAsking is fine when you only need the value, not a decision about that object: displaying or serializing data, reporting and analytics, test assertions, and pure value objects. The smell is asking and then deciding on the object's behalf.
Following Tell, Don't Ask tends to produce objects whose methods return little or nothing. How do you keep such designs testable and observable without re-exposing internal state?
basics
~10 sHave the object report outcomes instead of state: return a result type, emit a domain event, or call a collaborator you can substitute in tests. Assert on behavior and outcomes rather than on fields.
How does Tell, Don't Ask differ from the Law of Demeter, and why does fixing a "train wreck" like `a.getB().getC().doThing()` by adding delegating methods sometimes make the design worse?
basics
~20 sThe Law of Demeter limits how far you may navigate - only talk to immediate collaborators. Tell, Don't Ask says who should make a decision. Blindly adding pass-through methods satisfies Demeter's letter while still asking, and bloats the middle object.
What is Command-Query Separation (CQS), and how do you classify a method as a command or a query?
basics
~10 sCQS says every method should either change state (a command, returning nothing) or return information (a query, changing nothing) — never both. Put differently: asking a question must not change the answer.
Name several widely accepted violations of Command-Query Separation in real APIs and explain why each exception is justified.
basics
~10 sCommon accepted mixes: stack pop(), queue poll(), iterator next(), compare-and-swap, atomic getAndIncrement, putIfAbsent, and getOrCreate. Each needs the read and write to happen as one atomic step, which two separate calls cannot guarantee.
What is the difference between CQS (Command-Query Separation) and CQRS (Command Query Responsibility Segregation)?
basics
~20 sCQS is a method-level style rule: a method either mutates or returns, never both. CQRS is an architecture pattern: separate read and write models — often separate objects, services, or even databases — for the whole system or a bounded context.
How does Command-Query Separation relate to referential transparency and pure functions, and what concrete capabilities does that unlock?
basics
~20 sIf queries have no side effects, a call can be replaced by its result without changing the program — that is referential transparency. It lets you cache, reorder, parallelize, retry, and safely inspect calls in logs and debuggers.
How would you enforce Command-Query Separation across a large codebase and an HTTP API, and where does the principle break down?
basics
~20 sEnforce it with conventions the tools can check: naming, return types (commands return nothing or just an outcome), separate command/query interfaces, and lint or architecture tests. On HTTP, map queries to safe methods like GET and commands to POST/PUT/DELETE.
In software design, what is the difference between abstraction and indirection, and why is it said that every abstraction introduces indirection but not every indirection is an abstraction?
basics
~20 sAbstraction hides detail behind a simpler idea you can reason about, like 'send a payment'. Indirection just means going through something in between instead of calling the thing directly. Abstractions use indirection, but a pass-through wrapper that hides nothing is only indirection.
What is a 'leaky abstraction' (Joel Spolsky's Law of Leaky Abstractions), and how should it change the way you design and consume abstractions?
basics
~20 sA leaky abstraction is one whose hidden details still show through — usually as performance surprises or unusual errors — so you must understand what is underneath to use it correctly. The law says all non-trivial abstractions leak to some degree.
'Every problem in computer science can be solved by another level of indirection — except the problem of too many levels of indirection.' What concrete costs does each extra layer impose, and how do you judge whether a new one earns its keep?
basics
~20 sEach layer adds code to read, a jump when tracing a bug, another place behaviour can differ, and sometimes runtime cost. A layer earns its place when it hides real complexity or enables a change you actually need now — not a hypothetical one.
How do you decide when to extract an abstraction from duplicated code versus leaving the duplication in place, and what is meant by 'a wrong abstraction is more expensive than duplication'?
basics
~20 sExtract when the pieces are the same idea, not merely the same text, and when they change together for the same reason. If they only look alike, keep the duplication — a shared abstraction forced to serve two different reasons grows flags and branches and becomes harder to remove than the copies were.
What makes an abstraction 'deep' rather than 'shallow' in John Ousterhout's sense (interface cost versus functionality hidden), and how would you apply that idea when drawing module or service boundaries?
basics
~20 sA deep module offers a small, simple interface but hides a lot of work; a shallow one exposes almost as much complexity as it contains. Prefer fewer, deeper boundaries — each interface is a cost paid by every caller.
What is the Principle of Least Astonishment (also called Least Surprise) in software design, and how does it drive naming decisions?
basics
~20 sIt says a component should behave the way a reasonable reader expects from its name and the surrounding conventions. If people are surprised by what a function does, the design is wrong even when the code is correct. Names must promise exactly what happens.
What does "predictable error semantics" mean under the Principle of Least Astonishment, and what are the most common ways an API surprises callers when things go wrong?
basics
~20 sIt means failures are reported the same way everywhere, so callers can predict what happens without reading each implementation: one style of signalling errors, consistent handling of "not found" versus "invalid" versus "broken", and a clearly defined state after a failure.
How does the Principle of Least Astonishment apply to default values and default behavior in configuration and API design, and when should a "safe" default win over an "expected" default?
basics
~20 sDefaults are what most people actually get, so they must match the common case and never hide risk. Pick the value an informed user would have chosen; when the expected default is dangerous — no timeout, verification off — choose the safe one and make the risky option explicit.
The Principle of Least Astonishment can conflict with other goals — performance, security, or introducing a genuinely better abstraction. How do you decide when a deliberate surprise is justified, and how do you manage that across a large system?
basics
~20 sAsk whose expectations you are breaking, how often they will hit it, and how bad it is if they guess wrong. Break an expectation only when the gain is large and durable, then make the surprise loud and impossible to miss — in names, types, and tooling.
What does the "fail fast" design principle mean, and why is stopping at the moment invalid state is detected usually better than letting execution continue?
basics
~20 sFail fast means checking for invalid input or state right where it appears and stopping immediately with a clear error, instead of continuing with bad data that causes a confusing failure much later somewhere else.
Why is validating invariants inside a constructor or factory (an "always-valid" object) considered a stronger application of fail fast than validating the same rules later, and what practical problems does it introduce?
basics
~20 sIf an object checks its rules when it is created, an invalid one can never exist, so no later code has to re-check or handle it. Validating later means broken objects float around until something notices.
When should a system deliberately NOT fail fast, and how do you decide between failing fast and degrading gracefully?
basics
~20 sFail fast when continuing would give wrong answers or corrupt data. Degrade instead when the failure only affects a non-essential part and the core result is still correct - for example hiding a recommendations panel rather than failing the whole page.
When applying fail fast, what is the difference between an assertion, a precondition guard, and validation of untrusted external input - and why is it dangerous to enforce a security rule with an assertion?
basics
~20 sAssertions check assumptions the developer believes are always true and are often switched off in production. Guards check what a caller must supply and always run. Input validation checks data from outside and produces a user-facing error. A security check written as an assertion can vanish in production.
Fail fast is about detecting problems as early as possible. Rank the points at which a defect can be caught - compile/build, CI, service startup, first request, and never - and explain what you gain by moving detection earlier.
basics
~20 sBest is catching it when the code is compiled or built, then in CI, then when the service starts, then on the first request, and worst is never noticing. The earlier the catch, the fewer people are affected and the cheaper the fix.
What does the "Single Source of Truth" design principle mean, and what problem does it prevent?
basics
~20 sEvery fact — a piece of data, a setting, a rule — lives in exactly one authoritative place. Everything else reads from that place or is generated from it, so copies can never quietly disagree.
How does Single Source of Truth differ from the DRY (Don't Repeat Yourself) principle, and how does database normalization relate to both?
basics
~20 sDRY is about not duplicating knowledge in code; SSoT generalises the same idea to data, config, schemas and docs. Database normalization is the data-modelling technique that implements SSoT: each fact stored in exactly one place.
If one canonical store holds the truth, how do you keep derived copies — caches, read models, denormalized tables, generated code — from silently diverging from it?
basics
~20 sMake every copy generated, never hand-edited; give each one a defined refresh mechanism (regeneration, invalidation, replication, or an event stream); make regeneration cheap and repeatable; and add a check that compares copy against owner and alerts on mismatch.
How would you make an API contract or configuration schema the single source of truth across server, clients and documentation, and how do you prove in CI that nothing has drifted from it?
basics
~20 sKeep one machine-readable schema file as the definition, generate server stubs, client code, validation and docs from it, never hand-edit the generated output, and have CI regenerate and fail the build if the result differs from what is committed.
In a system split across several services, how do you decide which service is the single source of truth for a given piece of data, and what goes wrong when two services both write it?
basics
~20 sGive each fact to exactly one service — the one that owns the business rules for changing it. Others read it or subscribe to its change events; they never write it. Two writers means conflicting values with no rule for which is correct.