Separation of Concerns

Keeping different responsibilities — like displaying data, fetching it, and validating it — in different places instead of tangled together.

What is it?

Picture a single function that, when a user submits a form, checks that the email address looks valid, sends the data to the server, formats the response into HTML, and updates the page — all in one block of code. It works. But now imagine you need to reuse that validation logic somewhere else, or you need to test it, or the designer wants the success message to look different. Every one of those small requests forces you to wade through the whole tangled function, being careful not to break the other three things it's also doing.

The fix is to give each responsibility — each concern — its own place: one piece of code whose only job is validating the email, one whose only job is sending the request, one whose only job is updating what's on screen. This is called separation of concerns. Each piece becomes small enough to read in one sitting, test on its own, and reuse in a different context without dragging the other concerns along with it.

It's less about where you draw the lines (that varies by project) and more about the discipline of drawing lines at all — refusing to let "validate this," "fetch that," and "display this" live in the same breath of code.

Explain like I'm 10

A restaurant kitchen has a station for chopping vegetables, a station for grilling, and a station for plating — not one cook doing everything at once at a single counter. Each station focuses on one job, so the kitchen can scale, and one person's mistake doesn't ruin every dish.

Examples

Before: everything tangled together

function submitForm(email, password) {
  // validation
  if (!email.includes("@")) {
    alert("Invalid email");
    return;
  }

  // network call
  fetch("/api/signup", {
    method: "POST",
    body: JSON.stringify({ email, password }),
  }).then((res) => {
    // updating the UI
    document.getElementById("status").innerText = "Signed up!";
  });
}

Validation, the network request, and the UI update are all interleaved in one function. You can't test the validation without also triggering a real network call.

After: each concern separated

function isValidEmail(email) {
  return email.includes("@");
}

function signup(email, password) {
  return fetch("/api/signup", {
    method: "POST",
    body: JSON.stringify({ email, password }),
  });
}

function showStatus(message) {
  document.getElementById("status").innerText = message;
}

async function submitForm(email, password) {
  if (!isValidEmail(email)) return showStatus("Invalid email");
  await signup(email, password);
  showStatus("Signed up!");
}

Now isValidEmail can be unit-tested with no network involved, signup can be reused from a different form, and showStatus can change its rendering without touching the other two.

How it works

Separation of concerns works by asking, for every piece of logic, "what is the single reason this code would need to change?" Validation rules change for one reason (the business decides what counts as valid). Network logic changes for another reason (the API changes). Display logic changes for a third (the designer wants different wording). When code for multiple reasons-to-change is mixed into one place, a change driven by any one of those reasons risks breaking the others. Splitting by concern means each piece only ever changes for its own reason.

Why does it exist?

It exists because tangled code is expensive in very predictable ways: it's hard to test (you can't isolate one concern), hard to reuse (you'd have to copy the whole tangle to reuse one part), and hard to change safely (touching one concern risks breaking another that happens to sit next to it). Separating concerns trades a small amount of upfront organization for a large, ongoing reduction in the cost of change.

When to use it

Apply it as soon as a piece of code is doing more than one identifiably-different job — especially the classic trio of getting data, transforming or validating it, and displaying or outputting it. It's most valuable in code you expect to test, reuse, or revisit.

When not to use it

For a genuinely tiny, one-off piece of code — a five-line script you'll run once — splitting it into multiple functions across multiple concerns can be more overhead than the code itself. The judgment call is whether the code will be read, tested, or changed again; if not, some tangling is harmless.

Common mistakes

  • Splitting code into more functions without actually separating concerns — e.g., three functions that each still mix validation and network calls.

  • Over-separating trivial code, creating a maze of tiny functions that's harder to follow than the original tangle.

  • Leaving a 'coordinator' function that itself contains business logic, instead of having it purely delegate to the separated pieces.

Practice exercises

  1. Easy:

    Take a function that both validates a phone number and formats it for display, and split it into two separate functions.

  2. Medium:

    Refactor a function that fetches a list of users from an API, filters out inactive ones, and renders them to the DOM, into three separated pieces.

  3. Hard:

    Given a single 40-line function that reads a file, parses CSV rows, validates each row, and writes valid rows to a database, redesign it into separated concerns and explain what you'd unit test on each piece.

Interview questions

What is separation of concerns?

The practice of keeping different responsibilities of a program — such as data access, business rules, and presentation — in separate, independent parts of the code rather than mixed together.

Why does mixing concerns together make code harder to test?

Because you can't exercise one concern (like a validation rule) in isolation — running it also triggers the other concerns it's tangled with, like a network call or a UI update.

How do you decide where to draw the line between two concerns?

Ask whether the two pieces of logic would change for different reasons — if validation rules and network error handling change independently of each other, they're different concerns and belong in different places.