Harmonious geometric patterns with balanced symmetry and golden proportions
Software Engineering May 14, 2026 • 16 min read

Design Patterns: The Beautiful Solutions Nobody Teaches You First

Every problem you'll face in software has already been solved. Factory, Observer, Strategy, Singleton (and why it's evil). This lesson covers the Gang of Four patterns, when to use them, and more importantly, when NOT to use them.

Share:
Lee Foropoulos

Lee Foropoulos

16 min read

Continue where you left off?
Text size:

Contents

Complete Guide | Lesson 7: SOLIDLesson 8: Design Patterns → Lesson 9: Generics (May 21)

Every problem you'll ever face in software has already been solved. The question is whether you'll spend six months reinventing the wheel or six minutes reading about the solution someone catalogued thirty years ago.

This is Lesson 8, and it maps to Tiphareth on the Tree of Life. Tiphareth means "Beauty" or "Harmony." It sits at the exact center of the Tree, balancing all the forces above and below. Design patterns are the harmonizing force of software: proven solutions that balance flexibility with structure, abstraction with implementation, cleverness with clarity. They're beautiful not because they're complex, but because they're precisely the right amount of architecture for the problem at hand.

What Are Design Patterns?

In 1994, four authors (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, collectively known as the Gang of Four or GoF) published Design Patterns: Elements of Reusable Object-Oriented Software. It catalogued 23 patterns: recurring solutions to recurring problems. The book didn't invent these patterns. It named them. And naming them changed everything.

Before the book, two developers might independently solve the same problem with the same structural approach and not realize they were doing the same thing. After the book, you could say "use the Observer pattern" and every developer in the room understood the entire architecture in two words.

Design patterns aren't copy-paste code. They're templates for solving categories of problems. Knowing when to use a pattern is wisdom. Knowing when NOT to use one is mastery.

The 23 patterns fall into three categories:

Creational patterns: How objects are born. Factory, Builder, Singleton, Prototype, Abstract Factory.

Structural patterns: How objects are organized. Adapter, Decorator, Facade, Composite, Bridge, Proxy, Flyweight.

Behavioral patterns: How objects communicate. Observer, Strategy, Command, State, Template Method, Iterator, Mediator, Chain of Responsibility, Visitor, Memento, Interpreter.

You don't need to memorize all 23. You need to deeply understand about 6 of them and recognize the rest when you encounter them. Here are the ones that matter most.

23
design patterns in the original Gang of Four catalogue. Published in 1994, still referenced daily in 2026. Good abstractions age like whiskey, not like milk.
Organized library of reference books with systematic categorization
The Gang of Four book is to software design what the periodic table is to chemistry: a systematic catalogue of the building blocks. You don't memorize every element. You learn the patterns in the table.

Creational Patterns: How Things Are Born

Factory Method

Instead of calling new SomeClass() directly, you ask a factory to create the right object. The factory decides which concrete class to instantiate based on the input.

Analogy: you walk into a restaurant and order "a burger." You don't walk into the kitchen and assemble the ingredients yourself. The kitchen (factory) handles the creation. You just specify what you want.

1CLASS ShapeFactory
2    STATIC METHOD create(type: String): Shape
3        IF type == "circle" THEN
4            RETURN new Circle()
5        ELSE IF type == "rectangle" THEN
6            RETURN new Rectangle()
7        ELSE IF type == "triangle" THEN
8            RETURN new Triangle()
9        END IF
10    END METHOD
11END CLASS
12
13// Usage: the caller never uses "new" directly
14shape = ShapeFactory.create("circle")

Why this matters: the caller doesn't need to know which concrete class is created. If you add a Hexagon class later, you update the factory. The calling code doesn't change.

Builder

Construct complex objects step by step instead of passing 15 parameters to a constructor.

Analogy: building a custom PC. You choose the CPU, then the RAM, then the GPU, then the storage. Each step is independent. The builder assembles the final product.

1pizza = PizzaBuilder()
2    .setSize("large")
3    .setCrust("thin")
4    .addTopping("pepperoni")
5    .addTopping("mushrooms")
6    .addSauce("marinara")
7    .build()

Without Builder, you'd have: new Pizza("large", "thin", ["pepperoni", "mushrooms"], "marinara", null, null, false, true). What's the seventh parameter? Nobody knows. Builder makes the construction self-documenting.

Singleton (The Controversial One)

Ensure only one instance of a class exists, ever. Like a president: only one at a time, globally accessible.

