Home › Low-Level Design › Design Principles: DRY, KISS, YAGNI, Coupling and Cohesion
D

Design Principles: DRY, KISS, YAGNI, Coupling and Cohesion

These five words decide whether your design survives the second feature request. Patterns and SOLID get the attention, but in interviews I can tell within ten minutes whether someone actually internalized these. They never show up as a direct question. The interviewer hands you a twist and watches whether your design bends or snaps.

Low-Level DesignMedium11 min read

If you remember five things

The whole answer in one glance. Everything below is the defense of these five calls.

  1. DRY is about knowledge, not characters. One home per business rule. Two blocks that merely look alike but change for different reasons are not a violation, and merging them is the mistake.
  2. KISS is scored by the reader. Simple means a tired teammate can follow it at midnight, not fewest lines. A clever regex one-liner fails KISS even when it is shorter.
  3. YAGNI beats speculative generality. Build the requirement in front of you. The imagined future arrives in a shape you did not predict, so the hooks you pre-installed are wrong anyway.
  4. Low coupling, high cohesion is the dial. Modules talk through small stable interfaces, and each class does one job. This is the one pair you can actually draw, so it earns the points.
  5. Narrate the trade-off out loud. "Gateway stays behind an interface, no cache yet (YAGNI), here is where it would go." Saying it on purpose is what separates senior from mid-level.

1The problem it solves#

Code does not rot because it was wrong on day one. It rots three specific ways, and these principles are the names we give those failures so we can catch them early.

The three ways a codebase dies.

  • Duplication drifts. The same logic lives in four places and they fall out of sync.
  • Everything touches everything. One change ripples across files that had no business being involved.
  • Speculative machinery. Someone built a plugin framework for a feature that still has one implementation.

I have watched all three kill real systems.

  • A pricing rule copied into the mobile client, the web client, and a nightly batch job. The tax law changed, one copy got missed, and the invoices went out wrong. That is a DRY failure.
  • A class that knows the database, the HTTP layer, and the email templates all at once. You cannot test any of it without standing up the whole world, so nobody tests it. That is low cohesion plus high coupling.
  • An abstraction layer with three interfaces and a factory feeding one concrete class that never got a sibling in two years. That is a YAGNI failure.

What each principle fights.

  • DRY fights duplication of knowledge.
  • KISS fights needless cleverness.
  • YAGNI fights speculative generality.
  • Coupling and cohesion are the two dials that tell you whether your module boundaries sit in the right places.

Get these right and the fancy patterns mostly take care of themselves. Strategy, Factory, and Observer are just these principles with sharper names.

The point of this page

Nobody asks you to define cohesion in an interview. They hand you a feature twist and grade whether your design absorbs it with one new class or forces a rewrite. These five words are the levers behind that single moment.

2The idea (mental model)#

Here is how I hold each one in my head. Short, so I can say it at the whiteboard without a speech.

