Design Patterns: Singleton, Factory, Observer, and More
Design patterns are reusable solutions to common software design problems. They are not finished code — they are templates that you adapt to your specific situation. The concept was popularized by the Gang of Four (GoF) book, which categorized 23 patterns into three groups: creational, structural, and behavioral.
This guide covers the most important design patterns with practical examples and guidance on when to use each.
Creational Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a way that suits the situation.
Singleton
Singleton ensures a class has only one instance and provides a global point of access to it.
public class ConfigManager {
private static ConfigManager instance;
private Properties config;
private ConfigManager() {
config = new Properties();
loadConfig();
}
public static synchronized ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
public String get(String key) {
return config.getProperty(key);
}
---Use when: You need exactly one instance of a class — configuration managers, connection pools, logging services.
Avoid when: It makes testing difficult (you cannot replace the instance with a mock) or when you could pass the dependency explicitly instead.
Factory Method
Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate.
abstract class DocumentCreator {
public abstract Document createDocument();
public void openDocument() {
Document doc = createDocument();
doc.open();
doc.read();
}
---
class PdfCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new PdfDocument();
}
---
class WordCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new WordDocument();
}
---Use when: A class cannot anticipate the type of objects it needs to create, or you want to delegate creation to subclasses.
Builder
Builder separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
public class Pizza {
private String dough;
private String sauce;
private List<String> toppings;
public static class Builder {
private String dough;
private String sauce;
private List<String> toppings = new ArrayList<>();
public Builder dough(String dough) {
this.dough = dough;
return this;
}
public Builder sauce(String sauce) {
this.sauce = sauce;
return this;
}
public Builder addTopping(String topping) {
this.toppings.add(topping);
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
private Pizza(Builder builder) {
this.dough = builder.dough;
this.sauce = builder.sauce;
this.toppings = builder.toppings;
}
---
Pizza pizza = new Pizza.Builder()
.dough("thin crust")
.sauce("tomato")
.addTopping("cheese")
.addTopping("pepperoni")
.build();Use when: An object has many optional parameters, or the construction process should allow different representations.
Structural Patterns
Structural patterns explain how to assemble objects and classes into larger structures while keeping them flexible and efficient.
Adapter
Adapter allows incompatible interfaces to work together. It wraps an existing class with a new interface.
interface EuropeanSocket {
int voltage();
---
class EuropeanSocketImpl implements EuropeanSocket {
public int voltage() { return 230; }
---
interface USASocket {
int voltage();
---
class SocketAdapter implements USASocket {
private EuropeanSocket europeanSocket;
public SocketAdapter(EuropeanSocket socket) {
this.europeanSocket = socket;
}
public int voltage() {
return europeanSocket.voltage() / 2;
}
---Use when: You want to use an existing class but its interface does not match the one you need.
Decorator
Decorator attaches additional responsibilities to an object dynamically.
interface Coffee {
double cost();
String description();
---
class SimpleCoffee implements Coffee {
public double cost() { return 2.0; }
public String description() { return "Coffee"; }
---
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
---
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String description() { return coffee.description() + ", milk"; }
---
class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.3; }
public String description() { return coffee.description() + ", whip"; }
---
Coffee coffee = new WhipDecorator(new MilkDecorator(new SimpleCoffee()));Use when: You need to add responsibilities to objects dynamically without affecting other objects of the same class.
Behavioral Patterns
Behavioral patterns concern algorithms and the assignment of responsibilities between objects.
Observer
Observer defines a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.
Strategy
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable.
interface PaymentStrategy {
void pay(double amount);
---
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
public void pay(double amount) {
System.out.println("Paid $" + amount + " with card " + cardNumber);
}
---
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
}
---
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double total) {
paymentStrategy.pay(total);
}
---Use when: You have multiple algorithms for the same task and want to switch between them at runtime.
Pattern Selection Criteria
Choosing the right pattern depends on the problem context, not just the pattern name. Consider: what is varying (encapsulate what varies), what is the relationship between objects (inheritance vs composition), and what lifecycle management is needed (creation patterns). Overusing patterns leads to unnecessary complexity — the simplest solution that meets requirements is usually best. Patterns are recipes, not rules.
Modern Pattern Evolution
Many classical GoF patterns have modern alternatives. Strategy → higher-order functions and dependency injection. Observer → reactive streams and event buses. Factory → DI containers and builder APIs. Singleton → DI-managed singletons and module-level instances. Template Method → strategy pattern or hook methods. The underlying problems remain, but language features (closures, generics, traits) provide simpler solutions.
Choosing a Pattern
Pick patterns to solve problems, not because they look impressive. Over-engineering with patterns is a common mistake. Start simple, and introduce patterns when you encounter a specific problem that a pattern solves.
Conclusion
Design patterns give you a shared vocabulary and proven solutions to common problems. The most frequently used patterns — Singleton, Factory, Builder, Adapter, Decorator, Observer, and Strategy — cover the majority of real-world scenarios. Study them, understand the problems they solve, and apply them judiciously.
FAQ
How many design patterns should I learn? Start with the seven covered here: Singleton, Factory, Builder, Adapter, Decorator, Observer, and Strategy. These cover most real-world scenarios. Learn others as you encounter specific problems they solve.
Are design patterns still relevant in functional programming? The underlying problems remain, but the solutions often differ. Strategy patterns become higher-order functions. Observer patterns become reactive streams. Factory patterns become constructor functions. Study the problem, not just the pattern.
When should I not use a design pattern? When a simpler solution works. Code without patterns is better than code with incorrectly applied patterns. Refactor to a pattern when you feel the pain the pattern solves, not preemptively.
What is the most commonly overused pattern? Singleton. It introduces global state, makes testing difficult, and hides dependencies. DI-managed singletons (one instance controlled by a container) achieve the same goal without the drawbacks.
Pattern Selection Guide
Choosing the right design pattern depends on the problem context. Here is a decision framework:
When you need to create objects without specifying concrete classes: Creational patterns — Factory Method (single product family), Abstract Factory (multiple families), Builder (complex object construction), Prototype (cloning), Singleton (single instance).
When you need to compose objects into larger structures: Structural patterns — Adapter (interface conversion), Bridge (abstraction/implementation separation), Composite (tree structures), Decorator (dynamic behavior addition), Facade (simplified interface), Flyweight (shared fine-grained objects), Proxy (controlled access).
When you need to manage object interactions: Behavioral patterns — Chain of Responsibility (request passing), Command (request encapsulation), Iterator (sequential access), Mediator (coordinated communication), Memento (state capture/restore), Observer (event notification), State (state-dependent behavior), Strategy (algorithm selection), Template Method (algorithm skeleton), Visitor (operation separation).
Patterns Are Not Goals
Design patterns describe solutions to recurring problems — they are not architectural goals. Premature pattern application leads to over-engineered code. Start with a simple design, and introduce patterns when you encounter the specific problem they solve. As Martin Fowler notes, “Patterns are not ends in themselves — they are names for useful concepts that experienced developers recognize.”
Modern Alternatives
Modern language features often provide simpler alternatives to classic patterns. Java streams and lambdas replace many uses of the Strategy and Command patterns. Python decorators provide a cleaner syntax than the Decorator pattern. Rust’s traits and generics enable type-safe polymorphism without inheritance. Before applying a pattern, consider whether a language feature or library already solves the problem more directly.
Creational Patterns in Practice
The Builder pattern is especially useful in languages without named parameters. Instead of telescoping constructors with many parameters:
CoffeeOrder order = new CoffeeOrder.Builder()
.size("grande")
.milk("oat")
.shots(3)
.temperature("extra hot")
.build();The Factory Method pattern is useful when a class cannot anticipate the type of objects it needs to create. Frameworks use this extensively — Spring’s FactoryBean interface creates beans with complex initialization logic. The Abstract Factory pattern provides an interface for creating families of related objects without specifying concrete classes, making it ideal for UI toolkits that need to create platform-appropriate widgets.
Structural Patterns in Modern Code
The Adapter pattern converts one interface to another. In modern code, this often manifests as wrapper functions or traits that translate between API versions or protocol formats. The Decorator pattern adds behavior to objects dynamically — Python decorators and Java’s BufferedInputStream wrapping FileInputStream are everyday examples. The Proxy pattern controls access to an object — lazy initialization, access control, logging, and remote proxies are all variations of this pattern.
Behavioral Patterns and Functional Programming
Many behavioral patterns have functional alternatives. The Strategy pattern is naturally expressed as passing a function or lambda. The Observer pattern is implemented with event emitters or reactive streams (RxJS, Reactor). The Command pattern can be replaced with closures or function objects. Learn both the OOP and functional approaches — each has contexts where it is more idiomatic.
Related: SOLID Principles Guide | Software Architecture Patterns