MJH
Portfolio
Blog

Marouane El Moujahid

Full Stack Developer & Software Engineering Student based in Morocco, specialized in building modern web applications, backend systems, and DevOps solutions using React, NestJS, and Docker.

Quick Links

  • About Me
  • Projects
  • Blog
  • Resume

Connect

Keyboard Shortcuts

⌘K
Search
T
Toggle theme
2026 Marouane ElMoujahid. All rights reserved.
Built withusing Next.js & Tailwind
HomeBlogThe 7 Design Patterns Every Developer Should Know
Clean Code & ArchitectureClean CodeArchitectureSoftware Engineering

The 7 Design Patterns Every Developer Should Know

Marouane El Moujahid
October 24, 2026
8 min read
0 comments
On this page
  1. Why These 7 Design Patterns?
  2. 1. Singleton Pattern: One Shared Instance
  3. 2. Factory Method Pattern: Create Objects Without Naming the Class
  4. 3. Builder Pattern: Construct Complex Objects Step by Step
  5. 4. Adapter Pattern: Make Incompatible Interfaces Work Together
  6. 5. Decorator Pattern: Add Behavior Without Subclassing
  7. 6. Observer Pattern: Notify Many Objects of One Change
  8. 7. Strategy Pattern: Swap Algorithms at Runtime
  9. Cheat sheet
  10. When not to use a pattern
  11. Frequently Asked Questions
  12. What are the 3 types of design patterns?
  13. Which design patterns are used most?
  14. Are design patterns still relevant in 2026?
  15. Do I need to learn all 23 GoF patterns?
  16. Conclusion
  17. Sources

These are the 7 design patterns every developer should know: Singleton, Factory Method, Builder, Adapter, Decorator, Observer and Strategy. Each section below explains the problem the pattern solves, shows a short TypeScript example, and points to where you already use it in Java, Go, Node.js and the browser.

The 7 Design Patterns Every Developer Should Know

In this article

  • Why these 7 design patterns?

  • Creational: 1. Singleton · 2. Factory Method · 3. Builder

  • Structural: 4. Adapter · 5. Decorator

  • Behavioral: 6. Observer · 7. Strategy

  • Cheat sheet · When not to use a pattern · FAQ · Conclusion

Why These 7 Design Patterns?

Design patterns are named, reusable answers to recurring design problems. The canonical catalog is Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides — the "Gang of Four" (GoF). It was published on 21 October 1994, describes 23 patterns, and has sold over 500,000 copies (Wikipedia).

The 23 split into three families (Coursera):

  • Creational (5) — how objects get created.
  • Structural (7) — how classes and objects are composed into larger structures.
  • Behavioral (11) — how objects communicate and share responsibility.
GoF Design Pattern Overview

The seven below were picked because they show up on nearly every "must-know" list (e.g. ByteByteGo, DEV) and because you already use them daily through standard libraries: three creational, two structural, two behavioral.

Two principles from the book's first chapter explain almost all of them: program to an interface, not an implementation, and favor object composition over class inheritance. Keep both in mind as you read.

Code samples are in TypeScript so they read cleanly in any language.

1. Singleton Pattern: One Shared Instance

Intent: guarantee a class has exactly one instance and give everyone a single access point to it.

Problem it solves: some resources must be shared, not duplicated — a config object, a logger, a connection pool. Creating several copies wastes resources or produces inconsistent state.

Singleton Pattern Diagram
class Config {
  private static instance: Config | null = null;
  private constructor(private readonly values: Record<string, string>) {}

  static get(): Config {
    if (!Config.instance) {
      Config.instance = new Config({ dbUrl: process.env.DB_URL ?? "" });
    }
    return Config.instance;
  }

  value(key: string) { return this.values[key]; }
}

Config.get() === Config.get(); // true — same object every time

Where you already use it: Spring beans are singletons by default; Go code typically builds one with sync.Once; a Node.js module is cached after its first require/import, so exporting an instance behaves like a singleton.

Watch out: Singleton is the most criticized GoF pattern. It is global state in disguise: it hides dependencies, makes unit tests share state, and needs care under concurrency (the lazy check above is not thread-safe in Java or C++). Erich Gamma himself wanted to drop it from the book in a later revision, but the authors did not agree (Wikipedia). Prefer creating one instance at startup and passing it in through dependency injection — you keep "only one" without the global access.

2. Factory Method Pattern: Create Objects Without Naming the Class

Intent: create objects without the caller naming the exact class; a method decides which concrete type to return.

Problem it solves: new StripeGateway() scattered across 40 files means adding PayPal touches 40 files. Centralize the decision, and callers depend only on the interface.

