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

Design principles

Reading30mLesson 1 of 3 in this module

What you'll learn

  • Apply single responsibility and DRY judiciously
  • Split a function that mixes parsing, rules and formatting into small testable pieces
  • Give each business rule or magic number one authoritative home
  • Recognise when duplication is cheaper than the wrong abstraction

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:

JavaScript
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:

JavaScript
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.

JavaScript
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.

Key terms (6)

single responsibility
The principle that a unit of code should have one reason to change, such as one requirement or one team's rules.
DRY
Don't repeat yourself: every piece of knowledge, such as a rule or a limit, should have one authoritative place in the code.
magic number
An unnamed literal value like 0.08 buried in code; naming it as a constant makes its meaning clear and its change a one-line edit.
rule of three
A habit of waiting until code appears a third time before extracting a shared helper, so the common shape is clear.
dependency inversion
Passing collaborators such as the clock, database or mailer into a function instead of reaching for globals.
YAGNI
You aren't gonna need it: a reminder not to build flexibility or features before there is a real use for them.

Recap

  • Single responsibility means one reason to change per unit: separate parsing, business rules and formatting.
  • DRY is about knowledge, not characters: give each rule or constant one home.
  • Code that only looks alike may represent different rules; merging it leads to flags and confusion.
  • Share code when every caller should change together; otherwise keep the copies.
  • Splitting too finely hurts readability; split when a piece has its own name, reason to change or tests.
Also in this lesson: