Skip to main content
Saved
Foundation 12 min read

Logical Separation of Concerns

Component-based architectures still separate concerns, but they separate frontend code by feature instead of by technology.

TL;DR

TL;DR

Separation of concerns still applies in modern frontend development, but the boundaries have moved. We stopped separating HTML, CSS, and JavaScript and started separating features from each other. It’s still the same principle, that things which change together should stay together, and things that change independently should be kept apart.

Core principles:

A component groups the markup, styles, and logic for one feature, so your code is separated by feature instead of by technology
When you keep the code that changes together in one place, your code has high cohesion
When each unit of code has a single reason to change, coupling stays low
It’s still worth keeping presentation and logic separate, even inside a single component

It is what I sometimes have called "the separation of concerns", which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts, that I know of.

Edsger W. Dijkstra, On the role of scientific thought (EWD 447), 1974

The Evolution of a Principle

We used to keep HTML, CSS, and JavaScript in separate files, with structure in one place, presentation in another, and behavior in a third. We called this separation of concerns, and hardly anyone questioned it.

Then React arrived and put them all together. Vue and Svelte followed, and co-location became the norm, with components containing their own markup, styles, and logic in a single file or a tightly coupled set of files.

Critics called it a step backward and defenders called it evolution. I think both sides skipped a more basic question: what are we actually trying to separate, and why?

We still separate concerns, just along different boundaries. Knowing where those boundaries are now helps you make better architectural decisions in your frontend code.

Defining Separation of Concerns

With separation of concerns, you organize your code so that different parts of the system handle different responsibilities. Each part focuses on one thing, which is its “concern,” and depends as little as possible on the other parts. When one concern changes, you should only need to change the code responsible for it.

That makes your code easier to maintain, because you know where to look when something needs changing, and easier to test, because you can test each concern separately. You can also swap one implementation for another without rewriting unrelated code, and different people can work on different concerns without having to coordinate all the time.

Things that change together should stay together, and things that change independently should be kept apart.

When related code is grouped together like this and unrelated code is kept apart, we say the code has high cohesion.

There’s no fixed rule for which code counts as “related”, though, because it depends on what you’re optimizing for and who’s doing the work. Two pieces of code might be related in one sense, because they use the same technology, and unrelated in another, because they serve different features. So which relationship should you use when you decide how to group your code?

Over the history of frontend development, we’ve changed how we decide what’s “related” enough to group together.

The Classical Web Interpretation

For most of the web’s history, we grouped code by technology. HTML handled structure, CSS handled presentation, and JavaScript handled behavior, so we treated those as three concerns and kept them in three sets of files.

This made sense, because the web was built for documents, and documents have natural layers. The content exists independently of how it looks, and how it looks exists independently of any interactive behavior. A webpage should still work without CSS, just not as pretty, and without JavaScript, just not as dynamic. Each layer adds to the ones below it.

The Classical Separation Model

Benefits

  • Progressive enhancement: Each layer was optional, so the page still worked without it
  • Team alignment: Designers worked in CSS, and JavaScript specialists worked in scripts
  • Caching: CSS and JavaScript could be cached independently
  • Easy to find: You always knew the button styles were in the stylesheet

A Good Fit for Documents

It suited the way we built websites at the time, when:

  • Content existed independently of presentation
  • Presentation existed independently of behavior
  • Pages were server-rendered with modest interactivity
  • Teams were organized by technology specialism

Our directory structures followed the same split, with a styles/ folder for all the CSS, a scripts/ folder for all the JavaScript, and template files kept with the server-side code. When you needed to change how buttons looked, you went to the stylesheet. When you needed to change what happened when someone clicked a button, you went to the JavaScript.

This worked well for websites, until we started building applications.

Limits of the Model

Applications aren’t documents, and that changes the best way to split up the code. A document presents information, while an application manages state, responds to events, and shows an interface that changes from moment to moment. The concerns in an application don’t map cleanly onto structure, presentation, and behavior.