Factory Design Pattern
interface PaymentGateway { charge(amountMAD: number): Promise<string>; }

class StripeGateway implements PaymentGateway { async charge(a: number) { return `stripe:${a}`; } }
class CmiGateway    implements PaymentGateway { async charge(a: number) { return `cmi:${a}`; } }

function createGateway(provider: "stripe" | "cmi"): PaymentGateway {
  switch (provider) {
    case "stripe": return new StripeGateway();
    case "cmi":    return new CmiGateway();
  }
}

const gateway = createGateway(process.env.PAYMENT_PROVIDER as "stripe" | "cmi");
await gateway.charge(250); // caller never knows the concrete class

The strict GoF version uses a subclass that overrides a create…() method; the simple factory function above is the form you will write most often.

Where you already use it: Java's Calendar.getInstance() and List.of(), Go's sql.Open("postgres", dsn) (the driver name picks the implementation), document.createElement("div") in the browser.

Watch out: a factory for a class with one implementation is ceremony. Introduce it when a second variant actually appears.

3. Builder Pattern: Construct Complex Objects Step by Step

Intent: construct a complex object step by step, separating how it is built from what it ends up being.

Problem it solves: the "telescoping constructor" — new Request(url, "POST", null, null, 5000, true, headers). Nobody remembers what the fifth argument means, and every optional field multiplies constructors.

factory design pattern
class HttpRequest {
  constructor(
    readonly url: string, readonly method: string,
    readonly headers: Record<string, string>, readonly body?: string,
    readonly timeoutMs = 5000,
  ) {}
}

class RequestBuilder {
  private method = "GET";
  private headers: Record<string, string> = {};
  private body?: string;
  private timeoutMs = 5000;
  constructor(private url: string) {}

  post(json: unknown) { this.method = "POST"; this.body = JSON.stringify(json); return this; }
  header(k: string, v: string) { this.headers[k] = v; return this; }
  timeout(ms: number) { this.timeoutMs = ms; return this; }
  build() {
    if (this.method === "POST" && !this.body) throw new Error("POST needs a body");
    return new HttpRequest(this.url, this.method, this.headers, this.body, this.timeoutMs);
  }
}

const req = new RequestBuilder("https://api.example.com/orders")
  .post({ bookId: 42, qty: 1 })
  .header("Authorization", "Bearer …")
  .timeout(3000)
  .build();

Where you already use it: Java's StringBuilder, OkHttp's Request.Builder, Lombok's @Builder, query builders such as Knex or Prisma's chained calls, Rust's std::process::Command.

Watch out: in TypeScript or JavaScript, an options object (fetch(url, { method, headers })) often does the job with less code. Reach for a Builder when construction needs validation, ordering, or produces an immutable result.

4. Adapter Pattern: Make Incompatible Interfaces Work Together

Intent: let two classes with incompatible interfaces work together by wrapping one in a translator — a travel plug for code.

Problem it solves: your app expects a Notifier with send(to, text), but the SMS vendor's SDK exposes dispatchMessage({ msisdn, body, senderId }). You cannot change the SDK, and you should not leak its shape into your domain.

Adapter Design Pattern
// What your application depends on
interface Notifier { send(to: string, text: string): Promise<void>; }

// Third-party SDK you can't modify
class VendorSmsClient {
  async dispatchMessage(p: { msisdn: string; body: string; senderId: string }) { /* … */ }
}

// The adapter: implements your interface, delegates to theirs
class SmsNotifier implements Notifier {
  constructor(private client: VendorSmsClient, private senderId: string) {}
  async send(to: string, text: string) {
    await this.client.dispatchMessage({ msisdn: to, body: text, senderId: this.senderId });
  }
}

Swap vendors tomorrow and only SmsNotifier changes. This is the core idea behind Ports & Adapters (hexagonal architecture).

Where you already use it: Java's InputStreamReader (adapts a byte stream to a character reader) and Arrays.asList(), ORMs adapting one API to many SQL dialects, http.HandlerFunc in Go (adapts a plain function to the http.Handler interface).

Watch out: an adapter should only translate. Once it starts adding business logic, it is becoming a service and should be named and tested as one.

5. Decorator Pattern: Add Behavior Without Subclassing

Intent: add behavior to an object at runtime by wrapping it in another object with the same interface. Wrappers stack.

Problem it solves: you want a repository with caching, logging and retries — in any combination. Subclassing gives you CachedLoggedRetryingRepo and a class explosion. Decorators compose instead.

Decorrator Design Pattern
interface BookRepo { findById(id: number): Promise<Book | null>; }

