Code Design: Chapter 3 - Architectural Patterns

Mehdi JaiMehdi Jai
4 min read

Introduction

Architectural patterns provide structured ways to design software systems, especially when dealing with complex applications. Each pattern addresses specific challenges related to scalability, maintainability, and flexibility, making them essential for building robust applications. Here are some foundational architectural patterns and their benefits.

Microservices Architecture

Decomposes applications into small, autonomous services, each handling a distinct piece of functionality.

Benefits: Facilitates independent scaling, deployment, and team ownership of individual services. However, it adds complexity to service orchestration and inter-service communication.

Microservices are separate applications (Either in separate machines or process). They communicate with each others via HTTP, Messaging Queues, publish/subscribe, etc..

// Simplified example of a microservices setup
// Imagine a simplified e-commerce system with separate services for Orders and Inventory.

// Order Service
import express from 'express';
const orderService = express();

orderService.post('/order', (req, res) => {
  // Logic to place an order
  res.send('Order placed');
});

// Inventory Service
import express from 'express';
const inventoryService = express();

inventoryService.post('/update-stock', (req, res) => {
  // Logic to update stock
  res.send('Stock updated');
});

Microservices can have separate databases dedicated for that service. The complexity and design of microservices depends on the business logic and complexity of the application inter-communications.

Event-Driven Architecture

Organizes applications around events that trigger actions in response to specific occurrences (e.g., user actions or system updates), making components decoupled and responsive to state changes.

Benefits: Increases scalability and flexibility, especially in complex workflows or real-time applications.

This kind of architecture is used in Real-time NoSQL databases like Firebase.

// Event-based system example
interface Event {
  eventSignature: string;
  data: any;
}

class EventBus {
  private listeners: { [signature: string]: Function[] } = {};

  subscribe(eventSignature: string, callback: Function) {
    if (!this.listeners[eventSignature]) this.listeners[eventSignature] = [];
    this.listeners[eventSignature].push(callback);
  }

  publish(event: Event) {
    const callbacks = this.listeners[event.eventSignature];
    if (callbacks) callbacks.forEach(callback => callback(event.data));
  }
}

const eventBus = new EventBus();

// It will only be triggred when it's published
eventBus.subscribe('userRegistered', (data) => {
  console.log(`Welcome email sent to ${data.email}`);
});

eventBus.publish({ type: 'userRegistered', data: { email: 'user@example.com' } });

Layered Architecture

Divides an application into layers (often Presentation, Business Logic, and Data) to organize responsibilities and promote a clear separation of concerns.

Benefits: Great for large applications where modularity and a clear structure are essential for maintenance and collaboration.

// Example of a layered architecture: Presentation Layer, Service Layer, and Data Access Layer.

// Data Access Layer
class UserRepository {
  findUserById(id: string) {
    return { id, name: 'Alice' }; // Database call simulated
  }
}

// Business Logic (Service) Layer
class UserService {
  constructor(private userRepository: UserRepository) {}

  getUserProfile(id: string) {
    return this.userRepository.findUserById(id);
  }
}

// Presentation Layer (It can be inside the ExpressJS router handler or separate view.)
const userService = new UserService(new UserRepository());
console.log(userService.getUserProfile('1')); // Output user profile data

Hexagonal Architecture (Ports & Adapters)

Structures systems around the core business logic, which communicates through "ports" (interfaces) to "adapters" (implementations), making the core independent from external dependencies.

Benefits: Improves flexibility, as dependencies like databases or third-party services can be swapped without impacting core functionality.

// Example of a Hexagonal Architecture setup with a port (interface) and adapter (implementation).

// Port (interface)
interface PaymentProcessor {
  processPayment(amount: number): boolean;
}

// Adapter (e.g., Stripe implementation)
class StripePaymentProcessor implements PaymentProcessor {
  processPayment(amount: number): boolean {
    console.log(`Processing $${amount} through Stripe`);
    return true; // Simulate successful payment
  }
}

// Core business logic
class PaymentService {
  constructor(private processor: PaymentProcessor) {}

  makePayment(amount: number) {
    return this.processor.processPayment(amount);
  }
}

const paymentService = new PaymentService(new StripePaymentProcessor());
paymentService.makePayment(100);

This architecture helps creating scalable and maintainable code, respecting also the SOLID principale.

When the app scales, you might encounter a moment where you have multiple Payment Processors depending on the client. You can then select the process depending on some configs. Or, a maybe change the payment processor by another new one (Let's say PayPal.). Then, All you will do is create the PayPalPaymentProcessor and replace it here:

- const paymentService = new PaymentService(new StripePaymentProcessor());
+ const paymentService = new PaymentService(new PayPalPaymentProcessor());

Summary

Architectural patterns address different needs based on project scale, complexity, and business goals. Microservices and Event-Driven Architectures support flexibility and scalability, particularly in distributed systems. Layered Architecture ensures clear boundaries between responsibilities, enhancing modularity and maintainability. Hexagonal Architecture promotes a highly adaptable system by making the core independent from external dependencies, which is especially valuable in systems with frequently changing requirements or dependencies. These patterns help developers design robust, efficient, and scalable systems adaptable to a range of requirements and evolving technologies.


Series Chapters

  1. Introduction

  2. Chapter 1 - Design Principles

  3. Chapter 2 - Development Methodologies

  4. Chapter 3 - Architectural Patterns

  5. Chapter 4 - Coding Standards and Best Practices

0
Subscribe to my newsletter

Read articles from Mehdi Jai directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mehdi Jai
Mehdi Jai

Result-oriented Lead Full-Stack Web Developer & UI/UX designer with 5+ years of experience in designing, developing and deploying web applications.