In the classical model, a dropdown’s markup is in HTML, its appearance is in CSS, and its open and close logic is in JavaScript, spread across three files in three locations. To understand the dropdown, you need to keep all three in your head at once, and to change it, you need to touch all three.

Your app might have dozens of interactive components like that. The 'concerns' we separated weren't independent at all, because each one depended on the others.

The HTML structure constrained what CSS selectors could target, and the CSS classes determined what JavaScript could toggle, so you couldn’t make sense of any one piece without the other two.

We’d organized code around technology boundaries, but most of the complexity was in features: the dropdown, the modal, the form validation, and the data table. Each feature was spread across the technology boundaries, which led to what’s called shotgun surgery, where a single change needed updates in several places.

Single-page applications made this worse. Once JavaScript was rendering the HTML itself, there was no real line between “structure” and “behavior” any more, because scripts generated the markup instead of enhancing a static document. Keeping the two in separate files just split up what was really a single concern.

Separating by Feature

With the component model, we kept separation of concerns, but we started separating by feature instead of by technology.

In a component, everything related to the dropdown, including its structure, its styles, and its behavior, is together in one place. The dropdown component has high cohesion because all the pieces that make it work are grouped together, and it has low coupling because it doesn’t depend on unrelated parts of the system.

Things that change together still stay together, but the boundary is in a different place. When the dropdown needs a change, you edit one component instead of three files scattered across your project.

We stopped separating HTML from CSS and JavaScript, and started separating the dropdown, the modal, the navigation, and the user profile from each other.

Your day-to-day work gets simpler too, because whether a designer wants the dropdown to look different or a product manager wants it to support keyboard navigation, you open the same dropdown component.

The principle was really about organizing code so that each unit has a single reason to change, and keeping languages apart was only one way of doing that. When we were building documents, the technology was a reasonable guide to why a piece of code would need to change. Now that we build applications out of components, the feature is a better guide.

Understanding Concerns

We often talk about “separation of concerns” as if concerns were fixed categories, but a concern is just anything you might need to think about separately.

Whether something counts as a concern depends on context:

  • In one context, “all the styling” is a concern
  • In another, “everything about the dropdown” is the concern
  • Neither is wrong, because they’re two ways of dividing up the same code

In your frontend application, the concerns most worth separating are usually things like:

  • Business logic versus presentation logic
  • Data fetching versus data display
  • Application state versus UI state
  • Global concerns versus local concerns

These separations apply whatever technologies you’re using.

Separation Within Components

Components might be the right top-level boundary, but that doesn’t mean everything inside a component should be mixed together. Components have their own internal concerns, and those are worth separating too.

Internal Component Concerns

Presentation vs Logic

A component that displays a user profile and a component that manages user profile data are doing different things.

A common way to split them:

  • Smart components handle data fetching, state, and business logic
  • Dumb components render the data that’s passed to them as props
  • Pure components always produce the same output from the same input

This makes your components easier to test and reuse.

Side Effects vs Pure Code

Network requests, local storage, analytics, and timers all interact with the outside world, which makes them different from pure computation.

A common way to handle them:

  • Code with side effects is kept in dedicated places where it’s easy to spot
  • Framework mechanisms like useEffect, watchers, or lifecycle hooks manage the side effects
  • Side effects stay separate from render logic

That way, the code is easier to test and reason about.

A component that renders a list of products is doing two things, and the difference between them is subtler. It’s handling the generic problem of rendering a list, which is infrastructure, and the specific problem of how products should look, which is domain. If you separate the two, you can reuse the list-rendering logic across many domains, and the domain experts on your team can focus on how products are displayed without reimplementing list management.

None of these internal separations need different file types. They’re logical separations, so you decide how to organize the code conceptually, and then express that with whatever your framework provides.

The Boundaries That Appear at Scale