class PostgresBookRepo implements BookRepo {
  async findById(id: number) { /* SELECT … */ return null; }
}

class CachedBookRepo implements BookRepo {
  private cache = new Map<number, Book | null>();
  constructor(private inner: BookRepo) {}
  async findById(id: number) {
    if (!this.cache.has(id)) this.cache.set(id, await this.inner.findById(id));
    return this.cache.get(id)!;
  }
}

class LoggedBookRepo implements BookRepo {
  constructor(private inner: BookRepo) {}
  async findById(id: number) {
    const t = Date.now();
    const r = await this.inner.findById(id);
    console.log(`findById(${id}) ${Date.now() - t}ms`);
    return r;
  }
}

// Compose at wiring time — order matters
const repo: BookRepo = new LoggedBookRepo(new CachedBookRepo(new PostgresBookRepo()));

Decorator vs Adapter: both wrap. An Adapter changes the interface; a Decorator keeps it and adds behavior.

Where you already use it: Java I/O (new BufferedReader(new InputStreamReader(new FileInputStream(f)))), Express/Koa and Go HTTP middleware (func(next http.Handler) http.Handler), Python's @functools.lru_cache.

Watch out: deep stacks are hard to debug, and wrapper order changes results (logging outside the cache logs hits; inside, it logs only misses). Keep the wiring in one place.

6. Observer Pattern: Notify Many Objects of One Change

Intent: when one object (the subject) changes, every object subscribed to it (the observers) is notified automatically. It is the publish/subscribe pattern at object level (Wikipedia).

Problem it solves: when an order is paid, you must send an email, decrement stock and update analytics. If OrderService calls all three directly, it knows too much and grows with every new side effect.

Observer Design Pattern
type Listener<T> = (event: T) => void;

class EventBus<T> {
  private listeners = new Set<Listener<T>>();
  subscribe(fn: Listener<T>) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
  publish(event: T) { for (const fn of this.listeners) fn(event); }
}

type OrderPaid = { orderId: string; email: string; items: { bookId: number; qty: number }[] };
const orderPaid = new EventBus<OrderPaid>();

orderPaid.subscribe(e => sendReceipt(e.email, e.orderId));
orderPaid.subscribe(e => e.items.forEach(i => decrementStock(i.bookId, i.qty)));
const unsubscribe = orderPaid.subscribe(e => track("order_paid", e.orderId));

// OrderService only knows the bus
orderPaid.publish({ orderId: "A-1029", email: "reader@example.com", items: [{ bookId: 42, qty: 1 }] });

Where you already use it: addEventListener in the DOM, Node's EventEmitter, RxJS observables, React/Vue reactivity and state stores, Go channels fanning out events. Scaled across services, the same idea becomes message brokers such as Kafka or RabbitMQ.

Watch out: forgotten subscriptions leak memory — always keep and call the unsubscribe function. Notification order is not guaranteed, and long chains of events make control flow hard to trace.

7. Strategy Pattern: Swap Algorithms at Runtime

Intent: define a family of interchangeable algorithms behind one interface, and pick one at runtime.

Problem it solves: a calculateShipping() that is a 200-line if/else over carrier, city and weight. Each new rule risks breaking the others. Strategy turns each branch into its own small, testable unit.

Strategy Design Pattern
interface ShippingStrategy { cost(weightKg: number): number; }

const standard: ShippingStrategy = { cost: kg => 30 + kg * 5 };
const express:  ShippingStrategy = { cost: kg => 60 + kg * 8 };
const pickup:   ShippingStrategy = { cost: () => 0 };

class Checkout {
  constructor(private shipping: ShippingStrategy) {}
  setShipping(s: ShippingStrategy) { this.shipping = s; }
  total(subtotal: number, weightKg: number) { return subtotal + this.shipping.cost(weightKg); }
}

const checkout = new Checkout(standard);
checkout.total(200, 1.2);   // 236
checkout.setShipping(pickup);
checkout.total(200, 1.2);   // 200

In languages with first-class functions, a strategy is often just a function you pass in — no class needed. This is one reason Peter Norvig found that 16 of the 23 GoF patterns are simplified or disappear in dynamic languages like Lisp (Wikipedia).

Where you already use it: the comparator in array.sort((a, b) => a.price - b.price) or Java's Comparator, Passport.js authentication strategies, Go's sort.Slice less function, pluggable compression or retry/backoff policies.

Watch out: with two branches that never change, a plain if is clearer. Strategy pays off when variants keep being added or must be chosen from configuration.

Cheat sheet

