DEV Community

Cover image for Demystifying Architectural Patterns with JavaScript
Alvison Hunter Arnuero | Front-End Web Developer
Alvison Hunter Arnuero | Front-End Web Developer

Posted on • Updated on

Demystifying Architectural Patterns with JavaScript

Howdy Folks! Welcome to the world of architectural patterns! These design blueprints are the secret sauce behind crafting scalable, maintainable, and efficient software systems. Whether you're a fledgling developer or a seasoned pro, understanding these architectural patterns can significantly enhance your software engineering prowess.

In this article, we will delve into a variety of architectural patterns, using JavaScript with TypeScript to exemplify each one. Fear not, for we'll keep things as clear as a bell, ensuring even those with basic programming knowledge can appreciate these concepts.

1. Static Content Hosting Pattern

The Static Content Hosting Pattern is all about serving static assets like HTML, CSS, and JavaScript from a Content Delivery Network (CDN) to reduce server load and improve performance.

// Load a static image from a CDN
const imageUrl = "https://alvisonhunter.com/imgs/codecrafterslabslogo.png";
const imgElement = document.createElement("img");
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
Enter fullscreen mode Exit fullscreen mode

2. Event-Driven Pattern

The Event-Driven Pattern revolves around event handling. It decouples components by allowing them to communicate through events, promoting scalability and maintainability.

// Event-driven example using the DOM
const button = document.getElementById("myButton");
button.addEventListener("click", (event) => {
  console.log("Button clicked!");
});
Enter fullscreen mode Exit fullscreen mode

3. Peer-to-Peer Pattern

In the Peer-to-Peer Pattern, nodes or peers communicate directly with each other, without a central server. It's perfect for applications like file sharing.

// Simulating peer-to-peer file sharing
class Peer {
  constructor(public name: string) {}
  sendFileTo(peer: Peer, file: string) {
    console.log(`${this.name} is sending ${file} to ${peer.name}`);
  }
}

const alvison = new Peer("Alvison");
const declan = new Peer("Declan");

alvison.sendFileTo(declan, "pythonCode.txt");
Enter fullscreen mode Exit fullscreen mode

4. Publisher-Subscriber Pattern

The Publisher-Subscriber Pattern enables components to subscribe to and receive updates from publishers, enhancing modularity and reducing dependencies.

class Publisher {
  private subscribers: Function[] = [];

  subscribe(subscriber: Function) {
    this.subscribers.push(subscriber);
  }

  publish(message: string) {
    this.subscribers.forEach((subscriber) => subscriber(message));
  }
}

const myPublisher = new Publisher();

function mySubscriber(message: string) {
  console.log(`Received: ${message}`);
}

myPublisher.subscribe(mySubscriber);
myPublisher.publish("CCL really rocks!");
Enter fullscreen mode Exit fullscreen mode

5. Sharding Pattern

Sharding involves breaking a dataset into smaller, more manageable pieces that can be distributed across multiple servers or databases for improved performance.

// Sharding users into different databases
const userShard1 = new Map<string, string>();
const userShard2 = new Map<string, string>();

function addUser(userId: string, data: string) {
  const shard = userId.charCodeAt(0) % 2 === 0 ? userShard1 : userShard2;
  shard.set(userId, data);
  return shard.get(userId);
}

const res = addUser("aHunter2023", "Frontend Web Developer");
console.log(res)
Enter fullscreen mode Exit fullscreen mode

6. Circuit Breaker Pattern

The Circuit Breaker Pattern prevents a system from repeatedly trying to perform an operation that is likely to fail, preserving system integrity.

class CircuitBreaker {
  private isOpen = false;

  attempt(operation: Function) {
    if (this.isOpen) {
      console.log("Circuit is open. Operation not attempted.");
    } else {
      try {
        operation();
      } catch (error) {
        console.error("Operation failed.", error);
        this.isOpen = true;
      }
    }
  }

  reset() {
    this.isOpen = false;
  }
}

