Patternsintermediateschedule11 min read

You've Already Used Design Patterns — You Just Didn't Know It

You've Already Used Design Patterns — You Just Didn't Know It

Here's something that surprises most beginners: you've probably already invented a design pattern.

Have you ever written code where a bunch of different parts of your app need to know when some data changes — so you set up a list of "listeners" that get notified automatically? Congratulations, you reinvented the Observer pattern.

Have you ever created a function that picks which algorithm to use based on some condition — maybe one sorting method for small lists and another for big ones? That's the Strategy pattern.

Design patterns aren't ivory-tower computer science theory. They're names for solutions that developers kept rediscovering independently. In 1994, four authors — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (known as the "Gang of Four") — noticed that experienced programmers kept solving the same structural problems the same way. So they documented 23 of these recurring solutions in a book called Design Patterns: Elements of Reusable Object-Oriented Software.

Think of it this way: design patterns are to software what recipes are to cooking. Nobody "invented" the concept of sautéing vegetables — cooks worldwide figured it out independently. But giving it a name means one chef can say "sauté the onions" and every other chef immediately knows exactly what to do. That's the real power of patterns: shared vocabulary.

The Three Families of Patterns

The Gang of Four organized their 23 patterns into three categories based on what kind of problem they solve. Think of these as three different toolboxes.

hourglass_empty

Rendering diagram...

  • Creational patterns deal with making things. When constructing objects gets complicated — too many parameters, too many variations, or you need exactly one instance — creational patterns help.
  • Structural patterns deal with assembling things. When you need to make incompatible pieces work together, add features without modifying existing code, or simplify a complex system — structural patterns help.
  • Behavioral patterns deal with coordinating things. When objects need to talk to each other, divide responsibilities, or change behavior dynamically — behavioral patterns help.

You don't need to memorize all 23. Let's walk through six essential patterns — two from each family — that cover the scenarios you'll hit most often.

Six Patterns Every Developer Should Know

1. Singleton — "There Can Only Be One"

The Problem: Your app has a database connection pool. If every part of your code creates its own pool, you'll exhaust your database connections in seconds. You need one shared instance that everyone uses.

The Analogy: A country has one president at a time. You don't elect a new president every time someone needs to talk to the government — everyone talks to the same one.

// Pseudo code — the Singleton pattern

CLASS DatabasePool:
    PRIVATE STATIC instance = NULL

    PRIVATE CONSTRUCTOR():
        // Create the actual pool (expensive operation)
        this.pool = CREATE connection pool with 10 connections

    PUBLIC STATIC getInstance():
        IF instance IS NULL:
            instance = NEW DatabasePool()   // First call creates it
        RETURN instance                      // Every call returns the SAME one

// Anywhere in your app:
pool = DatabasePool.getInstance()    // Always the same pool

When you see it in the wild: Logger instances, configuration managers, thread pools, caches. In JavaScript, a simple module-level variable is effectively a Singleton because modules are cached after the first import.

2. Factory — "I'll Build It, You Just Tell Me What You Want"

The Problem: You're building a notification system. Sometimes you need to send an email, sometimes a text, sometimes a push notification. The calling code shouldn't need to know how each type gets constructed — it just needs to say "give me a notification of type X."

The Analogy: Ordering at a restaurant. You say "I'll have the burger." The kitchen (factory) handles all the complexity of how to make it. You don't walk into the kitchen and assemble ingredients yourself.

// Pseudo code — the Factory pattern

FUNCTION createNotification(type, message):
    IF type IS "email":
        RETURN NEW EmailNotification(message, smtp_config)
    ELSE IF type IS "sms":
        RETURN NEW SMSNotification(message, twilio_config)
    ELSE IF type IS "push":
        RETURN NEW PushNotification(message, firebase_config)
    ELSE:
        THROW "Unknown notification type"

// The calling code stays clean and simple:
notification = createNotification("email", "Your order shipped!")
notification.send()

// Adding Slack notifications later? Change the factory.
// The 50 places that CALL the factory don't change at all.

When you see it in the wild: document.createElement('div') in the browser, React.createElement(), any framework method that returns different object types based on input.

3. Adapter — "The Universal Travel Plug"

The Problem: You're integrating a third-party payment library. Your app expects payment processors to have a method called processPayment(amount), but this library uses executeCharge(cents, currency). You can't change either interface.

The Analogy: A travel power adapter. Your American laptop plug doesn't fit a European outlet. The adapter sits between them, translating one shape into another without modifying either the plug or the outlet.

// Pseudo code — the Adapter pattern

// What YOUR code expects:
INTERFACE PaymentProcessor:
    processPayment(amount_in_dollars) → result

// What the LIBRARY provides:
CLASS StripeLibrary:
    executeCharge(amount_in_cents, currency_code) → response

// The adapter bridges the gap:
CLASS StripeAdapter IMPLEMENTS PaymentProcessor:
    PRIVATE stripe = NEW StripeLibrary()

    processPayment(amount_in_dollars):
        cents = amount_in_dollars * 100       // Translate format
        response = stripe.executeCharge(cents, "USD")
        RETURN convert(response)               // Translate response

// Your app code never touches the library directly:
processor = NEW StripeAdapter()
processor.processPayment(29.99)    // Clean, consistent interface

When you see it in the wild: ORMs (adapting your code to different databases), API wrappers, any "client library" that wraps a REST API into method calls.

4. Decorator — "Adding Toppings to Your Pizza"

The Problem: You have a basic logging function. Sometimes you need it to also add timestamps. Sometimes you need it to write to a file. Sometimes both. You could create TimestampLogger, FileLogger, TimestampFileLogger... but this explodes with every new feature.

The Analogy: Pizza toppings. You start with a base pizza, then wrap it with toppings. Each topping adds something without changing the base pizza or the other toppings. Mushrooms + peppers + olives = three decorators on one base.

// Pseudo code — the Decorator pattern

// Base behavior:
CLASS SimpleLogger:
    log(message):
        PRINT message

// Decorator adds timestamps WITHOUT modifying SimpleLogger:
CLASS TimestampDecorator:
    PRIVATE wrapped_logger

    CONSTRUCTOR(logger):
        this.wrapped_logger = logger

    log(message):
        timestamped = "[" + CURRENT_TIME + "] " + message
        this.wrapped_logger.log(timestamped)    // Pass it along

// Another decorator adds file output:
CLASS FileDecorator:
    PRIVATE wrapped_logger

    CONSTRUCTOR(logger):
        this.wrapped_logger = logger

    log(message):
        WRITE message TO "app.log"
        this.wrapped_logger.log(message)        // Also pass it along

// Stack them like toppings:
logger = NEW SimpleLogger()
logger = NEW TimestampDecorator(logger)     // Adds timestamps
logger = NEW FileDecorator(logger)          // Also writes to file
logger.log("User signed in")               // Gets BOTH features

When you see it in the wild: Express/Koa middleware (each middleware "decorates" the request), Python's @decorator syntax, Java I/O streams (BufferedReader(FileReader(file))).

5. Observer — "Subscribing to a YouTube Channel"

The Problem: Your e-commerce app needs to do five things when an order is placed: send a confirmation email, update inventory, notify the warehouse, log analytics, and award loyalty points. If the order code calls all five directly, it becomes a tangled mess that breaks every time you add a sixth action.

The Analogy: YouTube subscriptions. When a creator uploads a video, YouTube doesn't have the creator personally message each subscriber. Instead, subscribers register interest, and the platform broadcasts the update to all of them automatically.

// Pseudo code — the Observer pattern

CLASS OrderSystem:
    PRIVATE listeners = []

    subscribe(listener):
        ADD listener TO this.listeners

    placeOrder(order):
        // Core order logic
        SAVE order TO database

        // Notify ALL subscribers — order code doesn't know or care who
        FOR EACH listener IN this.listeners:
            listener.onOrderPlaced(order)

// Each listener handles its own concern:
orderSystem.subscribe(EmailService)        // Sends confirmation
orderSystem.subscribe(InventoryService)    // Updates stock
orderSystem.subscribe(AnalyticsService)    // Logs event

// Adding loyalty points later? Just subscribe a new listener.
// The order code NEVER changes.
orderSystem.subscribe(LoyaltyService)

When you see it in the wild: React's useState (components re-render when state changes), browser event listeners (addEventListener), pub/sub messaging systems, RxJS observables.

