Separation of Concerns
Organize code so that each section addresses a distinct concern, minimizing overlap and entanglement.
Overview
Separation of Concerns (SoC) is a design principle for dividing a program into distinct sections, each handling a separate “concern”—a set of information or behavior that affects the code. When concerns are separated, you can understand, modify, and test each part independently. When they’re tangled together, changes become risky and comprehension becomes difficult.
SoC operates at every level: functions, classes, modules, layers, and services. The goal is always the same—isolate things that change for different reasons so that changing one doesn’t require understanding or modifying the others.
Key Principles
Identify Distinct Concerns
A concern is anything that matters to stakeholders: business logic, data persistence, user interface, security, logging, error handling, configuration. Each deserves its own home in the codebase.
Common concerns in applications:
- Presentation - How data is displayed to users
- Business logic - Rules and processes that define the domain
- Data access - How data is persisted and retrieved
- Infrastructure - Cross-cutting concerns like logging, caching, security
Layered Architecture
A classic application of SoC is organizing code into layers, where each layer handles a specific concern and only communicates with adjacent layers:
┌─────────────────────────┐
│ Presentation Layer │ UI, API endpoints, views
├─────────────────────────┤
│ Application Layer │ Use cases, orchestration
├─────────────────────────┤
│ Domain Layer │ Business logic, entities
├─────────────────────────┤
│ Infrastructure Layer │ Database, external services
└─────────────────────────┘
Modular Boundaries
Within each layer, further separate concerns into modules with clear boundaries. A module should have a focused purpose and expose a well-defined interface.
// Each module handles a distinct concern
import { validateOrder } from './validation';
import { calculatePricing } from './pricing';
import { processPayment } from './payment';
import { sendConfirmation } from './notifications';
async function processOrder(order: Order): Promise<void> {
validateOrder(order);
const total = calculatePricing(order);
await processPayment(order.customerId, total);
await sendConfirmation(order);
}Example
Consider a function that mixes multiple concerns:
// Before: Concerns are tangled
async function submitForm(formData: FormData): Promise<void> {
// Validation concern
if (!formData.email || !formData.email.includes('@')) {
document.getElementById('error').textContent = 'Invalid email';
return;
}
// UI concern
document.getElementById('submit').disabled = true;
document.getElementById('loading').style.display = 'block';
// Data transformation concern
const user = {
email: formData.email.toLowerCase().trim(),
name: `${formData.firstName} ${formData.lastName}`,
};
// Persistence concern
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
// More UI concern
document.getElementById('loading').style.display = 'none';
// Error handling mixed with UI
if (!response.ok) {
document.getElementById('error').textContent = 'Server error';
return;
}
// Navigation concern
window.location.href = '/success';
}After separating concerns:
// Validation module
function validateUserForm(formData: FormData): ValidationResult {
if (!formData.email?.includes('@')) {
return { valid: false, error: 'Invalid email' };
}
return { valid: true };
}
// Data transformation module
function createUserFromForm(formData: FormData): User {
return {
email: formData.email.toLowerCase().trim(),
name: `${formData.firstName} ${formData.lastName}`,
};
}
// API client module
async function createUser(user: User): Promise<ApiResult> {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user),
});
return { success: response.ok };
}
// UI controller - orchestrates other concerns
async function submitForm(formData: FormData): Promise<void> {
const validation = validateUserForm(formData);
if (!validation.valid) {
showError(validation.error);
return;
}
showLoading(true);
const user = createUserFromForm(formData);
const result = await createUser(user);
showLoading(false);
if (result.success) {
navigateTo('/success');
} else {
showError('Server error');
}
}Common Mistakes
Mixing layers - Database queries in UI code, HTML in business logic, business rules in persistence layer.
God objects - Classes that do everything violate SoC completely. If a class name includes “Manager,” “Processor,” or “Handler” without specificity, it might be doing too much.
Over-separation - Creating too many tiny modules adds indirection without benefit. Separate things that change for different reasons, not every possible distinction.
Leaky abstractions - When lower layers leak into higher ones (e.g., SQL concepts in business logic), separation breaks down.
Related Concepts
- Single Responsibility - SoC applied to classes
- Coupling and Cohesion - Well-separated concerns have low coupling
- Dependency Injection - Helps maintain separation between layers
- Small Functions - SoC at the function level
References
- Dijkstra, E. W. (1974). “On the role of scientific thought.” (Origin of the term)
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design.
- Bass, L., Clements, P., & Kazman, R. (2012). Software Architecture in Practice.