const breaker = new CircuitBreaker();
breaker.attempt(() => {
  console.log("Risky operation here, anything from nacatamal to cacao")
});
Enter fullscreen mode Exit fullscreen mode

7. Pipe-Filter Pattern

The Pipe-Filter pattern is like assembling a pipeline of filters to process and transform data in stages. It's an excellent choice for data manipulation tasks.

interface Filter {
  process(data: string): string;
}

class FilterPipeline {
  filters: Filter[] = [];

  addFilter(filter: Filter) {
    this.filters.push(filter);
  }

  process(data: string) {
    for (const filter of this.filters) {
      data = filter.process(data);
    }
    return data;
  }
}

class UppercaseFilter implements Filter {
  process(data: string) {
    return data.toUpperCase();
  }
}

class ReverseFilter implements Filter {
  process(data: string) {
    return data.split('').reverse().join('');
  }
}

const pipeline = new FilterPipeline();
pipeline.addFilter(new UppercaseFilter());
pipeline.addFilter(new ReverseFilter());

const result = pipeline.process('Alvison');
console.log(result); // Output should be 'NOSIVLA'
Enter fullscreen mode Exit fullscreen mode

8. Primary-Replica Pattern

The Primary-Replica pattern deals with data replication and distribution. It allows for load balancing and fault tolerance, where multiple replicas of the same data are maintained.

class PrimaryReplicaManager {
  primary: string;
  replicas: string[] = [];

  setPrimary(data: string) {
    this.primary = data;
  }

  addReplica(data: string) {
    this.replicas.push(data);
  }

  getPrimary() {
    return this.primary;
  }

  getReplicas() {
    return this.replicas;
  }
}

const manager = new PrimaryReplicaManager();
manager.setPrimary('Primary Data');
manager.addReplica('Replica 1');
manager.addReplica('Replica 2');
manager.addReplica('Replica 3');

console.log(manager.getPrimary()); 
console.log(manager.getReplicas());
Enter fullscreen mode Exit fullscreen mode

9. Model-View-Controller Pattern

The Model-View-Controller (MVC) pattern separates the application into three interconnected components: Model (data), View (user interface), and Controller (business logic). This separation enhances maintainability and testability.

class Model {
  data: string;

  constructor() {
    this.data = [];
  }

  addData(item: string) {
    this.data.push(item);
  }
}

class View {
  render(data: string) {
    console.log(`${data.length} Rendered View:`, data);
  }
}

class Controller {
  model: Model;
  view: View;

  constructor(model: Model, view: View) {
    this.model = model;
    this.view = view;
  }

  update(data: string) {
    this.model.addData(data);
    this.view.render(this.model.data);
  }
}

const model = new Model();
const view = new View();
const controller = new Controller(model, view);

controller.update('Alvison Hunter');
controller.update('Declan Hunter');
controller.update('Liam Hunter');
Enter fullscreen mode Exit fullscreen mode

10. Interpreter Pattern

The Interpreter Pattern is like having a multilingual guide in a foreign land. It helps you parse and interpret a language's grammar or expression and execute it. This is particularly useful when dealing with domain-specific languages.

interface Interpreter {
  interpret(expression: string): number;
}

class SimpleInterpreter implements Interpreter {
  interpret(expression: string): number {
    const tokens = expression.split(' ');
   expression.split(" ").forEach((element, i) => {
  if (i > 0) {
    tokens[i] = parseInt(element);
  }
});

let result = 0;

if (tokens[0] === "add") {
  result = tokens[1] + tokens[2];
} else if (tokens[0] === "subtract") {
  result = tokens[1] - tokens[2];
} else if (tokens[0] === "multiply") {
  result = tokens[1] * tokens[2];
} else if (tokens[0] === "divide") {
  result = tokens[1] / tokens[2];
}
    return result;
  }
}

