Skip to main content
Back to module
Software Engineering Practices · Design

Design principles

Reading30mLesson 1 of 3 in this module

Apply single responsibility and DRY with judgement, so code is easy to test and change.

Why it matters

Code is read far more often than it is written. A feature you ship today will be changed next month by a teammate, or by you after you have forgotten the details. Design principles are rules of thumb that make that change cheaper. They are not laws: applied blindly they produce code that is harder to follow, not easier. This lesson covers the two you will use every day, single responsibility and DRY, and the judgement that goes with them.

Single responsibility

The single responsibility principle (the "S" in SOLID) says a unit of code should have one reason to change. "Reason to change" means a person or a requirement: the finance team changes tax rules, the design team changes how a receipt looks, the API team changes the input format. If one function has to change for all three, those concerns are tangled together.

Here is a function that does too much:

function receipt(csv) {
  let total = 0;
  const lines = csv.trim().split("\n");
  for (const line of lines) {
    const [name, qty, price] = line.split(",");
    total += Number(qty) * Number(price);
  }
  const tax = total * 0.08;
  return "Total: $" + (total + tax).toFixed(2);
}

It parses text, does arithmetic, applies a tax rule and formats money. To test the tax rule you have to build a CSV string and then pick a number out of a sentence. Split it by responsibility:

function parseLine(line) {
  const [name, qty, price] = line.split(",").map((s) => s.trim());
  return { name, qty: Number(qty), price: Number(price) };
}

function lineTotal(item) {
  return item.qty * item.price;
}

function formatMoney(amount) {
  return "$" + amount.toFixed(2);
}

function receipt(csv, taxRate) {
  const items = csv.trim().split("\n").map(parseLine);
  const subtotal = items.reduce((sum, item) => sum + lineTotal(item), 0);
  return "Total: " + formatMoney(subtotal * (1 + taxRate));
}

Each small function can be tested on its own, and receipt now reads like a summary of the steps. Notice the tax rate became a parameter: the rule no longer hides inside the code.

DRY: don't repeat yourself

DRY is often summarised as "never copy code", but the original idea (from The Pragmatic Programmer) is narrower: every piece of knowledge should have one authoritative place. If the minimum password length is 8, that number should live in one constant, not in the signup form, the reset form and the API validator separately. When the rule changes to 12, you change one line.

const MIN_PASSWORD_LENGTH = 8;

function isValidPassword(pw) {
  return typeof pw === "string" && pw.length >= MIN_PASSWORD_LENGTH;
}

Use them judiciously

Both principles can be overdone.

  • Accidental duplication is not real duplication. Two functions may look the same today but represent different rules. A shipping fee and a service fee might both be "5% of the subtotal" this year. If you merge them into one fivePercent() helper, the day one of them changes you will add a flag, then another, and the shared helper becomes harder to read than the two copies were.
  • The rule of three. Many teams wait until the third copy before extracting a helper. By then the shared shape is clear.
  • Tiny functions everywhere. Splitting a clear ten-line function into six two-line functions makes readers jump around. Split when a piece has its own name, its own reason to change, or its own tests.
  • Wrong abstraction costs more than duplication. If a shared helper grows a parameter for every caller, inline it back into its callers and start again.

A useful question before extracting anything: if this rule changes, should every caller change with it? If yes, share it. If no, keep the copies.

Other principles you will hear about

SOLID also includes open/closed, Liskov substitution, interface segregation and dependency inversion. In everyday JavaScript the most practical of these is dependency inversion: pass things like the clock, the database or the mailer into a function instead of reaching for globals. You will use that in the testing lesson. KISS (keep it simple) and YAGNI (you aren't gonna need it) are reminders not to build flexibility you have no use for yet.

Common mistakes

  • Treating "one responsibility" as "one line" and fragmenting code.
  • Merging code that only looks alike, then adding boolean flags to tell callers apart.
  • Leaving magic numbers (0.08, 8) scattered through the code instead of naming them once.
  • Mixing input parsing, business rules and output formatting in one function, so nothing can be tested alone.

Recap

  • Single responsibility: one reason to change per unit. Separate parsing, rules and formatting.
  • DRY is about knowledge, not characters. Give each rule one home.
  • Duplication is cheaper than the wrong abstraction. Share code when the callers should change together.

Try it

Open the Do tab and split a tangled receipt function into small, testable pieces.

Also in this lesson: