Six months into my first serious project, a senior developer reviewed my code and said: "You've reinvented the Observer pattern, but broken." I didn't know what the Observer pattern was. I had spent two days building a custom event notification system because two parts of the app needed to react when a user's status changed. My system worked β mostly β but it was tangled, hard to extend, and had an edge case that caused double-notifications.
He showed me the Observer pattern. Same concept I had been building, but clean, named, and with a documented solution to the exact edge case I was struggling with. I felt two things simultaneously: impressed that this problem was solved and documented, and frustrated that I hadn't known it existed before spending two days on it.
That's what learning design patterns does. It stops you from reinventing wheels β and reinventing them with flat spots.
Design patterns are proven, named solutions to problems that recur so frequently that the software community documented them formally. Understanding them doesn't just improve individual code quality; it gives you a shared vocabulary with every developer who has read the same patterns β which is most professional developers.
The Origin of Design Patterns
The concept was popularized by the Gang of Four (GoF) β four computer scientists who published "Design Patterns: Elements of Reusable Object-Oriented Software" in 1994. They documented 23 patterns organized into three categories: Creational, Structural, and Behavioral. These patterns weren't invented by the authors β they were observed and documented from existing successful codebases. That's what makes them valuable: they represent hard-won wisdom about what works.
Design patterns are not code you copy-paste. They're templates β approaches to solving problems that you adapt to your specific language and context. The same pattern looks different in JavaScript than in Java, but the underlying idea is identical.
Creational Patterns: Controlling Object Creation
Creational patterns deal with how objects are created, aiming to make creation more flexible and reusable.
The Singleton pattern ensures that a class has only one instance, accessible globally. A database connection pool is a classic example β you want one pool shared across your entire application, not a new pool created every time you need a database connection. The Singleton provides a global access point to that single instance. Use it sparingly; overuse creates hidden global state that's hard to test.
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. Instead of calling new Button() directly, you call createButton() and the factory decides whether to create a WindowsButton or a MacButton based on the operating system. This decouples your code from specific implementations.
The Builder pattern separates the construction of a complex object from its representation. Rather than a constructor with dozens of parameters, a Builder lets you construct an object step by step. Think of building a SQL query: query.select("name").from("users").where("age > 18").limit(10). Each method call configures one aspect of the final object.
Structural Patterns: Composing Objects
Structural patterns describe how to combine objects and classes into larger structures.
The Adapter pattern makes incompatible interfaces work together. Your application expects a specific interface, but you're integrating a third-party library with a different one. An Adapter wraps the library and translates calls to the expected interface β like a power plug adapter that lets a US-plug device work in a European socket.
The Decorator pattern adds behavior to objects dynamically without modifying their class. Instead of creating subclasses for every combination of features, you wrap objects with Decorators that add functionality. A logging decorator might wrap any service to log its method calls without changing the service itself. JavaScript's function decorators and Python's @decorator syntax are language-level implementations of this concept.
The Facade pattern provides a simplified interface to a complex subsystem. A home theater system might involve a receiver, projector, streaming box, and sound system β each with its own controls. A Facade provides a single watchMovie() method that sets everything up correctly. Your application interacts with the Facade rather than the complex subsystem directly.
Behavioral Patterns: Communication Between Objects
Behavioral patterns focus on how objects communicate and responsibilities are distributed.
The Observer pattern establishes a one-to-many dependency: when one object changes state, all its dependents are automatically notified. This is how event systems work. A button in a UI fires an event when clicked; all registered listeners receive the notification. React's state updates, JavaScript's EventEmitter, and reactive programming libraries are all implementations of the Observer pattern.
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. An e-commerce platform might support multiple payment strategies: credit card, PayPal, cryptocurrency. The Strategy pattern lets you switch between these algorithms without changing the code that uses them. Simply swap the strategy at runtime.
The Command pattern encapsulates a request as an object. This lets you parameterize methods with different requests, queue requests, log them, and support undo/redo. Text editors use the Command pattern β every edit operation is a Command object, making undo as simple as reversing the last command.
When to Use Patterns (and When Not To)
The most important lesson about design patterns is knowing when to apply them. A pattern applied to a problem it doesn't fit creates complexity without benefit β this is called "pattern overengineering." Before applying a pattern, identify the specific problem it solves and verify your situation actually has that problem.
Patterns are most valuable when the same problem keeps appearing in your codebase, when you need to communicate architectural decisions to teammates, and when anticipating specific types of change or extension. Start with simple, direct code. When complexity grows and a pattern's benefits become clear, refactor toward the pattern.
