Why I Use Design Patterns in My Frontend and Backend Projects


In software development, design patterns are proven solutions to common problems. They’re like blueprints created by experienced developers to solve recurring challenges - so you don’t have to reinvent the wheel every time.

Here’s why I always keep them in mind when building both frontend and backend code:

  • They speed up development by providing ready-made approaches.
  • They improve code readability — other devs recognize familiar structures.
  • They help maintain scalability and flexibility in your app.
  • They reduce bugs and technical debt by encouraging solid architecture.

Some of the most common design patterns I use

1. Singleton

Ensures a class has only one instance and provides a global point of access to it.

Use case: Managing a centralized store or configuration.

class Logger {
  private static instance: Logger;
  private constructor() {}
  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
  log(msg: string) {
    console.log(msg);
  }
}

2. Observer (Publish-Subscribe) Pattern

The Observer pattern, also known as Publish-Subscribe (Pub/Sub), is a behavioral design pattern that allows one object (the subject) to maintain a list of dependents (the observers) and automatically notify them of any state changes, usually by calling one of their methods.

This pattern is incredibly useful for decoupling components or modules, enabling them to communicate without tight dependencies.

Why use Observer?

  • It allows loose coupling between components.
  • Observers can subscribe/unsubscribe dynamically.
  • Great for event-driven programming.
  • Simplifies communication in complex UI apps or systems.

Common use cases

  • Event buses to send and listen for events.
  • Reactive frameworks (like Vue’s reactivity or React’s state updates).
  • Real-time applications (e.g., chat apps, notifications).
  • Logging systems or monitoring.

Simple JavaScript example of Observer pattern

Here’s a minimal implementation of a Pub/Sub system:

class EventBus {
  constructor() {
    this.events = {};
  }

  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  unsubscribe(eventName, callback) {
    if (!this.events[eventName]) return;
    this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
  }

  publish(eventName, data) {
    if (!this.events[eventName]) return;
    this.events[eventName].forEach(callback => callback(data));
  }
}

Usage:

const bus = new EventBus();

function onUserLogin(data) {
  console.log(`User logged in: ${data.name}`);
}

// Subscribe to event
bus.subscribe('user:login', onUserLogin);

// Publish event somewhere in your app
bus.publish('user:login', { name: 'Bartek' });

// Later, unsubscribe if needed
bus.unsubscribe('user:login', onUserLogin);

Observer in Vue (reactivity example)

Vue’s reactivity system is a built-in observer pattern under the hood. When reactive data changes, Vue automatically updates the DOM or triggers watchers.

import { reactive, watch } from 'vue';

const state = reactive({
  count: 0,
});

watch(() => state.count, (newVal) => {
  console.log('Count changed:', newVal);
});

state.count++; // This triggers the watcher, logging "Count changed: 1"

Summary

The Observer pattern helps organize communication in your apps cleanly and flexibly. It’s a must-know, especially for frontend developers dealing with event-driven UIs or backend developers building event systems.

3. Factory Pattern

The Factory pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. In simpler terms, it lets you create objects without exposing the instantiation logic to the client, and you can decide which object type to create at runtime.


Why use Factory?

  • Encapsulates object creation logic.
  • Supports open/closed principle - easy to add new types without changing existing code.
  • Useful when your app needs to create different objects based on input or configuration.
  • Improves code maintainability and readability.

Common use cases

  • Creating UI components dynamically based on user input or config.
  • Creating different service instances depending on environment (e.g., mock or real API clients).
  • Managing plugins or strategy objects.

Simple JavaScript example of Factory pattern

Suppose you want to create different types of notifications: EmailNotification, SMSNotification, or PushNotification. The Factory decides which one to instantiate based on input:

class EmailNotification {
  send(message) {
    console.log(`Sending email: ${message}`);
  }
}

class SMSNotification {
  send(message) {
    console.log(`Sending SMS: ${message}`);
  }
}

class PushNotification {
  send(message) {
    console.log(`Sending push notification: ${message}`);
  }
}

class NotificationFactory {
  static create(type) {
    switch(type) {
      case 'email':
        return new EmailNotification();
      case 'sms':
        return new SMSNotification();
      case 'push':
        return new PushNotification();
      default:
        throw new Error('Unknown notification type');
    }
  }
}

// Usage:
const notification = NotificationFactory.create('sms');
notification.send('Hello from Factory!');

Factory pattern in React (dynamic components)

You can use Factory pattern to render different components based on props:

const components = {
  primary: (props) => <PrimaryButton {...props} />,
  secondary: (props) => <SecondaryButton {...props} />,
};

function ButtonFactory({ variant, ...props }) {
  const Component = components[variant] || components.primary;
  return <Component {...props} />;
}

// Usage
<ButtonFactory variant="secondary" onClick={() => alert('Clicked!')} />;

Summary

Factory pattern is a powerful way to centralize and organize object/component creation. It helps keep your codebase scalable and easy to maintain when you have many related objects or components with similar interfaces.

4. Strategy Pattern

The Strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

In practice, you create different strategies (algorithms) that share the same interface, and at runtime you decide which strategy to use.


Why use Strategy?

  • Enables selecting algorithms dynamically at runtime.
  • Promotes open/closed principle - easy to add new algorithms without modifying existing code.
  • Simplifies code by avoiding complex conditional statements (e.g., long if-else or switch).
  • Improves testability by isolating algorithms.

Common use cases

  • Switching between different validation rules.
  • Selecting sorting algorithms based on data size or type.
  • Changing authentication methods (e.g., OAuth, JWT, API key).
  • Different pricing or discount strategies in e-commerce.

Simple JavaScript example of Strategy pattern

Imagine you have different sorting algorithms and want to switch between them:

class BubbleSortStrategy {
  sort(data) {
    // Simple bubble sort implementation
    const arr = [...data];
    for(let i = 0; i < arr.length; i++) {
      for(let j = 0; j < arr.length - i - 1; j++) {
        if(arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSortStrategy {
  sort(data) {
    if(data.length < 2) return data;
    const pivot = data[0];
    const lesser = data.slice(1).filter(x => x <= pivot);
    const greater = data.slice(1).filter(x => x > pivot);
    return [...this.sort(lesser), pivot, ...this.sort(greater)];
  }
}

class SortContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sort(data) {
    return this.strategy.sort(data);
  }
}

// Usage:
const data = [5, 3, 8, 1, 2];

const context = new SortContext(new BubbleSortStrategy());
console.log('Bubble Sort:', context.sort(data));

context.setStrategy(new QuickSortStrategy());
console.log('Quick Sort:', context.sort(data));

Strategy pattern in React (form validation example)

You can use Strategy to swap validation rules dynamically:

const validators = {
  email: (value) => /\S+@\S+\.\S+/.test(value) ? null : 'Invalid email',
  password: (value) => value.length >= 6 ? null : 'Password too short',
};

function validateField(type, value) {
  const validate = validators[type];
  return validate ? validate(value) : null;
}

// Usage in a form component
const error = validateField('email', 'test@example.com');

Summary

The Strategy pattern helps you manage interchangeable algorithms cleanly and flexibly. Instead of hardcoding logic, you can swap behavior easily, which makes your code more modular and maintainable.

5. Decorator Pattern

The Decorator pattern is a structural design pattern that allows you to add behavior to objects dynamically without altering their original structure. Instead of modifying the original object, you wrap it with a decorator that adds new functionality.


Why use Decorator?

  • Adds features to objects at runtime.
  • Avoids subclassing for every possible combination of behaviors.
  • Keeps code flexible and adheres to the open/closed principle.
  • Useful for cross-cutting concerns like logging, caching, or UI enhancements.

Common use cases

  • Enhancing UI components (e.g., adding borders, animations).
  • Adding logging or performance monitoring without changing business logic.
  • Wrapping data objects with validation or formatting.
  • Middleware in HTTP servers.

Simple JavaScript example of Decorator pattern

Imagine we have a simple logger, and we want to add timestamp functionality without modifying the original logger:

class Logger {
  log(message) {
    console.log(message);
  }
}

class TimestampLogger {
  constructor(logger) {
    this.logger = logger;
  }

  log(message) {
    const timestamp = new Date().toISOString();
    this.logger.log(`[${timestamp}] ${message}`);
  }
}

// Usage:
const logger = new Logger();
const decoratedLogger = new TimestampLogger(logger);

logger.log("Hello!");             // Output: Hello!
decoratedLogger.log("Hello!");    // Output: [2025-07-04T10:00:00.000Z] Hello!

Decorator in React (HOC example)

In React, Higher-Order Components (HOCs) are a common way to decorate components:

function withLogger(WrappedComponent) {
  return function(props) {
    console.log('Rendering component with props:', props);
    return <WrappedComponent {...props} />;
  };
}

// Usage:
const Button = (props) => <button {...props}>Click me</button>;
const ButtonWithLogger = withLogger(Button);

Here, withLogger adds logging behavior without modifying Button.

Summary

Decorator pattern lets you add responsibilities to objects or components dynamically and transparently. It’s perfect when you want to keep your original code clean but still add extra functionality.

Final thoughts

Design patterns are not strict rules - they’re tools. Knowing them lets you pick the right approach faster and communicate better with your team.

Remember: someone already solved your problem elegantly - why not use that solution?

Bartłomiej Nowak

Bartłomiej Nowak

Programmer

Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.

Recent Posts