PatternFamilyUse it when…You've seen it in
SingletonCreationalExactly one shared instance must existSpring beans, Go sync.Once
Factory MethodCreationalThe concrete type depends on config or inputsql.Open, Calendar.getInstance()
BuilderCreationalAn object has many optional parts or needs validationStringBuilder, OkHttp, Lombok
AdapterStructuralYou must fit a foreign API into your interfaceInputStreamReader, http.HandlerFunc
DecoratorStructuralYou want to stack extra behavior without subclassingJava I/O streams, HTTP middleware
ObserverBehavioralMany parts must react to one eventDOM events, EventEmitter, RxJS
StrategyBehavioralAn algorithm must be swappable at runtimeSort comparators, Passport.js
7 Design Pattern Cheet Sheet

When not to use a pattern

Patterns are vocabulary, not goals. Paul Graham argued that repeated patterns in code can signal that the language's abstractions are not strong enough, and the GoF book itself warns that highly parameterized software is harder to understand (Wikipedia).

A practical rule: write the simplest code first, and refactor toward a pattern when the problem it solves actually appears — a second payment provider, a third notification channel, a fifth shipping rule. Naming the pattern in a comment or PR then tells reviewers your intent in one word.

Frequently Asked Questions

What are the 3 types of design patterns?

The Gang of Four grouped their 23 patterns into creational (5 patterns, object creation), structural (7, composing classes and objects) and behavioral (11, communication between objects).

Which design patterns are used most?

Singleton, Factory Method, Builder, Adapter, Decorator, Observer and Strategy appear in almost every major framework and standard library, which is why this guide covers them.

Are design patterns still relevant in 2026?

Yes, as shared vocabulary. Modern languages make some patterns trivial (a Strategy is often just a function), but the problems they solve still appear in every codebase.

Do I need to learn all 23 GoF patterns?

No. Learn these seven well, then add Facade, Proxy, Command and Composite. Learn the rest when you meet the problem they solve.

Conclusion

Master these seven and you can read most framework source code: creation (Singleton, Factory Method, Builder), wrapping (Adapter, Decorator) and behavior (Observer, Strategy). Next steps worth learning are Facade, Proxy, Command and Composite, plus Dependency Injection and Repository, which the GoF authors themselves said they would consider adding today.

Sources

  • Design Patterns (book) — Wikipedia
  • Gang of Four Design Patterns — Coursera
  • 18 Key Design Patterns Every Developer Should Know — ByteByteGo
  • Design Patterns Every Developer Should Know — DEV Community
  • 5 Must-Know Design Patterns — DEV Community
Previous post20 PySpark Commands Every Spark Developer Should Know
Marouane El Moujahid

Written by Marouane El Moujahid

Software engineering student at FST Mohammedia and full stack developer (React, NestJS, Docker). Open to PFE internships and freelance projects.

About meGet in touch

Keep reading

PFE Internship in Morocco 2027 – Software & Data Engineer

Career & Growth · Oct 9, 2026

PFE Internship in Morocco 2027 – Software & Data Engineer

Final-year software engineering student seeking a 2027 PFE internship in Morocco. Go, C++, .NET, React, data pipelines. Available end of January 2027.

Software Engineering · Mar 5, 2026

What Happens When Your Server Loses Power Mid-Processing?

Building a crash-resilient storage engine for a custom message broker — from first principles.

Test Category · Nov 29, 2025

Benchmarking C++ Trees and Hash Tables at Scale: What Actually Happens at 60 Million Ops

I pushed RBTree, AVLTree, BST, std::set, std::map, and unordered_set through 60 million operations. Sorted inserts. Sorted searches. Sorted deletes. This is what these structures really do under load — not in theory, but in code

Comments (0)

Leave a Comment

No comments yet. Be the first to comment!

On this page

  1. Why These 7 Design Patterns?
  2. 1. Singleton Pattern: One Shared Instance
  3. 2. Factory Method Pattern: Create Objects Without Naming the Class
  4. 3. Builder Pattern: Construct Complex Objects Step by Step
  5. 4. Adapter Pattern: Make Incompatible Interfaces Work Together
  6. 5. Decorator Pattern: Add Behavior Without Subclassing
  7. 6. Observer Pattern: Notify Many Objects of One Change
  8. 7. Strategy Pattern: Swap Algorithms at Runtime
  9. Cheat sheet
  10. When not to use a pattern
  11. Frequently Asked Questions
  12. What are the 3 types of design patterns?
  13. Which design patterns are used most?
  14. Are design patterns still relevant in 2026?
  15. Do I need to learn all 23 GoF patterns?
  16. Conclusion
  17. Sources