PushBackLog

Separation of Concerns

Soft enforcement Complete by PushBackLog team
Topic: architecture Topic: quality Skillset: any Technology: generic Stage: execution Stage: review

Separation of Concerns

Status: Complete
Category: Architecture
Default enforcement: Soft
Author: PushBackLog team


Tags

  • Topic: architecture, quality
  • Skillset: any
  • Technology: generic
  • Stage: execution, review

Summary

A system should be organised so that each distinct concern (business logic, data access, presentation, infrastructure) is handled in a distinct module or layer, with minimal coupling between them. Changes to one concern should rarely require changes to another.


Rationale

What a “concern” is

Edsger Dijkstra introduced the term “separation of concerns” in 1974. A concern is any aspect of a system that has a distinct purpose: displaying data, persisting data, validating business rules, handling HTTP requests, sending emails, logging. When two concerns are tangled in the same module or function, a change to either one requires understanding and risk-assessing the other.

Change propagation

The cost of a change in a well-separated system is local. The cost of a change in a tangled system is unpredictable. When business logic is embedded in SQL queries, “update the commission calculation” requires a database migration. When database logic is in a React component, “swap MySQL for PostgreSQL” requires frontend changes. Separation of concerns contains the blast radius of change.

Testability as a proxy for SoC

A module that is easy to test in isolation is almost always well-separated from its concerns. A unit test that requires a running database, a real HTTP server, and a configured email sender to test a discount calculation is a diagnostic: those concerns have not been separated.


Guidance

Common concern boundaries

ConcernResponsibility
Presentation / UIRendering, formatting, user interaction
Application / Use caseOrchestrating domain objects to fulfil a specific use case
Domain / Business logicBusiness rules, invariants, calculations, domain events
InfrastructureDatabase queries, HTTP calls, file I/O, queue publishing
ConfigurationEnvironment-specific settings, feature flags

The classic layered architecture institutionalises these concern boundaries. Hexagonal architecture (ports and adapters) takes it further by making the domain entirely independent of all infrastructure.

Practical signs of mixed concerns

  • SQL queries in UI components or controllers
  • Business rules in database stored procedures
  • HTTP error handling logic in domain objects
  • Logging and error reporting embedded in business logic methods
  • Formatting/presentation logic inside API response serialisers that also apply business transformations

Dependency direction

Separation of concerns is most effective when the dependency direction is intentional: lower-level concerns (infrastructure, persistence) depend on higher-level ones (domain, application), not vice versa. This is the Dependency Inversion Principle applied at the architectural level.


Examples

Mixed concerns (anti-pattern)

// An express route handler that queries the DB, applies business logic,
// formats a response, AND sends an email
app.post('/orders', async (req, res) => {
  const items = await db.query(`SELECT * FROM products WHERE id IN (${req.body.items.join(',')})`);
  let total = items.reduce((s, i) => s + i.price * req.body.quantities[i.id], 0);
  if (total > 1000) total *= 0.9;  // Business rule buried in HTTP handler
  const order = await db.query(`INSERT INTO orders ... RETURNING *`);
  await sendgrid.send({ to: req.body.email, subject: 'Order confirmed' });
  res.json({ orderId: order.id, total: total.toFixed(2) + ' GBP' }); // Formatting in handler
});

Five concerns in one function. Untestable without a real DB, a real email service, and a running HTTP server.

Separated concerns

// Controller: HTTP concern only
app.post('/orders', async (req, res) => {
  const command = CreateOrderCommand.fromRequest(req.body);
  const order = await orderService.createOrder(command);
  res.status(201).json(OrderResponse.from(order));
});

// Application service: orchestration
class OrderService {
  async createOrder(command: CreateOrderCommand): Promise<Order> {
    const items = await this.productRepo.findByIds(command.itemIds);
    const order = Order.create(items, command.quantities);  // Domain logic
    await this.orderRepo.save(order);
    await this.emailService.sendConfirmation(command.email, order);
    return order;
  }
}

// Domain: business logic only, no infrastructure
class Order {
  static create(items: Product[], quantities: Map<string, number>): Order { ... }
  applyVolumeDiscount(): void { if (this.total > 1000) this.total *= 0.9; }
}

Each concern can be tested in isolation. The domain class has no imports from infrastructure.


Anti-patterns

1. Database logic in UI components

SQL queries or ORM calls in React/Vue/Angular components. Frontend components should call API endpoints; the API layer handles persistence.

2. Business rules in HTTP controllers

Controllers translate HTTP to domain language and back. When discount rules, validation logic, and permission checks accumulate in controllers, they become untestable without a running HTTP server.

3. Formatting in domain models

Domain objects returning currency strings, formatted dates, or HTML. Domain models should work with raw values; formatting is a presentation concern.

4. Cross-cutting concerns repeated everywhere

Logging, tracing, timing, and audit logic copied into every method. These cross-cutting concerns are handled once with middleware, decorators, or aspect-oriented patterns — not inline in every function.



Part of the PushBackLog Best Practices Library. Suggest improvements →