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.
In this article
-
Creational: 1. Singleton · 2. Factory Method · 3. Builder
-
Structural: 4. Adapter · 5. Decorator
-
Behavioral: 6. Observer · 7. Strategy
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.
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.
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.
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.
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.
// 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.
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.
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.
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
| Pattern | Family | Use it when… | You've seen it in |
|---|---|---|---|
| Singleton | Creational | Exactly one shared instance must exist | Spring beans, Go sync.Once |
| Factory Method | Creational | The concrete type depends on config or input | sql.Open, Calendar.getInstance() |
| Builder | Creational | An object has many optional parts or needs validation | StringBuilder, OkHttp, Lombok |
| Adapter | Structural | You must fit a foreign API into your interface | InputStreamReader, http.HandlerFunc |
| Decorator | Structural | You want to stack extra behavior without subclassing | Java I/O streams, HTTP middleware |
| Observer | Behavioral | Many parts must react to one event | DOM events, EventEmitter, RxJS |
| Strategy | Behavioral | An algorithm must be swappable at runtime | Sort comparators, Passport.js |
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.