Beginner14 min readDesign Principles & Heuristicslive prototype

Separation of Concerns

Split a program so each part handles ONE concern — one reason to care — and the parts overlap as little as possible. A 'concern' is a distinct job: checking input, applying business rules, talking to the database, formatting the output. Keep them apart and you can change or test one without breaking the others.

The idea

What it is

Separation of Concerns is a simple idea: split your program so each part has one job. A concern is one distinct thing the program has to care about — checking the input is valid, applying the business rules, saving to the database, formatting what the user sees. When all of those jobs are crammed into one function, the function is doing too much. Pull each job into its own part, and let the parts touch as little as possible.

Think of a restaurant. The waiter takes your order. The chef cooks the food. The cashier handles the money. Each person has one concern and does it well. Now imagine one person trying to take orders, cook, and run the till all at once — orders get dropped, the food burns, the cash drawer is wrong. That is what a tangled function feels like. Separation of concerns is just giving each job to its own role.

The one sentence to remember

One part, one concern — one reason to care. If you can describe what a part does without saying 'and', it probably has a single concern. If you keep saying 'it validates the input and saves to the DB and renders the page', that is three concerns wearing one coat.

This is the umbrella over many rules you already know

Separation of Concerns is the big idea that smaller rules are special cases of. The Single Responsibility Principle (a class should have one reason to change) is SoC at the class level. MVC (Model–View–Controller) is SoC for UI apps — data, display, and control kept apart. Clean / layered architecture is SoC across a whole system. Learn the umbrella and the rest click into place.

Mechanics

How it works

What counts as a 'concern'

A concern is a distinct aspect of the work — one reason the code might need to change. In a typical request handler you can usually spot four:

  • Validation — is the input well-formed and allowed? (empty email? password too short?)
  • Business logic — the actual rules of your product. (a new user starts on the free plan; block disposable email domains)
  • Data access — talking to storage. (insert the row, read it back, handle a duplicate key)
  • Presentation — shaping the output. (build the JSON, render the HTML, pick the status code)

Each of these changes for a different reason and at a different time. The validation rules come from product. The database choice comes from infrastructure. The page layout comes from design. When they share a function, a change to any one of them puts the others at risk.

The smell: one function doing everything

Here is the trap, and it is the natural first draft everyone writes — the four concerns interleaved line by line:

typescript
function handleSignup(req) {
  if (!req.email.includes("@")) return res(400, "bad email");   // validation
  if (req.password.length < 8)  return res(400, "weak password"); // validation
  const plan = "free";                                            // business
  if (blocked(req.email))       return res(403, "blocked domain");// business
  db.query("INSERT INTO users (email, plan) VALUES (?, ?)",       // data access
           [req.email, plan]);
  const user = db.query("SELECT * FROM users WHERE email = ?",    // data access
                        [req.email]);
  return res(200, "<h1>Welcome, " + user.email + "!</h1>");       // presentation
}

It works. But it is welded together. Want to move from MySQL to Postgres? You are editing the same function that holds your validation and your HTML. Want to unit-test the signup rules without a database? You can't — the rules and the db.query calls live on adjacent lines. Want to return JSON for a mobile app and HTML for the web? Now the presentation logic is tangled in too. One concern can't move without disturbing the others.

The fix: give each concern its own part

Pull each concern into its own module with a clear, narrow job. The handler becomes a short coordinator that calls them in order:

typescript
// validation.ts  — one concern: is the input OK?
export function validateSignup(req) { /* ...checks, throws on bad... */ }

// signup-service.ts — one concern: the business rules
export function signup(email) { return users.create(email, "free"); }

// users-repo.ts — one concern: talking to storage
export const users = { create(email, plan) { /* db insert + read back */ } };

// presenter.ts — one concern: shaping the output
export function welcomeHtml(user) { return `<h1>Welcome, ${user.email}!</h1>`; }

// handler.ts — a thin coordinator that wires the concerns together
function handleSignup(req) {
  validateSignup(req);
  const user = signup(req.email);
  return res(200, welcomeHtml(user));
}

Now each part has one reason to change. Swap the database? You touch only users-repo.ts. Tweak a validation rule? Only validation.ts. Add a JSON response? Add a presenter — the rules and storage never notice. The pieces are also reusable (the validator can guard other endpoints) and testable in isolation (test the business rules with a fake repo, no real database).

Layering: the most common way to separate

The classic shape is layers, stacked so each only talks to the one below it: Presentation (what the user sees) → Domain / Business Logic (your rules) → Data Access (storage). This is sometimes called presentation–domain–data layering. The point of the stack is that a concern is sealed off: the presentation layer never writes SQL, and the data layer never builds HTML.

BEFORE — tangled AFTER — separated handleSignup() validate email pick free plan INSERT user check password render HTML SELECT user 4 concerns, one function Presentation render output · 1 concern Business Logic the rules · 1 concern Data Access talk to storage · 1 concern each layer changes alone
Before, one handleSignup() holds four interleaved concerns — validation (blue), business (orange), data (green), presentation (violet) — so a change to any one risks the rest. After, the same work is split into stacked single-concern layers, each only talking to the one below. Swap the storage and only Data Access changes; the layers above it never notice.

Why this is the whole point

  • Change safely — touch one concern, leave the rest alone. Swapping the database can't break your validation if they live in different modules.
  • Test in isolation — unit-test the business rules with a fake repository: no database, no network, fast and deterministic.
  • Reuse — a sealed-off validator or presenter can be reused by other endpoints instead of being copy-pasted out of a giant function.
  • Understand faster — to fix an HTML bug you open the presentation module, not a 200-line function where everything is mixed together.

