Skip to main content
Saved
Foundation

Logical Separation of Concerns

Understanding the evolution from separating technologies to separating logical boundaries in component-based architectures.

Den Odell
By Den Odell Added
In this article
  1. TL;DR
  2. The Evolution of a Principle
  3. Defining Separation of Concerns
  4. The Classical Web Interpretation
  5. Benefits
  6. Why It Worked
  7. Limits of the Model
  8. Separating by Feature
  9. Understanding Concerns
  10. Separation Within Components
  11. Presentation vs Logic
  12. Side Effects vs Pure Code
  13. The Boundaries That Matter at Scale
  14. Recognizing Good Separation
  15. Questions That Reveal Separation Quality
  16. A Living Principle
  17. Summary
TL;DR

TL;DR

Separation of concerns still applies in modern frontend, but the boundaries have moved. We stopped separating HTML, CSS, and JavaScript and started separating features from each other. The principle is the same: things that change together stay together, and things that change independently are kept apart.

Core principles:

Separate by feature, not by technology: components group related concerns
Things that change together stay together: optimize for cohesion
Each unit has a single reason to change: minimize coupling
Separate presentation from logic: keep concerns distinct within components

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. This was separation of concerns, and it was gospel.

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

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

Separation of concerns is still here, applied to different boundaries. Knowing where those boundaries sit now helps you make better architectural decisions in modern frontend development.

The principle is as relevant as it was. We’re just applying it to different boundaries than we used to.

Defining Separation of Concerns

Separation of concerns is the practice of organizing code so that different responsibilities are handled by different parts of the system. Each part has a single focus, a single “concern,” and knows as little as possible about other parts. Changes to one concern should require changes only to the code responsible for that concern.

Maintainability improves because you know where to look when something needs changing. Testability improves because each concern can be tested on its own. Replacing an implementation gets easier because swapping one for another doesn’t require rewriting unrelated code. Collaboration improves because different people can work on different concerns without constant coordination.

The core idea is that things that change together should stay together, and things that change independently should be kept apart.

This is the principle of cohesion. Code is cohesive when related things are grouped and unrelated things are apart.

“Related” isn’t an objective property, though. It depends on what you’re optimizing for, who’s doing the work, and how your system evolves over time. Two pieces of code might be related in one sense (they use the same technology) and unrelated in another sense (they serve different features). Which relationship matters more?

The answer to that question, what makes things “related” enough to group together, is what has changed over the history of frontend development.

The Classical Web Interpretation

For most of the web’s history, we answered that question by technology type: HTML handled structure, CSS handled presentation, and JavaScript handled behavior. Three technologies meant three concerns and three sets of files.

This wasn’t arbitrary. The web was built as a document medium, 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 work without CSS (it just won’t be pretty). It should work without JavaScript (it just won’t be dynamic). Each layer enhances the layers below.

The Classical Separation Model

Benefits

  • Progressive enhancement: Each layer was optional, degrading gracefully
  • Team alignment: Designers worked in CSS, JavaScript specialists in scripts
  • Caching: CSS and JavaScript could be cached independently
  • Findability: Need to change button styles? Go to the stylesheet

Why It Worked

The model fit document-centric development well:

  • Content existed independently of presentation
  • Presentation existed independently of behavior
  • Server-rendered pages with modest interactivity
  • Teams organized by technology specialism

The directory structures reflected this thinking. A styles/ folder stored all CSS. A scripts/ folder stored all JavaScript. Template files lived 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: documents and server-rendered pages with modest interactivity, built by teams organized by technology specialism.

Then we started building applications.

Limits of the Model

Applications aren’t documents, and that changes how the code is best split up. A document presents information. An application manages state, responds to events, and presents dynamic interfaces that change moment to moment. The concerns in an application don’t map cleanly to structure/presentation/behavior.

Consider a dropdown menu. In the classical model, its markup lives in HTML, its appearance in CSS, and its open/close logic in JavaScript, spread across three files in three locations with three different contexts. To understand the dropdown, you need to keep all three in your head simultaneously, and to modify it, you need to touch all three.

Now multiply that across dozens of interactive components. 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. The CSS classes determined what JavaScript could toggle. None of the pieces made sense on its own.

We’d organized code around technology boundaries, but most of the complexity was in features: in the dropdown, the modal, the form validation, the data table. Each feature spread across the technology boundary, creating shotgun surgery: every change required updates in multiple 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 them in separate files meant separating things that were the same concern.

Separating by Feature

The component paradigm didn’t abandon separation of concerns. It asked a different question: What if we separated by feature instead of by technology?

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

It's the same principle applied to a different boundary: things that change together stay together. When the dropdown needs a change, you edit one component instead of three files scattered across the project.

Co-location still separates code, just along a different axis. We stopped separating HTML from CSS from JavaScript and started separating the dropdown from the modal from the navigation from the user profile.

That makes day-to-day work simpler. Whether a designer wants the dropdown to look different, a product manager wants it to support keyboard navigation, or a bug report mentions it, you open the same dropdown component. Each feature lives in one place.

The change shows what the principle was about all along: organizing code so that each unit has a single reason to change, rather than keeping languages apart. In document-centric development, technology type was a reasonable proxy for “reason to change.” In component-centric development, feature is a better proxy.

Understanding Concerns

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