DRY (Don't Repeat Yourself).

  • Every piece of knowledge has one home. The key word is knowledge, not characters.
  • Two functions that look similar but encode different business rules are not a DRY violation.
  • A tax calculation living in three files is: change the rate and you have three edits, one of which will be missed.

KISS (Keep It Simple).

  • Prefer the solution a tired teammate can read at midnight during an incident.
  • Simple is not short, and it is not easy to write.
  • A clever one-liner with a regex and a nested ternary is easy to write and hard to read. That fails KISS even at five lines shorter.

YAGNI (You Aren't Gonna Need It).

  • Build for the requirement in front of you, not the one you imagine.
  • The future requirement either never comes, or comes in a shape you did not predict.
  • Either way the hooks you pre-installed are wrong, and now you maintain two mistakes.

Coupling.

  • How much one module depends on the guts of another.
  • Low coupling means I can change module A without opening module B. That is the goal.

Cohesion.

  • How well the things inside one module belong together.
  • High cohesion means a class does one job and everything in it serves that job.

The gut-check I use at the whiteboard. Aim for low coupling, high cohesion, and watch for two tells.

  • A class name with "And" or "Manager" or "Util" in it usually has low cohesion.
  • If changing one feature forces edits across five classes, coupling is probably high.
  • Neither check is a law, but both have saved me from bad designs more than once.

Low coupling, high cohesion is the only design dial you can draw. So draw it.

3Structure#

Coupling and cohesion are the two you can put on a whiteboard, and drawing them is what earns points. Picture an order checkout.

The low-cohesion version: one OrderProcessor does everything.

  • Validates the cart, charges the card, writes the database, sends the email.
  • Four reasons to change, one class. That is low cohesion.
  • It reaches into the payment gateway's concrete type and the SMTP client directly, so a change in either ripples straight into the core. That is high coupling.

The clean version: split by responsibility.

  • OrderService orchestrates, nothing more.
  • It talks to a PaymentGateway interface, an OrderRepository interface, and a Notifier interface.
  • Each collaborator has one job (high cohesion), and OrderService depends on abstractions, not concrete classes (low coupling).
  • Swapping Stripe for Razorpay means writing one new class, not editing the orchestrator or touching a single line inside checkout.
classDiagram class OrderService { -PaymentGateway gateway -OrderRepository repo -Notifier notifier +checkout(cart) Order } class PaymentGateway { <> +charge(amount) PaymentResult } class OrderRepository { <> +save(order) void } class Notifier { <> +send(order) void } class StripeGateway class SqlOrderRepository class EmailNotifier class Order class OrderItem PaymentGateway <|-- StripeGateway OrderRepository <|-- SqlOrderRepository Notifier <|-- EmailNotifier OrderService --> PaymentGateway OrderService --> OrderRepository OrderService --> Notifier Order *-- OrderItem
Low coupling and high cohesion drawn out. Notice OrderService points only at the three interfaces, never the concrete classes, and the concrete classes implement those interfaces so they are swappable by construction. The filled diamond on Order *-- OrderItem is composition: items have no life outside their order, so kill the order and the items die with it. This is the same shape as the dependency inversion idea in SOLID.

4Implementation in Java#

First, what bad looks like. One class, everything inside, welded to concrete types.

// Low cohesion, high coupling. Avoid this.
class OrderProcessor {
    void process(Cart cart) {
        if (cart.getItems().isEmpty()) throw new IllegalStateException("empty");

        double total = 0;
        for (Item i : cart.getItems()) total += i.price() * i.qty();

        StripeClient stripe = new StripeClient("sk_live_...");  // hard dependency
        stripe.charge(total);

        Connection c = DriverManager.getConnection("jdbc:...");  // hard dependency
        // ... raw SQL insert ...

        SmtpSender smtp = new SmtpSender("smtp.myhost.com");     // hard dependency
        smtp.send(cart.getUserEmail(), "Thanks for your order");
    }
}

What is wrong, specifically.

  • You cannot test process without a live Stripe key, a database, and an SMTP server.
  • Four responsibilities, three concrete dependencies baked in with new.
  • The total is computed inline with a double: a money bug waiting to happen, and a DRY violation the moment a second caller needs the same number.
Pause before the clean version: which single change to this class removes the most pain, coupling or cohesion?
Reveal the answer
Coupling, because it unblocks testing. The three new calls are what make process untestable, and testability is the first thing an interviewer probes. Pull the dependencies behind interfaces passed through the constructor and you can run the whole flow with fakes and zero network. Cohesion (splitting the four jobs) is the follow-on move, but the hard dependencies are the bleeding wound. Fix coupling first.

Now the version that respects the principles.

interface PaymentGateway { PaymentResult charge(long cents); }
interface OrderRepository { void save(Order order); }
interface Notifier { void send(Order order); }

class OrderService {
    private final PaymentGateway gateway;
    private final OrderRepository repo;
    private final Notifier notifier;

    OrderService(PaymentGateway gateway, OrderRepository repo, Notifier notifier) {
        this.gateway = gateway;
        this.repo = repo;
        this.notifier = notifier;
    }

    Order checkout(Cart cart) {
        cart.validate();                         // cart owns its own rules (DRY)
        long cents = cart.totalInCents();        // total computed in ONE place
        PaymentResult result = gateway.charge(cents);
        if (!result.success()) {
            throw new PaymentFailedException(result.reason());
        }
        Order order = Order.from(cart, result.transactionId());
        repo.save(order);
        notifier.send(order);
        return order;
    }
}

Three things to point at while the interviewer reads it.

  • DRY. The total lives in Cart.totalInCents() as integer cents, so every caller gets the same number and nobody fights floating-point rounding.
  • Low coupling. OrderService depends on interfaces passed through the constructor, so I hand it fakes in a unit test and run the whole checkout with zero network calls.
  • YAGNI. No plugin registry, no strategy map, no YAML config for payment providers I do not have yet. When a second gateway shows up, I write one class that implements PaymentGateway and change one line at the composition root.

On KISS. checkout reads top to bottom like a sentence: validate, charge, build, save, notify. Nobody needs a diagram to follow it, and that readability is the point, not the line count.

5A real-world example#

The clearest place I see these play out is a notification system, a common LLD prompt at companies like Uber and Atlassian.

The early version: a NotificationManager with an if/else chain.

  • If the channel is email do this, if SMS do that, if push do the other.
  • Low cohesion: one class knows every channel's quirks.
  • High coupling: adding Slack means editing the one class everyone else also edits, a merge-conflict magnet.
  • Quiet DRY violation: retry and backoff logic gets copy-pasted into each branch and drifts. The email branch retries three times, the SMS branch retries five, and nobody meant for that.

The clean version: each channel behind a Channel interface.

  • Cohesion goes up: each class knows exactly one delivery mechanism and nothing else.
  • The shared retry and logging logic lives in one ReliableChannel wrapper (the Decorator pattern). That is the DRY win: the retry policy now has one home.

The YAGNI part that trips people up.

  • Do not build a rules engine that lets admins configure channel routing through a UI unless the prompt asks for it.
  • Name the extension point out loud instead: "I would add a routing strategy here if we needed per-user preferences," then move on.
  • Stating the seam shows judgment. Building it shows you did not hear the requirement.

Why this pattern is worth memorizing. That same split, an orchestrator plus one-job collaborators behind interfaces, is the backbone of a parking lot design, a rate limiter, a logging framework, and a vending machine. Learn it once and you have a template for most of the catalog.

6When to use it, and when not to#

Every principle has an edge where it stops helping. Knowing the edge is the senior part.

DRY. Apply it to knowledge, not to lines that happen to match.

  • The trap is deduplicating two things that look identical today but represent different concepts.
  • Force them to share code and a change to one breaks the other, and you spend a sprint untangling them.
  • If two chunks have different reasons to change, let them stay separate. A little duplication is far cheaper than the wrong abstraction.

KISS. Almost always on.

  • The rare exception is where the simple version genuinely cannot meet a hard requirement, like a hot path that needs a cache or a lock-free structure to hit a latency target.
  • Even then, isolate the complexity behind a clean interface so the rest of the code stays simple and the clever part is one small, well-tested box.

YAGNI. Default to it in interviews and in most product code.

  • Pull back when the cost of adding something later is brutal: a public API contract, or a database schema that millions of rows will depend on.
  • There, a little upfront thought is cheap insurance, because migrating a live schema under load is one of the worst jobs in the business.
  • For internal code you control end to end, wait.

Coupling and cohesion. Always the goal, but do not chase the extremes.

  • Do not chase zero coupling. Modules have to talk to do anything useful. The aim is that they talk through small, stable interfaces instead of reaching into each other's internals.
  • Do not split a cohesive class just to hit a class-count target. One well-named class that does one job beats three anemic ones that only make sense read together.

7Common mistakes#

Five failure modes I see on repeat, each a principle applied without judgment.

  • Over-DRYing into a god helper. A Utils class with forty unrelated static methods is not DRY, it is a dumping ground with terrible cohesion. DRY means one home per piece of knowledge, not one home for all knowledge.
  • Confusing simple with easy. People defend a dense clever block by calling it "simpler" because it is fewer lines. KISS is about the reader. Measure it by how fast a new teammate understands it cold, not by how good it felt to write.
  • YAGNI as an excuse for sloppiness. YAGNI says do not build unused features. It does not say skip error handling or write the thing badly. Simple and unfinished are different, and conflating them is how you ship bugs with a clear conscience.
  • Faking low coupling with an interface that has one implementation forever. An interface helps only if it hides something that varies or needs swapping in tests. Wrapping every class in an interface out of habit adds indirection and buys nothing. That is YAGNI biting back, now with extra files to navigate.
  • Chasing cohesion metrics instead of responsibilities. The question is never "how many methods," it is "does everything here serve one job." Split by responsibility, not by line count, and let the method count fall out of that.
Red flag that fails you here

Reaching for a generic plugin system, a config-driven rules engine, or an interface on every class before anyone asked for a second implementation. It reads as "I cannot tell signal from habit." Name the seam, do not build it, and the interviewer sees judgment instead of gold-plating.

8Where it shows up in interviews#

Interviewers rarely ask you to explain DRY. They watch for it while you design something else. Here are the signals they grade silently.

  • You compute once and reuse. Put the total in one place instead of recomputing it in three methods. That is DRY, and the recompute is the single most common tell of a junior answer.
  • You absorb the twist with one class. When they say "now support a second payment provider," a low-coupling design means you add a class and change one line. A high-coupling one means you rewrite the core. This is the most common follow-up I see, a coupling test disguised as a feature request.
  • You resist gold-plating. Start sketching a generic plugin system before anyone asked and a sharp interviewer marks a YAGNI miss. Name the extension point, do not build it.
  • You split on reasons to change. "I pulled payment out because it changes for different reasons than persistence" is a cohesion answer, and it lands because it shows you split on reasons, not on vibes.
From the interviewer's chair

I am not waiting for definitions. I am watching whether you narrate the trade-off as you draw: "gateway stays behind an interface so the service stays decoupled; no cache yet, YAGNI, but here is where it goes." That running commentary is the clearest senior signal I get, because it proves you are choosing on purpose rather than by accident.

Follow-ups the interviewer will ask (and your answers)
"Two methods look almost identical. Should I DRY them up?"
Only if they encode the same piece of knowledge. If they change for the same reason, merge them. If they just happen to look alike today but represent different business rules, leave them apart. Forcing a shared abstraction on two concepts that diverge later is more expensive than the duplication ever was.
"Now add a second payment provider. How much of this changes?"
One new class that implements PaymentGateway, and one line at the composition root where I wire it in. The orchestrator does not change, and nothing inside checkout moves. That is the whole point of depending on the interface instead of the concrete client.
"When is wrapping a class in an interface the wrong call?"
When it hides nothing. An interface earns its keep only if it has more than one implementation, or needs a fake in tests, or marks a real swap point. One implementation forever with no test seam is just indirection and extra files. That is YAGNI biting back.
"How do you know a class has low cohesion?"
Count its reasons to change. If I can say "it changes when payment changes, and when persistence changes, and when the email copy changes," that is three reasons in one class, so cohesion is low. The name is a tell too: "And," "Manager," or "Util" usually means a grab bag. Split by reason to change, not by method count.
"You said YAGNI, but schemas are expensive to migrate. Isn't that a contradiction?"
No, YAGNI has an edge and that is it. For internal code I control end to end, I wait, because changing it later is cheap. For a public API contract or a database schema millions of rows depend on, a little upfront thought is cheap insurance, because migrating a live schema under load is brutal. The cost of being wrong later is what moves the line.

What you can now explain in the room

  • Why DRY is about knowledge, not matching lines, and when deduplicating is actively the wrong move.
  • Why KISS is scored by the reader at midnight, not by line count.
  • When YAGNI flips, and why schemas and public APIs are the exception.
  • How to draw low coupling and high cohesion with an orchestrator plus interfaces.
  • Why pulling payment, persistence, and notification apart makes the design absorb a new provider in one class.
  • How to narrate the trade-offs out loud so the interviewer hears judgment instead of habit.

Next, pair this with the SOLID principles and the Strategy and Factory patterns, which are really just these ideas with sharper names.

Get a prep plan built around your Design Principles: DRY, KISS, YAGNI, Coupling and Cohesion interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Design Principles: DRY, KISS, YAGNI, Coupling and Cohesion prep plan is on the way.