Separation of Concerns
“Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one’s subject matter in isolation for the sake of its own consistency.” — Edsger W. Dijkstra
Context
Any non-trivial system has multiple reasons to change: the business rules evolve, the user interface gets redesigned, the database is replaced, the deployment strategy shifts. Separation of concerns is the principle of organizing a system so that each part addresses one of these reasons, and only one. It operates at the architectural scale and is one of the oldest principles in software design.
The idea is simple. The discipline of applying it consistently isn’t.
Problem
How do you keep a system changeable when different aspects of it evolve at different rates, for different reasons, driven by different people?
Forces
- Mixing concerns in the same module means a change to one concern risks breaking another.
- Separating concerns too aggressively creates indirection and fragmentation. The code for a single feature ends up scattered across many files.
- Some concerns are hard to separate cleanly (logging, error handling, and security tend to cut across everything).
- Different stakeholders care about different concerns and should be able to work without stepping on each other.
Solution
Identify the distinct reasons your system might change. Business logic is one concern. Presentation is another. Data persistence, authentication, error handling, configuration — each is a concern. Organize your code so that each concern lives in its own module or component, behind its own boundary.
The classic example is the Model-View-Controller pattern: the model handles business logic, the view handles presentation, and the controller handles input. Each can change independently. But separation of concerns isn’t limited to MVC. It applies at every level, from splitting a function that does two things into two functions, to splitting a monolith into services.
The test is simple: when a requirement changes, how many places do you need to edit? If a change to the pricing logic requires touching the database schema, the API handlers, and the email templates, those concerns are not separated. If it requires editing only the pricing module, they are.
In agentic coding, separation of concerns determines how precisely you can scope an agent’s work. “Update the pricing logic” is a clear instruction when pricing lives in one place. It’s a dangerous instruction when pricing is entangled with half the codebase. The agent either misses changes or makes ones it shouldn’t.
How It Plays Out
A web application mixes HTML generation, database queries, and business rules in the same functions. Every change is a risky, time-consuming affair. The team gradually refactors: business rules move into a domain layer, database access into a repository layer, and HTML into templates. Changes get smaller, safer, and faster.
An AI agent is tasked with updating the email notification format. In a system with separated concerns, the agent edits the email templates and the formatting logic — nothing else. In a tangled system, the agent finds that email content is generated inline within the order processing code, mixed with business logic and database calls. The agent either touches too much or too little.
When you notice a pull request touching many unrelated files for a single logical change, that is a smell: concerns are not well separated. Use that signal to guide refactoring priorities.
“Move the email content generation out of the order processing code. Put the email templates and formatting logic in their own module. The order processor should call a send_notification function, not build HTML.”
Consequences
Separation of concerns makes systems easier to understand (each piece has one job), easier to change (changes are localized), and easier to test (you can test each concern in isolation). It supports team autonomy, since different concerns can be owned by different people or agents.
The cost is structural overhead. Separate concerns need explicit interfaces between them. Cross-cutting concerns (like logging or authorization) don’t fit neatly into any one box and require special patterns. Over-separation can be as harmful as under-separation: if you split every concern into its own file in its own directory, working with the codebase becomes a scavenger hunt.
Related Patterns
- Implemented via: Module, Component, Boundary — these are the structural mechanisms for separating concerns.
- Measured by: Cohesion — a well-separated concern is a cohesive module.
- Reduces: Coupling — separated concerns are loosely coupled by design.
- Applied by: Decomposition — decomposing a system along concern boundaries.
- Refined by: Architecture — architecture decides which separations matter most.
- Violated by: Big Ball of Mud – concerns are scattered instead of isolated.