What 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. They’re two ways of dividing up the same code

In a 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

Recognizing that components are the right top-level boundary doesn’t mean everything inside a component should be mixed together. Components themselves have internal concerns that benefit from separation.

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.

The separation:

  • Smart components handle data fetching, state, and business logic
  • Dumb components handle rendering with data passed as props
  • Pure components produce the same outputs from the same inputs

This makes components easier to test, reuse, and understand.

Side Effects vs Pure Code

Network requests, local storage, analytics, timers: these interactions with the outside world are conceptually different from pure computation.

The separation:

  • Make impurity visible and contained within dedicated patterns
  • Use framework mechanisms like useEffect, watchers, or lifecycle hooks
  • Keep side effects separate from render logic

This makes code easier to test and reason about.

Infrastructure versus domain is a subtler distinction. A component that renders a list of products is doing two things: handling the generic problem of rendering lists (infrastructure) and handling the specific problem of what products look like (domain). Separating these concerns lets you reuse list-rendering logic across many domains and lets domain experts focus on product display without reimplementing list management.

None of these internal separations require different file types. They’re logical separations, architectural choices about how code is organized conceptually, expressed through whatever mechanisms the framework provides.

The Boundaries That Matter at Scale

As applications grow, new separation concerns emerge, and what’s manageable in a small codebase becomes hard to manage in a large one.

State management is perhaps the most significant, raising questions about where state lives, who can modify it, and how changes propagate. These questions become urgent when dozens of components need access to shared data, and centralized state stores address this by separating the “what data exists” concern from the “what components use it” concern. This separation enables debugging, testing, and reasoning about data flow independently from UI rendering.

Routing and navigation form another boundary because the question of “what URL maps to what view” is separate from the views themselves. Extracting routing into a dedicated concern makes it configurable, testable, and visible, allowing views to remain ignorant of URLs while routers remain ignorant of rendering details.

Data fetching and caching increasingly get their own architectural layer. Libraries that manage server state separately from client state are applying separation of concerns, because data from the server has different characteristics (it’s cached, it might be stale, it’s owned elsewhere) than data created locally.

At the scale of multiple teams or applications, separation extends to ownership boundaries. Which team owns which code? Where are the contracts between teams?

These organizational concerns map to architectural boundaries. Microfrontends, module federation, and workspace structures all create separation at the organizational level.

Recognizing Good Separation

I’ve found that a few practical questions show whether concerns are separated well.

Questions That Reveal Separation Quality

Can you change one thing without changing others? If modifying a feature requires updates scattered across the codebase, concerns are insufficiently separated. If adding a new feature means duplicating logic that already exists elsewhere, something that should be separated is being mixed.

Can you explain what each part does in one sentence? If describing a module requires “and” after “and,” it probably has multiple concerns. “This component displays user profiles and manages authentication and handles routing” is three concerns packed into one component.

Can you test each part in isolation? Concerns that are well-separated can be tested independently. If testing one thing requires setting up elaborate mocks of unrelated things, the boundaries are wrong.

Can different people work on different parts without constant coordination? Separation enables parallel work. If everyone’s changes conflict because everything depends on everything, the architecture lacks clear boundaries.

None of these tests mention technology types. They’re about cohesion and coupling, and whether parts of the code can change independently.

A Living Principle

Modern frontend development still relies on separation of concerns, and we’ve learned to apply it more precisely.

The classical web model separated by technology because that’s what made sense for documents, for progressive enhancement, for teams organized by specialism. The component model separates by feature because that’s what makes sense for applications, for rich interactivity, for teams organized by product area.

Both are valid applications of the same underlying principle. Both can be done well or poorly.

A component that mixes unrelated concerns is as problematic as a stylesheet that handles unrelated pages. Either way, the usual problem is boundaries drawn in the wrong places, more than the amount of co-location or separation.

The work is in asking what the concerns in this system are, what changes together, what should be independent, and where the boundaries belong. These questions don’t have universal answers. They depend on your application, your team, your constraints. A startup building fast may draw different boundaries than an enterprise managing decades of code. A solo developer has different coordination needs than a hundred-person team.

The questions stay the same, though, and answering them is what leads to maintainable architecture. When I find myself changing three files for one feature, I ask whether I’ve separated by the wrong axis. When I find a module growing unwieldy, I ask whether it’s accumulated multiple concerns. When tests become painful to write, I ask whether the boundaries make isolation difficult.

Separation of concerns is how we manage complexity, and we’ll always need it, even as we continue learning better ways to apply it.

Summary

Separation of concerns has evolved in modern frontend development:

  1. Technology boundaries to feature boundaries: We moved from separating HTML/CSS/JS to separating features from each other
  2. Cohesion matters more than technology: Things that change together should stay together, regardless of what languages they use
  3. Components are separation boundaries: Each component encapsulates related concerns while staying independent from others
  4. Internal separation still matters: Within components, separate presentation from logic, pure code from side effects
  5. Scale introduces new boundaries: State management, routing, data fetching become separate concerns in large applications
  6. Good separation enables change: The test is whether you can modify one thing without touching many others

The principle stays the same while the way you apply it depends on context, and knowing which is which helps you draw better boundaries.


Related Content:

External Resources:

Newsletter

A Monthly Email
from Den Odell

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

No spam. Unsubscribe anytime.