Separation has a cost — don't over-split

Splitting creates more files, more indirection, and more wiring to follow. For a tiny throwaway script or a one-off function, four layers is ceremony with no payoff — it just makes a 10-line job feel like a 5-file project. Separate the concerns that actually change for different reasons and at different times. If two 'concerns' always change together, they might really be one. Aim for low coupling, high cohesion, not the maximum number of files.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

One handleSignup() function whose body is a tangle of four colored concerns — validation (blue), business rules (orange), data access (green), and presentation (violet) — all interleaved on the same lines, with a live readout: concerns crammed in: 4. Press Separate concerns and the lines sort themselves into four clean stacked lanes, each now a single-concern module showing concerns: 1. Click any lane and the one fixed panel tells you exactly what a change to that concern now touches — swap the DB → only Data Access changes; the other three are untouched. One panel, replaced each click; nothing scrolls away.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

See the tangle

Open the prototype. The handleSignup() box is a list of mixed lines, each tinted by its concern — validation (blue), business logic (orange), data access (green), presentation (violet) — and they are visibly interleaved. Read the live counter: concerns crammed in: 4. This one function cares about four different things at once. That is the smell.

try 02

Separate the concerns

Press Separate concerns and watch the lines sort themselves into four clean stacked lanes — Validation → Business Logic → Data Access → Presentation. Each lane is now its own single-concern module, and each readout flips to concerns: 1. Nothing was deleted; the same work is just sorted so each part has one job.

try 03

Change one thing, see what it touches

Click any lane to ask 'what does changing this concern affect now?'. Click Data Access and the one explain panel says: swap the DB → only the Data Access layer changes; the other three are untouched. Click Presentation and see that adding a JSON response touches only that lane. That isolation — change one concern without disturbing the rest — is the entire payoff of separating them.

In practice

When to use it — and what trips people up

Separate the concerns that change for different reasons

Separation pays off when a chunk of code mixes jobs that evolve independently — different reasons to change, different people, different speeds. Those seams are where bugs leak across and where tests get hard.

  • Request handlers / controllers — split validation, business rules, storage, and response formatting instead of one fat handler.
  • Anything touching the database — keep SQL behind a data-access layer so the rules above it never see a query string.
  • UI apps — keep data, display, and control apart (this is exactly what MVC and its cousins do).
  • Code you want to unit-test — if you can't test the rules without standing up real infrastructure, the rules and the infrastructure want separating.

Don't separate things that always change together

Separation is a tool, not a quota. For a small script, a quick prototype, or two 'concerns' that always move at the same time, splitting just adds files and indirection for no benefit. The signal to separate is different reasons to change. If validation and presentation truly never change apart, leaving them together is fine — high cohesion beats artificial splits.

What it gives you

  • Change safely — touch one concern without disturbing the others, because each lives in its own part.
  • Testable in isolation — swap a fake data layer and unit-test the business rules with no database or network.
  • Reusable — a sealed-off validator, repo, or presenter can serve many callers instead of being copy-pasted.
  • Easier to understand — to fix one kind of bug you open one focused module, not a giant do-everything function.
  • Parallel work — once the seams are clear, different people can build the layers at the same time.

Common mistakes

  • More files and indirection — one function becomes several modules, which is more to open and navigate.
  • Wiring overhead — something must coordinate the parts and pass data between them.
  • Over-splitting — pushed too far it turns simple code into layer soup, with empty pass-through modules.
  • Wrong cuts hurt — separating along the wrong seam (concerns that always change together) adds ceremony with no payoff.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// BEFORE — one function tangles all four concerns together.
function handleSignupV0(req: { email: string; password: string }) {
  if (!req.email.includes("@")) throw new Error("bad email");      // validation
  if (req.password.length < 8) throw new Error("weak password");   // validation
  db.run("INSERT INTO users(email, plan) VALUES(?, ?)",            // data access
         [req.email, "free"]);
  return `<h1>Welcome, ${req.email}!</h1>`;                         // presentation
}

// AFTER — one concern per module; the handler just coordinates.
function validateSignup(req: { email: string; password: string }) {  // validation
  if (!req.email.includes("@")) throw new Error("bad email");
  if (req.password.length < 8) throw new Error("weak password");
}
const users = {                                                      // data access
  create(email: string, plan: string) {
    db.run("INSERT INTO users(email, plan) VALUES(?, ?)", [email, plan]);
    return { email, plan };
  },
};
function signup(email: string) {                                     // business logic
  return users.create(email, "free");
}
function welcomeHtml(user: { email: string }) {                      // presentation
  return `<h1>Welcome, ${user.email}!</h1>`;
}
function handleSignup(req: { email: string; password: string }) {   // coordinator
  validateSignup(req);
  return welcomeHtml(signup(req.email));
}

References & further reading

6 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is a 'concern' in separation of concerns?

question 02 / 05

A single handleSignup() function validates input, applies business rules, runs SQL, and builds the HTML response. Why is mixing these a problem?

question 03 / 05

After splitting signup into Validation, Business Logic, Data Access, and Presentation layers, you switch from MySQL to Postgres. What should you have to change?

question 04 / 05

How do the Single Responsibility Principle, MVC, and clean architecture relate to separation of concerns?

question 05 / 05

When is it a mistake to split code into separate concerns?

0/5 answered