As your application grows, new concerns appear, and things that were easy to manage in a small codebase get hard to manage in a large one.

State management is probably the biggest of these, because you have to decide where state is kept and how changes to it propagate. Those decisions get urgent once dozens of components need access to shared data. A centralized state store helps, because it keeps the data your app has separate from the components that use it, so you can debug and test how data flows without involving the UI rendering.

Routing and navigation are another boundary, because deciding which URL shows which view is a separate job from building the views themselves. When routing is its own concern, you can configure and test it separately, the views don’t need to know about URLs, and the router doesn’t need to know about rendering details.

Data fetching and caching increasingly get their own architectural layer. When a library manages server state separately from client state, that’s separation of concerns too, because data from the server is owned somewhere else and might be stale, and data created locally isn’t.

Once there are multiple teams or applications, you also have to separate code by who owns it. Which team owns which code, and where are the contracts between teams?

Microfrontends and workspace structures are two ways teams can turn those ownership lines into boundaries in the architecture.

Recognizing Good Separation

I’ve found a few practical questions useful for checking whether the concerns in your code are separated well.

Questions That Reveal Separation Quality

Each of these questions is about cohesion and coupling, and whether parts of your code can change independently, and none of them mention technology types.

Can you change one thing without changing others? If changing a feature means you have to update code scattered across the codebase, your concerns aren’t separated enough. If you have to duplicate logic that already exists elsewhere to add a new feature, that logic is probably mixed in with code it should be separate from.

Can you explain what each part does in one sentence? If you need “and” after “and” to describe a module, it probably has multiple concerns. “This component displays user profiles and manages authentication and handles routing” describes three concerns in one component.

Can you test each part in isolation? When concerns are well separated, you can test them independently. If you have to set up elaborate mocks of unrelated things just to test one part, your boundaries are in the wrong places.

Can different people work on different parts without constant coordination? When the code is separated well, people can work in parallel. If everyone’s changes conflict because everything depends on everything, your architecture doesn’t have clear boundaries.

A Living Principle

We still rely on separation of concerns in modern frontend development, and we’ve gotten more precise about how we apply it.

In the classical web model, we separated code by technology because that made sense for documents, and for teams organized by specialism. With components, we separate by feature, because that makes more sense for applications and for teams organized by product area.

Both approaches apply the same principle, and you can do either of them well or badly.

A component that mixes unrelated concerns is as much of a problem as a stylesheet that styles unrelated pages. In both cases, the problem is usually where the boundaries were drawn, more than how much code is kept together.

You have to work out what the concerns in your system are, what changes together, what should be independent, and where the boundaries belong. There’s no universal way to divide things up, because it depends on your application and your team. A startup building fast might draw different boundaries than an enterprise managing decades of code, and a solo developer has different coordination needs than a hundred-person team.

The questions stay the same, though, and if you keep asking them, your architecture should be easier to maintain. When I find myself changing three files for one feature, I check whether I’ve separated along the wrong axis. When a module gets too big or its tests become painful to write, I look at whether it’s picked up multiple concerns.

Summary

Separation of concerns has changed in modern frontend development in a few ways:

  1. We stopped separating HTML, CSS, and JavaScript, and started separating features from each other
  2. Things that change together should stay together, whatever languages they’re written in
  3. Each component groups related concerns and stays independent of the other components
  4. Inside a component, it still helps to keep presentation separate from logic, and pure code separate from side effects
  5. As your app grows, state management, routing, and data fetching become separate concerns
  6. Your concerns are well separated when you can change one thing without touching many others

Related Content:

External Resources:

Next Foundation · 14 min read

Performance as a Foundation

Performance belongs in the architecture from day one, instead of being bolted on after users start complaining.

Read the next Foundation →

Newsletter

A monthly email
from Den Odell.

Behind-the-scenes thinking on frontend patterns, site updates, and more.

No spam. Unsubscribe anytime.