Saved
Foundation

Logical Separation of Concerns

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

architecture
components
design-patterns
Read
12 mins

By Den Odell

Deep Dive
TL;DR

TL;DR

Separation of concerns didn’t die in modern frontend—it evolved. We stopped separating HTML, CSS, and JavaScript and started separating features from each other. The principle remains: things that change together stay together; things that change independently are separated.

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

The separation of concerns is the most important principle in software design.

— Edsger W. Dijkstra

The Evolution of a Principle

We used to keep HTML, CSS, and JavaScript in separate files. Structure in one place, presentation in another, behaviour in a third. This was separation of concerns, and it was gospel.

Then React arrived and put them all together. Vue followed. Svelte. Every modern framework embraced co-location—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. Both missed the deeper question: What are we actually trying to separate, and why?

Separation of concerns didn't die. It transformed. Understanding that transformation is essential for making good architectural decisions in modern frontend development.

The principle remains as relevant as ever—we’re just applying it to different boundaries than we used to.

What Is Separation of Concerns?

Before we can discuss how separation of concerns has evolved, we need to understand what the principle actually means.

Separation of concerns is the practice of organising 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.

The benefits are substantial. Maintainability improves because you know where to look when something needs changing. Testability improves because isolated concerns can be tested in isolation. Replaceability improves because swapping one implementation for another doesn’t require rewriting unrelated code. Collaboration improves because different people can work on different concerns without constant coordination.

The fundamental insight beneath all of this is deceptively simple: things that change together should stay together; things that change independently should be separated.

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

But here’s where it gets interesting. “Related” isn’t an objective property. It depends on what you’re optimising 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?

That question—what makes things “related” enough to group together—is at the heart of how separation of concerns has evolved in 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. JavaScript handled behaviour. Three technologies, three concerns, 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 behaviour. 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 mapped perfectly to document-centric development:

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

The directory structures reflected this thinking. A styles/ folder held all CSS. A scripts/ folder held 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 all worked beautifully—for documents. For server-rendered pages with modest interactivity. For teams organised by technology specialism. For websites.

Then we started building applications.

Where the Model Strained

Applications are not documents. 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/behaviour.

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

Now multiply that across dozens of interactive components. The 'concerns' we separated weren't actually independent—they were intimately related.

The HTML structure constrained what CSS selectors could target. The CSS classes determined what JavaScript could toggle. Each piece was incomprehensible without the others.

We’d optimised for technology boundaries, but that wasn’t where the real complexity lived. 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.

The rise of single-page applications intensified this strain. When JavaScript became responsible for rendering the HTML itself, the separation between “structure” and “behaviour” collapsed. The markup wasn’t a static document enhanced by scripts; it was generated by scripts. Keeping them in separate files meant separating things that were actually the same concern.

What We’re Actually Separating

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. Everything related to the dropdown—its structure, its styles, its behaviour—lives together in one place. The dropdown component has high cohesion: all the pieces that make it work are grouped. It has low coupling: it doesn’t depend on unrelated parts of the system.

This is the same principle, applied differently. Things that change together stay together. When the dropdown needs modification, you change one component, not three files scattered across the project.

Co-location isn’t the opposite of separation—it’s a different axis of separation. We stopped separating HTML from CSS from JavaScript. We started separating the dropdown from the modal from the navigation from the user profile. The boundaries moved from technology to domain.

Consider what this means for the daily work of development. When a designer requests a change to how the dropdown looks, you open the dropdown component. When a product manager wants the dropdown to support keyboard navigation, you open the dropdown component. When a bug report mentions the dropdown, you open the dropdown component. The mental model is simpler: one feature, one location.

This shift reveals what the principle was always about. Separation of concerns isn’t specifically about keeping languages apart. It’s about organising code so that each unit has a single reason to change. 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

The terminology here matters. 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 different ways of carving the same space

The real concerns in a frontend application aren’t structure, presentation, and behaviour. They’re things like:

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

These separations matter regardless of what technologies you’re using.

Separation Within Components

Recognising that components are the right top-level boundary doesn’t mean everything inside a component should be undifferentiated soup. 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, business logic
  • Dumb components: handle rendering, given data as props
  • Pure components: same inputs → same outputs
  • Easy 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
  • Use framework patterns (useEffect, watchers, lifecycle hooks)
  • Keep side effects separate from render logic
  • 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 organised conceptually, expressed through whatever mechanisms the framework provides.

The Boundaries That Matter at Scale

As applications grow, new separation concerns emerge. What’s manageable in a small codebase becomes critical in a large one.

State management is perhaps the most significant. Where does state live? Who can modify it? How do changes propagate? These questions become urgent when dozens of components need access to shared data. Centralised state stores separate 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. 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. Views don’t need to know about URLs; routers don’t need to know about rendering.

Data fetching and caching increasingly get their own architectural layer. Libraries that manage server state separately from client state are implementing separation of concerns—recognising that 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 organisational concerns map to architectural boundaries. Microfrontends, module federation, and workspace structures all create separation at the organisational level.

Recognising Good Separation

How do you know if you’ve separated concerns well? The tests are practical.

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 awkwardly cohabiting.

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 logical organisation, about cohesion and coupling, about change and independence.

A Living Principle

Separation of concerns didn’t weaken in modern frontend development. We just 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 organised by specialism. The component model separates by feature because that’s what makes sense for applications, for rich interactivity, for teams organised 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. The failure mode isn’t “too much co-location” or “too much separation”—it’s putting the boundaries in the wrong places.

The discipline is asking the right questions. What are the concerns in this system? What changes together? What should be independent? Where do 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.

But the questions remain constant, and they’re the questions that lead to maintainable architecture. When you find yourself changing three files for one feature, ask whether you’ve separated by the wrong axis. When you find a module growing unwieldy, ask whether it’s accumulated multiple concerns. When tests become painful to write, ask whether the boundaries make isolation difficult.

Separation of concerns is how we manage complexity. We’ll always need it. We’re just always 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 is eternal. The application is contextual. Understanding the difference is what makes you effective.


Related Content:

External Resources:

Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.