Software Design

Separation of Concerns (SoC): Lessons from dissecting an 800-line component

You’ve probably seen code where everything lives in a single 800-line view file.

3 min read
Cover image for Separation of Concerns (SoC): Lessons from dissecting an 800-line component

You’ve probably seen code where everything lives in a single 800-line view file.

The view code renders the screen, calls APIs, calculates discounts, formats dates, and sends analytics events. To change anything, you have to read all 800 lines.

The principle this kind of file violates is Separation of Concerns. The final installment of this series on development principles covers it.

Separation of Concerns (SoC) means dividing a program into distinct concerns so that each part handles only one concern.

A “concern” is a type of task the program needs to handle. How to render the UI, where to retrieve data, and how to apply business rules are different concerns.

The term was coined by computer scientist Dijkstra, who described it this way in a 1974 paper.

Focus your thinking on one aspect at a time. That is the only effective thinking technique I know.

The human mind cannot handle multiple concerns at once. So code should keep one concern in each piece.


A familiar example: HTML, CSS, and JS

If you’ve done web development, you’ve already been using separation of concerns every day.

Technology Concern
HTML Structure — what exists
CSS Presentation — how it looks
JavaScript Behavior — how it responds

If you pile style attributes and onclick into HTML tags as in the old days, you have to dig through HTML just to change a button’s color. Split things into three files, and the designer only needs CSS while the markup developer only needs HTML.

The backend controller–service–repository structure and separating views, view models, and data layers on iOS follow the same principle. MVC and layered architecture are ultimately patterns that formalize separation of concerns.


Let’s dissect that 800-line view

Let’s split the 800-line view from earlier by concern.

// Before: All concerns in one file
struct ProductPage: View {
    // Concern 1: Fetch data (network calls, loading, error handling 150 lines)
    // Concern 2: Business rules (discount and inventory calculations 200 lines)
    // Concern 3: Analytics events (tap tracking 100 lines)
    // Concern 4: Render the screen (body 350 lines)
}
// After: Give each concern its own home
ProductStore(id:)               // Fetch data
calcDiscount(product, user)     // Business rules (pure functions)
Tracker.track("product")        // Analytics events

struct ProductPage: View {      // Render the screen only
    @State private var store: ProductStore

    var body: some View {
        let price = calcDiscount(store.product, user)
        ...
    }
}

Once separated, the benefits are clear. When discount policies change, you only need to inspect calcDiscount, and because this function is pure and independent of the UI, it is easy to test. When the API specification changes, you only need to update ProductStore. Compared with reading all 800 lines, the scope of change is reduced to one-tenth.

After splitting it up, the scope of change became one-tenth
After splitting it up, the scope of change became one-tenth

How far should you split things?

The hardest question in separation of concerns is “Where should the boundary go?” Split too aggressively, and you end up navigating a maze across dozens of files.

My criterion is the reason for change. It is the same question discussed in the SOLID article on SRP.

Do these two pieces of code change for the same reason, at the same time?

Discount policies and screen layouts change for different reasons, so we separate them. A button and its tap handler, on the other hand, almost always change together, so we keep them together. SwiftUI bringing structure, style, and behavior together in one file at the view level is not contradictory from this perspective. The boundaries follow units that change together, not technology categories.

The six principles ultimately pointed to the same place
The six principles ultimately pointed to the same place

Wrapping up the series

Here is the key takeaway from separation of concerns.

  • Make each piece of code handle only one concern. The human mind cannot multitask.
  • In practice, draw boundaries around units that change together, not around technology categories.
  • Over-separation creates a maze. Splitting things up must not become the goal.

That wraps up the development principles series: KISS (keep it simple), DRY (keep knowledge in one place), YAGNI (build only what is needed now), SOLID (limit the impact of change), least surprise (make behavior predictable), and separation of concerns (one thing at a time).

Looking back, all six principles point to the same place. Code is read by people, not computers, and good code is code that is easy to change. Even if you forget the names, remember that one sentence.