Hernando Abella
Chapter 5SoCClean ArchitectureBest Practices

Separation of Concerns: Building Cleaner Backend Systems

Learn how to organize Node.js applications into distinct layers, reduce coupling, improve testability, and build systems that scale without turning into tangled logic.

πŸ“– 19 min readπŸ§‘β€πŸ’» Hernando AbellaπŸ“˜ Node.js Design Patterns
StackNode.jsJavaScriptTypeScriptExpressNestJSFastify

As backend systems grow in complexity, business logic starts mixing with database queries, HTTP handling, and external API calls. Over time, this creates tightly coupled systems that are difficult to test and even harder to evolve.

The principle of Separation of Concerns (SoC) provides a clear solution: organize your system so that each part of the application is responsible for a single concern or responsibility.


What Is Separation of Concerns?

A software system should be divided into distinct sections, where each section addresses a separate concern. A "concern" refers to a specific aspect of functionality, such as:

HTTP requestsBusiness logicData persistenceAuthenticationLoggingExternal APIs

The Problem: Mixed Responsibilities

A common pattern in beginner Node.js applications is combining everything into a single function.

javascript Β· bad-approach.js
app.post("/orders", async (req, res) => {
  const { userId, items } = req.body;

  // Validation
  if (!userId || !items) {
    return res.status(400).json({ error: "Invalid data" });
  }

  // Business logic
  const total = items.reduce((sum, item) => sum + item.price, 0);

  // Database logic
  const order = await db.orders.insert({
    userId,
    items,
    total,
  });

  // External service
  await emailService.sendOrderConfirmation(userId, order.id);

  // Logging
  console.log("Order created:", order.id);

  res.json(order);
});

⚠️ Problems: Hard to test, hard to reuse, difficult to debug, violates SRP, changes in one area risk breaking others.


The Solution: Layered Architecture

🌐 Request
↓
Controller Layer
HTTP handling
↓
Service Layer
Business logic
↓
Repository Layer
Data access
↓
External Services
Integrations
↓
✨ Response

1. Controller Layer

javascript Β· controller.js
class OrderController {
  constructor(orderService) {
    this.orderService = orderService;
  }

  async createOrder(req, res) {
    try {
      const order = await this.orderService.createOrder(req.body);
      res.json(order);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

2. Service Layer (Business Logic)

javascript Β· service.js
class OrderService {
  constructor(orderRepository, emailService) {
    this.orderRepository = orderRepository;
    this.emailService = emailService;
  }

  async createOrder({ userId, items }) {
    if (!userId || !items) {
      throw new Error("Invalid data");
    }

    const total = this.calculateTotal(items);

    const order = await this.orderRepository.create({
      userId,
      items,
      total,
    });

    await this.emailService.sendOrderConfirmation(userId, order.id);

    return order;
  }

  calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
  }
}

3. Repository Layer (Data Access)

javascript Β· repository.js
class OrderRepository {
  constructor(db) {
    this.db = db;
  }

  async create(order) {
    return this.db.orders.insert(order);
  }

  async findById(id) {
    return this.db.orders.findOne({ id });
  }

  async update(id, data) {
    return this.db.orders.update({ id }, data);
  }
}

4. External Service Layer

javascript Β· external-service.js
class EmailService {
  async sendOrderConfirmation(userId, orderId) {
    // Email sending logic
    console.log(`Sending email to ${userId} for order ${orderId}`);
  }
}

// Dependency injection setup
const orderRepository = new OrderRepository(db);
const emailService = new EmailService();
const orderService = new OrderService(orderRepository, emailService);
const orderController = new OrderController(orderService);

app.post("/orders", (req, res) => orderController.createOrder(req, res));

Benefits of Separation of Concerns

πŸ”§

Easier Maintenance

Each layer has a clear responsibility. Changes in one area do not affect others.

βœ…

Better Testability

Test each layer independently without HTTP or database setup.

♻️

Improved Reusability

Business logic can be reused across HTTP APIs, jobs, CLI tools, and serverless functions.

πŸ“ˆ

Scalability

New layers can be introduced without rewriting existing code.

πŸ“

Clear Code Structure

New developers quickly understand where logic belongs.

Example: Testing Business Logic Independently

javascript Β· test-service.js
// No need for HTTP or database setup
test("calculates order total correctly", () => {
  const service = new OrderService(null, null);
  
  const total = service.calculateTotal([
    { price: 10 },
    { price: 20 },
    { price: 30 }
  ]);
  
  expect(total).toBe(60);
});

The Four Layers

🌐

Controller

HTTP request/response handling

βš™οΈ

Service

Business logic and rules

πŸ—„οΈ

Repository

Database operations

πŸ”Œ

External Service

Third-party integrations


Common Mistakes

πŸ—οΈ

Over-Engineering Layers

Not every small project needs deep layering. Too many abstractions can make simple systems harder to understand.

🩸

Anemic Services

Services that contain no logic, just pass-through calls, add unnecessary structure without value.

πŸ”€

Mixing Logic in Controllers

Controllers should remain thin. Business logic belongs in services.

πŸ‘‘

God Services

A single service handling everything (validation, business logic, database, APIs) violates SoC.


Separation of Concerns in Modern Node.js Frameworks

Express

Encourages manual separation into routes, controllers, services, and repositories.

NestJS

Built around SoC with modules, controllers, providers (services), and dependency injection.

Fastify

Encourages plugin-based separation where each plugin handles a specific concern.

typescript Β· nestjs-example.ts
// NestJS - Built around Separation of Concerns
@Module({
  controllers: [OrderController],
  providers: [OrderService, OrderRepository],
})
export class OrderModule {}

@Controller("orders")
export class OrderController {
  constructor(private orderService: OrderService) {}

  @Post()
  async create(@Body() createOrderDto: CreateOrderDto) {
    return this.orderService.create(createOrderDto);
  }
}

@Injectable()
export class OrderService {
  constructor(private orderRepository: OrderRepository) {}

  async create(data: CreateOrderDto) {
    // Business logic here
    return this.orderRepository.save(data);
  }
}

When to Break Separation of Concerns

Strict separation is not always ideal. Sometimes combining logic improves clarity:

  • Simple scripts or one-off utilities
  • Small prototypes or MVPs
  • Internal tools with limited scope

Practical Guidelines

βœ… Apply SoC when:

  • Your application has multiple features or domains
  • Code is reused across multiple parts of the system
  • Complexity is growing
  • Testing is becoming difficult

⚠️ Avoid overusing when:

  • The project is small and simple
  • Logic is unlikely to change
  • Abstractions add more complexity than clarity

πŸ’‘ Good rule of thumb: Start simple, add layers when complexity justifies them. Don't build a cathedral for a doghouse.


Conclusion

Separation of Concerns is one of the most powerful principles in backend engineering. By ensuring that each part of a Node.js system has a clear and focused responsibility, developers can build applications that are easier to maintain, scale, and test.

When combined with principles like DRY, KISS, and Composition over Inheritance, SoC becomes the foundation of clean backend architecture.

Good software design is not about adding layers of complexityβ€”it is about organizing complexity so that it remains manageable as systems evolve.


πŸ“˜ From the Book

Node.js Design Patterns

Master Separation of Concerns, SOLID principles, clean architecture, and essential design patterns for building scalable Node.js applications.

🎯 SoCπŸ—οΈ Clean Architecture⚑ Best PracticesπŸ”§ Layered Design
Get it on Amazon β†’
Node.js Design Patterns book cover