const interpreter = new SimpleInterpreter();
console.log('Interpreter Pattern Result:');
console.log('Addition:', interpreter.interpret('add 10 2')); // outputs 12
console.log('Subtraction:', interpreter.interpret('subtract 30 20')); //outputs 10
console.log('Multiplication:', interpreter.interpret('multiply 10 2')); // outputs 20
console.log('Division:', interpreter.interpret('divide 10 2')); // outputs 5
Enter fullscreen mode Exit fullscreen mode

11. Client-Server Pattern

The Client-Server Pattern is akin to the classic waiter-customer relationship in a restaurant. The server provides services, and clients consume them. This architecture separates concerns, improving scalability and maintenance.

// Server
class Server {
  handleRequest(request: string): string {
    return `Received and processed: ${request}`;
  }
}

// Client
class Client {
  sendRequest(request: string, server: Server): string {
    return server.handleRequest(request);
  }
}

const server = new Server();
const client = new Client();

const response = client.sendRequest('Serve me, Server!', server);
console.log('Client-Server Pattern Response:', response);
Enter fullscreen mode Exit fullscreen mode

12. Layered Pattern

The Layered Pattern is analogous to assembling a delicious sandwich. It organizes your application into layers, each handling specific tasks, making it easier to maintain and scale your code.

// Presentation Layer
class PresentationLayer {
  render(data: string): string {
    return `Rendered: ${data}`;
  }
}

// Business Logic Layer
class BusinessLogicLayer {
  process(data: string): string {
    return `Processed: ${data}`;
  }
}

// Data Access Layer
class DataAccessLayer {
  retrieveData(): string {
    return 'Data from the database';
  }
}

// Main Application
const presentationLayer = new PresentationLayer();
const businessLayer = new BusinessLogicLayer();
const dataAccessLayer = new DataAccessLayer();

const data = dataAccessLayer.retrieveData();
const processedData = businessLayer.process(data);
const renderedData = presentationLayer.render(processedData);

console.log('Layered Pattern Result:', renderedData);
Enter fullscreen mode Exit fullscreen mode

13. Microservices Pattern

The Microservices Pattern is like having a buffet of small dishes at a grand feast. It breaks down a monolithic application into smaller, independent services that can be developed and deployed separately.

Here's a simple JavaScript TypeScript example using HTTP communication:

// Microservice 1
const microservice1 = () => {
  return 'Microservice 1 is serving.';
};

// Microservice 2
const microservice2 = () => {
  return 'Microservice 2 is serving.';
};

console.log('Microservices Pattern Result 1:', microservice1());
console.log('Microservices Pattern Result 2:', microservice2());
Enter fullscreen mode Exit fullscreen mode

14. Command Query Responsibility Segregation Pattern

The Command Query Responsibility Segregation (CQRS) Pattern is like separating the roles of chefs and waitstaff in a restaurant. It segregates the reading (query) and writing (command) operations, optimizing your system for performance and scalability.

Here's a straightforward JavaScript TypeScript example:

class OrderService {
  private orders: string[] = [];

  addOrder(order: string): void {
    this.orders.push(order);
  }

  getOrders(): string[] {
    return this.orders;
  }
}

const orderService = new OrderService();
orderService.addOrder('Order 1');
orderService.addOrder('Order 2');
const orders = orderService.getOrders();

console.log('CQRS Pattern Orders:', orders);
Enter fullscreen mode Exit fullscreen mode

In conclusion, understanding these architectural patterns is essential for architects and developers alike. They serve as fundamental tools in your software design toolbox, helping you create more efficient, maintainable, and scalable applications. By using JavaScript with TypeScript, we've made these concepts approachable, even for those with basic programming knowledge.

Remember, it's not just about the code; it's about creating resilient, scalable, and maintainable systems. So go ahead, explore these patterns, and start building better software today! Happy coding!

❤️ Enjoyed the article? Your feedback fuels more content.
💬 Share your thoughts in a comment.
🔖 No time to read now? Well, Bookmark for later.
🔗 If it helped, pass it on, dude!

Top comments (0)