6. Strategy — "Choosing Your Route on Google Maps"

The Problem: Your app calculates shipping costs. Ground shipping uses one formula, express uses another, and international has its own rules. Every time you add IF shipping_type IS "overnight" inside the shipping function, it grows into a monster.

The Analogy: Google Maps route options. You pick your destination once, then choose a strategy: fastest, shortest, or avoid-tolls. The navigation system works the same way regardless — only the routing algorithm swaps out.

// Pseudo code — the Strategy pattern

// Define a family of interchangeable algorithms:
CLASS GroundShipping:
    calculate(weight, distance):
        RETURN weight * 1.5 + distance * 0.5

CLASS ExpressShipping:
    calculate(weight, distance):
        RETURN weight * 3.0 + distance * 1.2 + 10.00

CLASS InternationalShipping:
    calculate(weight, distance):
        RETURN weight * 5.0 + distance * 2.0 + customs_fee(weight)

// The context just delegates to whichever strategy it's given:
CLASS ShippingCalculator:
    PRIVATE strategy

    setStrategy(strategy):
        this.strategy = strategy

    getcost(weight, distance):
        RETURN this.strategy.calculate(weight, distance)

// Swap algorithms without touching the calculator:
calculator = NEW ShippingCalculator()
calculator.setStrategy(NEW GroundShipping())
calculator.getcost(10, 500)    // Uses ground formula

calculator.setStrategy(NEW ExpressShipping())
calculator.getcost(10, 500)    // Same call, different result

When you see it in the wild: Sorting functions that accept a comparator, authentication strategies (Passport.js), compression algorithms, any system where you pass a function/object that controls how work is done.

When to Reach for a Pattern

Patterns aren't something you use on every line of code. They're tools for specific recurring problems. Here's a quick decision guide:

You notice...Reach for...
Multiple parts of your code need the same shared resourceSingleton
Object creation logic is complex or varies by typeFactory
You're integrating a library with an incompatible interfaceAdapter
You need to add optional features without subclass explosionDecorator
Many components need to react when something changesObserver
An algorithm needs to be swappable at runtimeStrategy

The Golden Hammer Trap: When NOT to Use Patterns

Here's the most important section in this entire article.

Research consistently shows that beginners who just learned patterns tend to see them everywhere — a phenomenon called the "Golden Hammer" anti-pattern. If all you have is a hammer, everything looks like a nail.

Don't use a pattern when:

  • Simple code already works. Three lines of straightforward code are better than 30 lines of perfectly-patterned abstraction. A pattern should reduce complexity, not introduce it.
  • You're solving a future problem. "We might need to swap databases someday" is not a reason to add an Adapter today. Patterns solve current problems. Speculative patterns are just dead weight.
  • Your language already handles it. In Python, a module is a Singleton — you don't need the Singleton pattern. In JavaScript, first-class functions are strategies — you don't need a Strategy class hierarchy. Higher-level languages absorb many patterns into their features.
  • You can't name the problem it solves. If you can't articulate the specific pain a pattern eliminates, you're using it as decoration, not as a solution.

A systematic literature review by Wedyan et al. (2020) confirmed what experienced developers know intuitively: patterns improve maintainability and reusability, but they can hurt performance and increase complexity when misapplied. The key word is when misapplied.

The right question is never "which pattern should I use here?" It's "do I have a problem that a pattern solves?"

Patterns Are Already in Your Frameworks

One last insight that often surprises beginners: you're already using design patterns every day through your frameworks.

  • React's useState and useEffect → Observer pattern. When state changes, subscribed components re-render.
  • Express/Koa middleware → Chain of Responsibility (and Decorator). Each middleware wraps the next, adding behavior.
  • Angular's dependency injection → A formalized version of the Factory + Singleton patterns.
  • Django's class-based views → Template Method pattern. You override specific hooks while the framework controls the flow.
  • Redux reducers → Command pattern. Each action is an object describing what happened, processed by a central handler.

You don't need to master all 23 GoF patterns from day one. Start by recognizing the patterns hiding in the tools you already use — then, when you hit a genuine structural problem in your own code, you'll know exactly which pattern-shaped tool to reach for.

The Gang of Four gave us a vocabulary. Your job isn't to use every word — it's to know the right word when you need it.