1CLASS DatabaseConnection
2    PRIVATE STATIC _instance: DatabaseConnection = null
3
4    PRIVATE CONSTRUCTOR()
5        // Private! Nobody can call "new DatabaseConnection()"
6    END CONSTRUCTOR
7
8    STATIC METHOD getInstance()
9        IF _instance == null THEN
10            _instance = new DatabaseConnection()
11        END IF
12        RETURN _instance
13    END METHOD
14END CLASS
15
16// Everyone gets the same connection:
17db1 = DatabaseConnection.getInstance()
18db2 = DatabaseConnection.getInstance()
19// db1 and db2 are the same object

Now here's the controversy: Singleton is widely considered an antipattern. It's global state in disguise. It creates hidden dependencies (any code can access it from anywhere). It makes testing nearly impossible (you can't inject a mock). It violates the Single Responsibility Principle (the class manages both its behavior AND its own lifecycle).

The Singleton Problem

Singleton is the goto of object-oriented programming: technically legal, occasionally useful, and almost always a sign that something better exists. When you think you need a Singleton, you usually need dependency injection instead. Pass the dependency in through the constructor (Lesson 6). Let the caller decide what instance to use. Your code becomes testable, your dependencies become explicit, and you don't have global state hiding in corners.

When Singleton is acceptable: logging (maybe), configuration loading at startup (maybe), connection pools (maybe). The word "maybe" appears three times because the answer is almost always "use dependency injection instead."

70%
of senior developers consider Singleton an antipattern in a 2023 Stack Overflow survey. It's the most taught and most regretted pattern in the GoF catalogue.

Structural Patterns: How Things Are Organized

Adapter

Makes incompatible interfaces work together. Like a travel power adapter: your American plug doesn't fit a European outlet, so you use an adapter in between.

1// Old payment system with incompatible interface
2CLASS OldPaymentGateway
3    METHOD makePayment(dollars, cents, cardNum)
4    END METHOD
5END CLASS
6
7// Your system expects IPayable (from Lesson 6)
8CLASS PaymentAdapter IMPLEMENTS IPayable
9    gateway: OldPaymentGateway
10
11    METHOD processPayment(amount: Decimal)
12        dollars = floor(amount)
13        cents = round((amount - dollars) * 100)
14        this.gateway.makePayment(dollars, cents, this.cardNum)
15    END METHOD
16END CLASS

The adapter translates between what your code expects and what the old system provides. Neither side needs to change.

Decorator

Adds behavior to an object without changing its class. Like wrapping a gift: the gift stays the same, but now it has wrapping paper, a bow, and a card.

1// Base coffee
2coffee = new SimpleCoffee()             // $2.00
3
4// Decorate it
5coffee = new MilkDecorator(coffee)      // $2.50
6coffee = new SugarDecorator(coffee)     // $2.60
7coffee = new WhipCreamDecorator(coffee) // $3.10
8
9// Each decorator wraps the previous one, adding cost and description
10coffee.getCost()        // $3.10
11coffee.getDescription() // "Simple coffee, milk, sugar, whip cream"

Each decorator adds functionality while maintaining the same interface. You can stack them in any order, any combination.

Facade

Provides a simple interface to a complex subsystem. Like a TV remote: one button turns on the TV, receiver, speakers, and streaming box. You don't configure each device separately.

1CLASS HomeTheaterFacade
2    tv: Television
3    receiver: AudioReceiver
4    speakers: SurroundSound
5    streaming: StreamingBox
6
7    METHOD watchMovie(title)
8        this.tv.turnOn()
9        this.receiver.turnOn()
10        this.speakers.setVolume(40)
11        this.streaming.launch()
12        this.streaming.play(title)
13    END METHOD
14
15    METHOD endMovie()
16        this.streaming.stop()
17        this.speakers.setVolume(0)
18        this.receiver.turnOff()
19        this.tv.turnOff()
20    END METHOD
21END CLASS
22
23// One call instead of five:
24theater.watchMovie("Inception")
Various travel power adapters for different countries side by side
An adapter pattern in the physical world. Your plug doesn't fit the outlet? Don't rewire the outlet or redesign the plug. Add an adapter layer that translates between the two interfaces.

Behavioral Patterns: How Things Communicate

Observer

When one object changes state, all subscribers are notified automatically. Like subscribing to a YouTube channel: the creator publishes, all subscribers get the update.

1CLASS EventEmitter
2    subscribers: Map<String, List<Function>>
3
4    METHOD subscribe(event, callback)
5        this.subscribers[event].add(callback)
6    END METHOD
7
8    METHOD emit(event, data)
9        FOR EACH callback IN this.subscribers[event]
10            callback(data)
11        END FOR
12    END METHOD
13END CLASS
14
15// Subscribe
16priceTracker.subscribe("priceChanged", updateDashboard)
17priceTracker.subscribe("priceChanged", sendAlert)
18priceTracker.subscribe("priceChanged", logToDatabase)
19
20// When price changes, ALL subscribers fire:
21priceTracker.emit("priceChanged", newPrice)

The price tracker doesn't know what the subscribers do. It just notifies them. Each subscriber decides its own response. This is the foundation of every event-driven system, every notification service, and every reactive UI framework.

Strategy

Swap algorithms at runtime. Like choosing a route on GPS: fastest, shortest, or scenic. Same destination, different approach.

1INTERFACE ISortStrategy
2    METHOD sort(data: List): List
3END INTERFACE
4
5CLASS QuickSort IMPLEMENTS ISortStrategy
6    METHOD sort(data) // Fast for large datasets
7END CLASS
8
9CLASS InsertionSort IMPLEMENTS ISortStrategy
10    METHOD sort(data) // Fast for small or nearly-sorted datasets
11END CLASS
12
13CLASS Sorter
14    strategy: ISortStrategy
15
16    METHOD setStrategy(strategy)
17        this.strategy = strategy
18    END METHOD
19
20    METHOD sort(data)
21        RETURN this.strategy.sort(data)
22    END METHOD
23END CLASS
24
25// Swap algorithms at runtime:
26sorter = new Sorter()
27sorter.setStrategy(new QuickSort())
28sorter.sort(largeDataset)
29
30sorter.setStrategy(new InsertionSort())
31sorter.sort(smallDataset)

Strategy lets you change behavior without changing the object that uses it. This is composition (Lesson 5) and interfaces (Lesson 6) working together.

Command

Encapsulate a request as an object. Like a restaurant order ticket: it can be queued, tracked, undone, or replayed.

1INTERFACE ICommand
2    METHOD execute()
3    METHOD undo()
4END INTERFACE
5
6CLASS AddToCartCommand IMPLEMENTS ICommand
7    cart: Cart
8    item: Item
9
10    METHOD execute()
11        this.cart.add(this.item)
12    END METHOD
13
14    METHOD undo()
15        this.cart.remove(this.item)
16    END METHOD
17END CLASS
18
19// Commands can be stored, queued, undone:
20history = []
21cmd = new AddToCartCommand(cart, shoes)
22cmd.execute()
23history.push(cmd)
24
25// Undo!
26lastCmd = history.pop()
27lastCmd.undo()

Every undo/redo system, every transaction log, and every macro recorder uses the Command pattern.

The right pattern for the right problem is elegant. The wrong pattern for any problem is a Rube Goldberg machine that nobody can debug, including the person who built it.

When NOT to Use Patterns

This is the most important section of this lesson: pattern abuse is worse than no patterns at all.

The Golden Hammer: When you learn a new pattern, everything looks like a nail. You just learned Factory? Suddenly every object creation needs a factory, even when new Thing() works perfectly.

YAGNI: You Aren't Gonna Need It. Don't add a Strategy pattern when you have one algorithm. Don't create an Observer system for one listener. Don't build a Factory for one product.

Simple beats clever. A straightforward if/else that everyone understands is better than a pattern that only you understand. Code is read 10 times more often than it's written. Optimize for readability.

When you DO need a pattern: When you find yourself writing the same structural solution in multiple places. When you're fighting your own code to add features. When testing becomes impossible because of dependencies. That's when a pattern earns its place.

"Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away." Antoine de Saint-Exupery said that about design. It applies perfectly to pattern use: the right amount is the minimum that solves the problem.

Pattern Selection Cheat Sheet

Need to create objects without specifying exact classes? → Factory Need to build complex objects step by step? → Builder Need to add behavior without modifying existing classes? → Decorator Need to simplify a complex subsystem? → Facade Need to notify multiple objects of state changes? → Observer Need to swap algorithms at runtime? → Strategy Need undo/redo or command queuing? → Command Need to make incompatible interfaces work together? → Adapter Need to ensure only one instance? → Dependency injection first. Singleton only as a last resort.

Lesson 8 Practice 0/6
How was this article?

Share

Link copied to clipboard!

You Might Also Like

Lee Foropoulos

Lee Foropoulos

Business Development Lead at Lookatmedia, fractional executive, and founder of gotHABITS.

🔔

Never Miss a Post

Get notified when new articles are published. No email required.

You will see a banner on the site when a new post is published, plus a browser notification if you allow it.

Browser notifications only. No spam, no email.

0 / 0