Design Patterns in Object-Oriented Programming
Learning Objectives
By the end of this page, you will be able to:
- Explain what a design pattern is and why reusable solutions matter in software design
- Implement the Singleton pattern and explain its trade-offs
- Implement the Factory pattern to decouple object creation from object use
- Implement the Observer pattern to model one-to-many notification relationships
- Implement the Decorator pattern to add behavior without modifying existing classes
- Identify which pattern fits a given design problem and avoid unnecessary pattern use
Quick Answer
A design pattern is a named, reusable solution to a problem that recurs across many software designs — not a finished piece of code you copy-paste, but a proven template for structuring classes and their relationships. Patterns matter because they give developers a shared vocabulary ("just use a Factory here") that communicates an entire design idea in one word, and because they encode lessons learned from countless past projects, so you don't have to rediscover good structure by trial and error. The four patterns covered here — Singleton (one shared instance), Factory (delegate object creation), Observer (notify dependents automatically), and Decorator (add behavior dynamically) — are among the most commonly used in real codebases, from GUI frameworks to logging systems to web servers.
Core Content
1. What Is a Design Pattern?
Definition: A design pattern is a general, reusable arrangement of classes and objects that solves a commonly occurring design problem within a given context, first cataloged systematically by the "Gang of Four" (Gamma, Helm, Johnson, Vlissides) in 1994.
Explanation: Design patterns are not algorithms and not finished libraries — they are structural templates. A pattern describes which classes exist, how they relate (inheritance, composition, delegation), and what problem that arrangement solves, leaving the specific field names and business logic to you. Patterns are traditionally grouped into three categories: creational (how objects are created — Singleton, Factory), structural (how objects are composed — Decorator, Adapter), and behavioral (how objects communicate — Observer, Strategy). Knowing the category helps you recognize which kind of problem you're facing before reaching for a specific pattern.
Simple Example (conceptual, no single language): Recognizing "I need exactly one configuration object shared everywhere" is a creational problem → reach for Singleton. Recognizing "I want objects to react automatically whenever this data changes" is a behavioral problem → reach for Observer.
Real-World Example: Architectural blueprints have standard patterns too — a "load-bearing wall" or a "cantilever" is a named, reusable structural solution that any architect recognizes instantly, without the engineer re-deriving the physics from scratch each time. Design patterns give programmers the same shared shorthand for structuring code.
Why It Matters: Patterns speed up both design and communication: instead of describing an entire class arrangement in a meeting, you say "it's a Factory" and every experienced developer immediately pictures the structure.
Common Misunderstanding: Beginners often think a design pattern is a specific piece of code to memorize and paste in. It's actually a concept — the Java Singleton example below and a completely different-looking Python Singleton are both "the Singleton pattern" because they solve the same problem the same structural way, not because the code looks alike.
2. Singleton Pattern
Definition: The Singleton pattern restricts a class to exactly one instance for the lifetime of the program and provides a single, well-known way to access that instance.
Explanation: A Singleton typically makes its constructor private (so outside code can't create new instances directly) and exposes a static method — commonly getInstance() — that creates the one instance the first time it's needed and returns that same instance on every subsequent call. This is useful for things that genuinely should be singular and shared: a single logging system, a single connection pool, a single application configuration object. The trade-off is that Singletons introduce global, shared mutable state, which makes unit testing harder (tests can affect each other through the shared instance) and can hide dependencies that would otherwise be visible as constructor parameters.
Simple Example (Java):
public class Logger {
private static Logger instance;
private Logger() { } // private constructor blocks outside instantiation
public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
// Usage: Logger.getInstance().log("Started"); — always the same object
Simple Example (Python — module-level singleton, the idiomatic approach):
class _Logger:
def log(self, message):
print(f"[LOG] {message}")
logger = _Logger() # Python modules are only imported once, so this
# single instance is naturally shared everywhere
Real-World Example: A country has exactly one central bank — every financial institution refers to the same one for setting the base interest rate. Having two competing central banks issuing conflicting rates would cause chaos, which is exactly the inconsistency a Singleton prevents for shared program state.
Why It Matters: When a resource genuinely must be unique and globally coordinated — like a single point of access to a hardware device or a shared cache — Singleton prevents accidental duplicate instances that would fight over the same underlying resource.
Common Misunderstanding: Students overuse Singleton for anything that's merely "used a lot," not things that must be genuinely singular. Turning ordinary service classes into Singletons just to avoid passing them as parameters creates hidden global state and tightly coupled code that's hard to test in isolation.
3. Factory Pattern
Definition: The Factory pattern delegates the responsibility of creating objects to a separate method or class, so the calling code depends on an abstract type rather than concrete classes.
Explanation: Without a Factory, code that needs different kinds of related objects (Dog, Cat) typically litters new Dog() or new Cat() calls throughout the codebase, tied to if/else or switch logic wherever an object is needed. A Factory centralizes that decision in one place: calling code just asks the factory for "an Animal of type Dog" and receives an object typed as the common Animal interface, never needing to know the concrete class. This means adding a new subtype (Bird) only requires updating the factory, not every call site that creates animals.
Simple Example (Java):
abstract class Animal {
public abstract String sound();
}
class Dog extends Animal {
public String sound() { return "Woof"; }
}
class Cat extends Animal {
public String sound() { return "Meow"; }
}
class AnimalFactory {
public static Animal createAnimal(String type) {
switch (type) {
case "Dog": return new Dog();
case "Cat": return new Cat();
default: throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
// Usage: Animal a = AnimalFactory.createAnimal("Dog"); System.out.println(a.sound());
Simple Example (Python):
class Dog:
def sound(self):
return "Woof"
class Cat:
def sound(self):
return "Meow"
def animal_factory(kind):
animals = {"Dog": Dog, "Cat": Cat}
if kind not in animals:
raise ValueError(f"Unknown type: {kind}")
return animals[kind]()
pet = animal_factory("Cat")
print(pet.sound()) # Meow
Real-World Example: Ordering a car from a manufacturer: you specify "sedan" or "SUV," and the factory (literally, in this case) handles all the internal assembly steps for that model — you never need to know how the engine is bolted in to get the right car handed to you.
Why It Matters: Factories decouple what code needs (an Animal) from how it's constructed and which concrete class is used, which is essential for extensibility — new subclasses can be added without touching existing calling code, honoring the open/closed principle.
Common Misunderstanding: Students think a Factory is just "a method that calls new." The distinguishing feature isn't the wrapping method itself — it's that calling code programs against the abstract return type (Animal) and never directly references the concrete subclasses, which is what actually provides the decoupling benefit.
4. Observer Pattern
Definition: The Observer pattern defines a one-to-many dependency where a subject object notifies a list of registered observer objects automatically whenever its state changes.
Explanation: Instead of the subject knowing the specific concrete classes of everything that cares about its changes, it only knows about a generic Observer interface with an update() method. Any object implementing that interface can register itself and be notified — the subject doesn't need to change when new kinds of observers are added later. This pattern underlies most GUI event systems (a button doesn't know which specific code runs when clicked, it just notifies all registered listeners) and publish/subscribe messaging systems.
Simple Example (Java):
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
class ConsoleObserver implements Observer {
private String name;
public ConsoleObserver(String name) { this.name = name; }
public void update(String message) {
System.out.println(name + " received: " + message);
}
}
// Usage:
// Subject subject = new Subject();
// subject.addObserver(new ConsoleObserver("Watcher A"));
// subject.notifyObservers("Temperature changed to 30C");
Simple Example (Python):
class Subject:
def __init__(self):
self._observers = []
def add_observer(self, observer):
self._observers.append(observer)
def notify_observers(self, message):
for observer in self._observers:
observer.update(message)
class ConsoleObserver:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received: {message}")
subject = Subject()
subject.add_observer(ConsoleObserver("Watcher A"))
subject.notify_observers("Temperature changed to 30C")
Real-World Example: Subscribing to a YouTube channel: the channel (subject) doesn't maintain a hardcoded list of specific viewers by name — it just notifies everyone currently subscribed (observers) whenever a new video (state change) is published, and anyone can subscribe or unsubscribe at any time.
Why It Matters: Observer decouples the object that changes from the objects that react to the change, which is essential for building systems where the number and type of "listeners" can grow over time without modifying the subject's code.
Common Misunderstanding: Students assume observers are notified in some meaningful priority order or that notification is asynchronous. In a typical implementation like the one above, notification is synchronous and in registration order — if one observer's update() throws an exception or blocks, it directly affects the subject's notifyObservers() call.
5. Decorator Pattern
Definition: The Decorator pattern lets you attach new behavior to an individual object dynamically, by wrapping it in one or more decorator objects that share the same interface, instead of modifying the original class or using inheritance for every combination.
Explanation: Without Decorator, adding optional features (milk, sugar, whipped cream on a coffee) via inheritance alone would require a separate subclass for every combination of features — CoffeeWithMilk, CoffeeWithMilkAndSugar, and so on, which explodes combinatorially. Decorator instead wraps a base object in layers, where each layer implements the same interface and delegates to the object it wraps, adding its own behavior before or after. Because decorators share the base interface, they can be stacked in any order and any number of times, and calling code just sees "a Coffee" regardless of how many layers deep it is.
Simple Example (Java):
interface Coffee {
String getDescription();
double cost();
}
class SimpleCoffee implements Coffee {
public String getDescription() { return "Coffee"; }
public double cost() { return 5.0; }
}
class MilkDecorator implements Coffee {
private Coffee coffee;
public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
public String getDescription() { return coffee.getDescription() + " + Milk"; }
public double cost() { return coffee.cost() + 1.5; }
}
class SugarDecorator implements Coffee {
private Coffee coffee;
public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
public String getDescription() { return coffee.getDescription() + " + Sugar"; }
public double cost() { return coffee.cost() + 0.5; }
}
// Usage:
// Coffee order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
// System.out.println(order.getDescription() + " = $" + order.cost());
// Output: Coffee + Milk + Sugar = $7.0
Simple Example (Python — using composition, the same structural idea):
class SimpleCoffee:
def cost(self):
return 5.0
def description(self):
return "Coffee"
class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost() + 1.5
def description(self):
return self._coffee.description() + " + Milk"
order = MilkDecorator(SimpleCoffee())
print(order.description(), "=", order.cost()) # Coffee + Milk = 6.5
Real-World Example: A phone case (decorator) adds protection to a phone (base object) without modifying the phone's internal design, and you can stack a screen protector on top of the case — each layer adds a feature independently, and you can mix and match layers freely.
Why It Matters: Decorator gives you combinatorial flexibility (any subset of features, in any order) without an explosion of subclasses, and it respects the open/closed principle — you add new decorators without touching SimpleCoffee or existing decorators.
Common Misunderstanding: Students confuse Decorator with simple inheritance. Inheritance fixes behavior at compile time through a rigid class hierarchy; Decorator composes behavior at runtime by wrapping objects, which is what allows the exact same SimpleCoffee object to be wrapped differently in different parts of a program, or even reconfigured while the program runs.
Key Terms
| Term | Definition | Context/Related Concepts |
|---|---|---|
| Design pattern | A reusable, named solution to a recurring software design problem | Creational, structural, or behavioral |
| Creational pattern | Pattern concerned with how objects are created | Singleton, Factory |
| Structural pattern | Pattern concerned with how objects are composed | Decorator, Adapter |
| Behavioral pattern | Pattern concerned with how objects communicate | Observer, Strategy |
| Singleton | Pattern restricting a class to one shared instance | Private constructor + static accessor |
| Factory | Pattern that delegates object creation to a dedicated method/class | Decouples calling code from concrete classes |
| Observer | Pattern where a subject notifies registered dependents of changes | Basis of GUI events, pub/sub systems |
| Decorator | Pattern that adds behavior to an object by wrapping it | Same interface, stackable at runtime |
| Open/closed principle | Design goal: open for extension, closed for modification | Achieved by Factory, Decorator, and similar patterns |
| Gang of Four (GoF) | Common name for the authors who cataloged classic design patterns | Gamma, Helm, Johnson, Vlissides (1994) |
Common Mistakes
Misconception 1: "Design patterns are exact code templates you copy into any project." Why It's Wrong: A pattern describes a structural relationship between classes, not literal syntax — the same pattern looks different in Java, Python, or C++, and even within one language, field names and details vary by project. Correct Understanding: Learn the problem each pattern solves and the shape of its solution (which roles exist and how they relate), then adapt that shape to your specific classes and naming.
Misconception 2: "Singleton should be used whenever a class is used frequently throughout an application." Why It's Wrong: Frequent use doesn't imply the class must be globally unique; forcing a Singleton onto something that doesn't need to be singular introduces hidden global state, making unit tests harder to isolate and dependencies harder to see. Correct Understanding: Reserve Singleton for cases where having more than one instance would actually cause incorrect behavior (like two conflicting configuration objects), and prefer passing regular objects as parameters (dependency injection) otherwise.
Misconception 3: "Using more design patterns always makes code better." Why It's Wrong: Applying a pattern where it isn't needed adds indirection and extra classes without solving a real problem, making the code harder to read and navigate than a simpler, direct implementation would have been. Correct Understanding: Apply a pattern only when its specific problem is actually present in your design — if you're not fighting combinatorial subclassing, don't reach for Decorator; if you don't need shared global state, don't reach for Singleton.
Comparison and Connections
| Concept A | Concept B | Key Difference |
|---|---|---|
| Singleton | Factory | Singleton controls how many instances exist (one); Factory controls how instances are created (which concrete class) |
| Factory | Decorator | Factory decides which concrete class to instantiate up front; Decorator adds behavior to an already-existing object afterward |
| Observer | Decorator | Observer is about notification between separate objects; Decorator is about composing extra behavior into one object's interface |
| Inheritance | Decorator | Inheritance fixes behavior at compile time via a class hierarchy; Decorator composes behavior at runtime by wrapping objects |
| Creational pattern | Structural pattern | Creational patterns (Singleton, Factory) manage object creation; structural patterns (Decorator) manage how objects are combined |
| Design pattern | Algorithm | An algorithm is a precise step-by-step procedure for a computation; a design pattern is a structural template for organizing classes, with no fixed steps |
Practice Questions
Recall
-
What problem does the Singleton pattern solve? Answer: It ensures a class has exactly one instance throughout the program's lifetime and provides a single, well-known way to access that instance.
-
Name the three broad categories design patterns are usually grouped into. Answer: Creational (object creation), structural (object composition), and behavioral (object communication).
Understanding
-
Why does the Factory pattern make it easier to add a new subtype (e.g., a
BirdalongsideDogandCat) without breaking existing code? Answer: Calling code depends only on the abstractAnimaltype and the factory method, never on concrete classes directly. AddingBirdonly requires updating the factory's creation logic; every place that already callsAnimalFactory.createAnimal(...)keeps working unchanged. -
Why is the Observer pattern a natural fit for GUI event handling (e.g., button clicks)? Answer: A GUI button doesn't know in advance which application-specific code should run when clicked. By having the button (subject) simply notify all registered listener objects (observers) implementing a common interface, any number of unrelated pieces of code can react to the same event without the button's class needing to change.
Application
-
Using the Factory pattern shown, add a
Birdclass that returns"Tweet"fromsound(), and updateAnimalFactoryto support it. Answer:class Bird extends Animal {public String sound() { return "Tweet"; }}// in AnimalFactory.createAnimal:case "Bird": return new Bird(); -
Using the Decorator pattern shown for coffee, write the code to create a coffee with milk and sugar in that order, and give its final cost. Answer:
Coffee order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));// cost = 5.0 (base) + 1.5 (milk) + 0.5 (sugar) = 7.0
Analysis
-
A team implements Singleton for their
DatabaseConnectionclass, but later finds their unit tests fail intermittently because tests run in different orders and share the connection's leftover state. What's the root design issue, and how could it be fixed without abandoning the "single connection" requirement? Answer: The root issue is that Singleton's globally shared mutable state leaks between tests — one test's changes to the connection's state persist into the next test. Rather than removing the single-connection requirement, the fix is usually to inject the connection as a parameter (dependency injection) so tests can supply a fresh, isolated instance (or a mock) instead of relying on the process-wide global, while production code still uses one real shared connection. -
Compare using the Decorator pattern versus creating a new subclass for every combination of features (e.g.,
CoffeeWithMilk,CoffeeWithMilkAndSugar) when a coffee shop has 4 optional add-ons. Answer: With 4 independent add-ons, subclassing every combination requires up to 2^4 = 16 classes to cover all subsets, and adding a 5th add-on doubles that again. Decorator needs only 4 decorator classes total (one per add-on) regardless of how many are combined, because combinations are built at runtime by stacking wrappers rather than declared in advance as separate classes.
FAQ
Q: Do I need to memorize the Gang of Four's original 23 patterns? A: No — most working developers know a handful well (Singleton, Factory, Observer, Decorator, Strategy, Adapter) and look up the rest when a specific problem calls for them. Recognizing the underlying problem matters far more than reciting pattern names.
Q: Is Singleton considered an anti-pattern? A: It's controversial. Many experienced developers avoid it because of the global-state and testability issues described above, preferring dependency injection instead. It's still useful in narrow cases — like truly single hardware resources — but should not be a default choice.
Q: What's the difference between the Factory pattern and just calling a constructor directly? A: Calling a constructor directly ties your code to one specific concrete class forever. A factory method returns an abstract type and hides the decision of which concrete class to instantiate, so that decision can change (or be extended) without touching every place that requests an object.
Q: Can multiple design patterns be used together in the same system? A: Yes, and it's common — for example, a Factory might be used to create objects that are then wrapped in Decorators, or a Singleton might manage a list of Observers. Patterns are complementary building blocks, not mutually exclusive choices.
Q: How do I know which pattern to use for a given problem? A: Start from the problem, not the pattern: if you need exactly one shared instance, think Singleton; if you need to hide which concrete class gets created, think Factory; if many objects need to react to one object's changes, think Observer; if you need to add optional behavior without subclass explosion, think Decorator.
Quick Revision
- A design pattern is a reusable structural template for solving a recurring design problem, not literal code to copy.
- Patterns fall into three categories: creational (Singleton, Factory), structural (Decorator), behavioral (Observer).
- Singleton restricts a class to one instance via a private constructor and a static accessor like
getInstance(). - Singleton's main trade-off is global mutable state, which complicates unit testing.
- Factory delegates object creation to a method/class, so calling code depends on an abstract type, not concrete classes.
- Factory makes adding new subtypes easy without touching existing calling code.
- Observer defines a one-to-many notification relationship: a subject notifies all registered observers on change.
- Observer notifications are typically synchronous and run in registration order, not asynchronous or prioritized.
- Decorator adds behavior to an object by wrapping it in layers that share the same interface.
- Decorator avoids the combinatorial subclass explosion that pure inheritance would require for optional features.
- All four patterns support the open/closed principle: extend behavior without modifying existing, working code.
- Use a pattern only when its specific problem is genuinely present — don't apply patterns for their own sake.
Related Topics
Prerequisites
- Classes and objects
- Inheritance and interfaces/abstract classes
- Polymorphism
Related Topics
- Exception handling (used within pattern implementations for validation and error signaling)
- Encapsulation and access modifiers
- SOLID design principles
Next Topics
- Additional Gang of Four patterns (Strategy, Adapter, Builder)
- Software architecture and system design
- Dependency injection