What Are Design Patterns?
Think of design patterns like IKEA furniture instructions:
- 🪑 You don't reinvent how to build a chair - you follow a proven design
- 📘 Patterns are reusable solutions that experienced developers have refined
- 🗣️ They provide a common language: "Use a Singleton" instantly communicates the approach
What Is a Design Pattern?
A design pattern is a general reusable solution to a commonly occurring problem in software design. It's not finished code you can copy-paste, but a template for how to solve a problem.
Origin: The "Gang of Four" (GoF) published 23 classic patterns in 1994 that are still widely used today.
Three Categories of Patterns
- Creational Patterns: How to create objects (Singleton, Factory, Builder)
- Structural Patterns: How to compose objects (Adapter, Decorator, Facade)
- Behavioral Patterns: How objects interact (Observer, Strategy, Command)
Creational Patterns
1. Singleton Pattern - "Only One Instance"
Analogy: A country has only ONE president at a time. No matter how many times you ask "Who's the president?", you get the same person.
Problem: You need exactly one instance of a class (e.g., database connection, configuration manager).
// Thread-safe Singleton
public class Database {
// 1. Private static instance
private static Database instance;
// 2. Private constructor (can't create with 'new')
private Database() {
System.out.println("Database connection created!");
}
// 3. Public static method to get instance
public static synchronized Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
public void query(String sql) {
System.out.println("Executing: " + sql);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Database db1 = Database.getInstance(); // Creates instance
Database db2 = Database.getInstance(); // Returns same instance
System.out.println(db1 == db2); // true - Same object!
db1.query("SELECT * FROM users");
}
}
Output:
Database connection created!
true
Executing: SELECT * FROM users
Better approach (modern Java):
// Enum Singleton - Best practice!
public enum Database {
INSTANCE; // Only one instance, thread-safe automatically
public void query(String sql) {
System.out.println("Executing: " + sql);
}
}
// Usage
Database.INSTANCE.query("SELECT * FROM users");
2. Factory Pattern - "Let a Factory Create Objects"
Analogy: You go to a car dealership and say "I want an SUV." The dealer handles which specific model to give you.
Problem: You need to create objects but don't know the exact class until runtime.
// Product interface
interface Animal {
void speak();
}
// Concrete products
class Dog implements Animal {
@Override
public void speak() {
System.out.println("Woof!");
}
}
class Cat implements Animal {
@Override
public void speak() {
System.out.println("Meow!");
}
}
class Bird implements Animal {
@Override
public void speak() {
System.out.println("Chirp!");
}
}
// Factory class
class AnimalFactory {
public static Animal createAnimal(String type) {
switch (type.toLowerCase()) {
case "dog":
return new Dog();
case "cat":
return new Cat();
case "bird":
return new Bird();
default:
throw new IllegalArgumentException("Unknown animal: " + type);
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Animal pet1 = AnimalFactory.createAnimal("dog");
Animal pet2 = AnimalFactory.createAnimal("cat");
pet1.speak(); // Woof!
pet2.speak(); // Meow!
}
}
Why use it? Client code doesn't need to know about Dog, Cat, or Bird classes. Just ask the factory!
3. Builder Pattern - "Build Complex Objects Step-by-Step"
Analogy: Ordering a custom pizza: "Large, thin crust, extra cheese, pepperoni, olives, no mushrooms."
Problem: Constructors with many parameters are hard to read and maintain.
// Without Builder (ugly!)
User user = new User("Alice", "alice@example.com", 25, "123 Main St", "555-1234", true);
// With Builder (clean!)
User user = new User.Builder()
.name("Alice")
.email("alice@example.com")
.age(25)
.address("123 Main St")
.phone("555-1234")
.premium(true)
.build();
Implementation:
public class User {
// Final fields (immutable)
private final String name;
private final String email;
private final int age;
private final String address;
private final String phone;
private final boolean premium;
// Private constructor
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
this.premium = builder.premium;
}
// Static nested Builder class
public static class Builder {
// Required parameters
private final String name;
private final String email;
// Optional parameters with defaults
private int age = 0;
private String address = "";
private String phone = "";
private boolean premium = false;
public Builder(String name, String email) {
this.name = name;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this; // Return this for chaining
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder premium(boolean premium) {
this.premium = premium;
return this;
}
public User build() {
return new User(this);
}
}
@Override
public String toString() {
return "User{name='" + name + "', email='" + email + "', age=" + age +
", premium=" + premium + "}";
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Only required fields
User user1 = new User.Builder("Bob", "bob@example.com")
.build();
// All fields
User user2 = new User.Builder("Alice", "alice@example.com")
.age(25)
.address("123 Main St")
.phone("555-1234")
.premium(true)
.build();
System.out.println(user1);
System.out.println(user2);
}
}
Output:
User{name='Bob', email='bob@example.com', age=0, premium=false}
User{name='Alice', email='alice@example.com', age=25, premium=true}
Benefits: Readable, flexible, immutable objects, optional parameters
Structural Patterns
4. Adapter Pattern - "Make Incompatible Interfaces Work Together"
Analogy: A power adapter lets you plug a US device into a European outlet. The adapter translates between them.
Problem: You have an existing class with an interface that doesn't match what you need.
// Old system (incompatible interface)
class OldPrinter {
public void printOldFormat(String text) {
System.out.println("[OLD FORMAT] " + text);
}
}
// New system expects this interface
interface ModernPrinter {
void print(String text);
}
// Adapter makes OldPrinter work as ModernPrinter
class PrinterAdapter implements ModernPrinter {
private OldPrinter oldPrinter;
public PrinterAdapter(OldPrinter oldPrinter) {
this.oldPrinter = oldPrinter;
}
@Override
public void print(String text) {
// Translate new interface to old interface
oldPrinter.printOldFormat(text);
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Old printer with old interface
OldPrinter oldPrinter = new OldPrinter();
// Wrap it in adapter to use as ModernPrinter
ModernPrinter printer = new PrinterAdapter(oldPrinter);
printer.print("Hello World"); // Uses old printer via adapter
}
}
Output:
[OLD FORMAT] Hello World
5. Decorator Pattern - "Add Features Dynamically"
Analogy: Basic coffee costs $2. Add milk (+$0.50), then whipped cream (+$0.70), then caramel (+$0.60). Each addition "decorates" the base coffee.
Problem: You want to add responsibilities to objects without subclassing.
// Component interface
interface Coffee {
String getDescription();
double getCost();
}
// Concrete component
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// Decorator base class
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Whipped Cream";
}
@Override
public double getCost() {
return coffee.getCost() + 0.7;
}
}
class CaramelDecorator extends CoffeeDecorator {
public CaramelDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Caramel";
}
@Override
public double getCost() {
return coffee.getCost() + 0.6;
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Start with simple coffee
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Add milk
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Add whipped cream
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Add caramel
coffee = new CaramelDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
}
}
Output:
Simple Coffee - $2.0
Simple Coffee, Milk - $2.5
Simple Coffee, Milk, Whipped Cream - $3.2
Simple Coffee, Milk, Whipped Cream, Caramel - $3.8
Benefits: Add features dynamically, combine features flexibly, avoid class explosion
6. Facade Pattern - "Provide a Simple Interface"
Analogy: A TV remote control hides the complexity of the TV's internal circuits. One "Power" button does many things behind the scenes.
Problem: A complex subsystem is hard to use. Provide a simpler interface.
// Complex subsystem
class CPU {
public void freeze() { System.out.println("CPU: Freezing..."); }
public void jump(long position) { System.out.println("CPU: Jumping to " + position); }
public void execute() { System.out.println("CPU: Executing..."); }
}
class Memory {
public void load(long position, byte[] data) {
System.out.println("Memory: Loading data at " + position);
}
}
class HardDrive {
public byte[] read(long lba, int size) {
System.out.println("HardDrive: Reading " + size + " bytes from " + lba);
return new byte[size];
}
}
// Facade - Simple interface
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
System.out.println("Starting computer...");
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
System.out.println("Computer started!");
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Without Facade - complex!
CPU cpu = new CPU();
Memory memory = new Memory();
HardDrive hardDrive = new HardDrive();
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
System.out.println("---");
// With Facade - simple!
ComputerFacade computer = new ComputerFacade();
computer.start(); // One method does everything!
}
}
Output:
CPU: Freezing...
Memory: Loading data at 0
HardDrive: Reading 1024 bytes from 0
CPU: Jumping to 0
CPU: Executing...
---
Starting computer...
CPU: Freezing...
HardDrive: Reading 1024 bytes from 0
Memory: Loading data at 0
CPU: Jumping to 0
CPU: Executing...
Computer started!
Behavioral Patterns
7. Observer Pattern - "Subscribe to Events"
Analogy: YouTube subscriptions. When a channel posts a video, all subscribers get notified automatically.
Problem: When one object changes state, multiple other objects need to be notified.
import java.util.*;
// Observer interface
interface Observer {
void update(String message);
}
// Subject (Observable)
class Channel {
private List subscribers = new ArrayList<>();
private String channelName;
public Channel(String name) {
this.channelName = name;
}
// Subscribe
public void subscribe(Observer observer) {
subscribers.add(observer);
System.out.println("New subscriber!");
}
// Unsubscribe
public void unsubscribe(Observer observer) {
subscribers.remove(observer);
System.out.println("Unsubscribed!");
}
// Notify all observers
public void uploadVideo(String videoTitle) {
System.out.println(channelName + " uploaded: " + videoTitle);
notifyObservers("New video: " + videoTitle);
}
private void notifyObservers(String message) {
for (Observer observer : subscribers) {
observer.update(message);
}
}
}
// Concrete observer
class Subscriber implements Observer {
private String name;
public Subscriber(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received notification: " + message);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Channel javaChannel = new Channel("JavaMaster");
// Create subscribers
Subscriber alice = new Subscriber("Alice");
Subscriber bob = new Subscriber("Bob");
Subscriber charlie = new Subscriber("Charlie");
// Subscribe
javaChannel.subscribe(alice);
javaChannel.subscribe(bob);
javaChannel.subscribe(charlie);
System.out.println("---");
// Upload video - all subscribers notified
javaChannel.uploadVideo("Design Patterns Explained");
System.out.println("---");
// Bob unsubscribes
javaChannel.unsubscribe(bob);
// Upload another video - only Alice and Charlie notified
javaChannel.uploadVideo("Advanced Java Tips");
}
}
Output:
New subscriber!
New subscriber!
New subscriber!
---
JavaMaster uploaded: Design Patterns Explained
Alice received notification: New video: Design Patterns Explained
Bob received notification: New video: Design Patterns Explained
Charlie received notification: New video: Design Patterns Explained
---
Unsubscribed!
JavaMaster uploaded: Advanced Java Tips
Alice received notification: New video: Advanced Java Tips
Charlie received notification: New video: Advanced Java Tips
8. Strategy Pattern - "Choose Algorithm at Runtime"
Analogy: Navigation app lets you choose: fastest route, shortest route, or scenic route. Same destination, different strategies.
Problem: You have multiple ways to do something and want to switch between them easily.
// Strategy interface
interface PaymentStrategy {
void pay(double amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Credit Card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal account " + email);
}
}
class CryptoPayment implements PaymentStrategy {
private String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Crypto wallet " + walletAddress);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
if (paymentStrategy == null) {
System.out.println("Please select a payment method!");
return;
}
paymentStrategy.pay(amount);
}
}
// Usage
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Pay with credit card
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(100.50);
// Switch to PayPal
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(75.25);
// Switch to Crypto
cart.setPaymentStrategy(new CryptoPayment("0x1234abcd"));
cart.checkout(200.00);
}
}
Output:
Paid $100.5 using Credit Card 1234-5678-9012-3456
Paid $75.25 using PayPal account user@example.com
Paid $200.0 using Crypto wallet 0x1234abcd
Benefits: Easy to add new strategies, switch at runtime, clean separation of algorithms
9. Command Pattern - "Encapsulate Requests as Objects"
Analogy: Remote control buttons. Each button encapsulates a command: "turn on TV", "change channel", etc. You can also undo commands.
Problem: You want to parameterize objects with operations, queue operations, or support undo.
// Command interface
interface Command {
void execute();
void undo();
}
// Receiver
class Light {
private boolean isOn = false;
public void turnOn() {
isOn = true;
System.out.println("Light is ON");
}
public void turnOff() {
isOn = false;
System.out.println("Light is OFF");
}
}
// Concrete commands
class TurnOnCommand implements Command {
private Light light;
public TurnOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
class TurnOffCommand implements Command {
private Light light;
public TurnOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
@Override
public void undo() {
light.turnOn();
}
}
// Invoker
class RemoteControl {
private Command command;
private Command lastCommand;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
lastCommand = command;
}
public void pressUndo() {
if (lastCommand != null) {
System.out.print("Undo: ");
lastCommand.undo();
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Light livingRoomLight = new Light();
Command turnOn = new TurnOnCommand(livingRoomLight);
Command turnOff = new TurnOffCommand(livingRoomLight);
RemoteControl remote = new RemoteControl();
// Turn on
remote.setCommand(turnOn);
remote.pressButton();
// Turn off
remote.setCommand(turnOff);
remote.pressButton();
// Undo (turns back on)
remote.pressUndo();
}
}
Output:
Light is ON
Light is OFF
Undo: Light is ON
Benefits: Decouple sender from receiver, support undo/redo, queue commands
Pattern Comparison
| Pattern | Purpose | When to Use | Example |
|---|---|---|---|
| Singleton | One instance only | Database connections, config | Database.getInstance() |
| Factory | Create objects without specifying exact class | Multiple product types | AnimalFactory.create("dog") |
| Builder | Construct complex objects step-by-step | Many optional parameters | new User.Builder().name().email().build() |
| Adapter | Make incompatible interfaces work | Integrating old code | new PrinterAdapter(oldPrinter) |
| Decorator | Add features dynamically | Flexible feature combinations | new MilkDecorator(coffee) |
| Facade | Simplify complex subsystem | Complex APIs | computer.start() |
| Observer | Notify multiple objects of changes | Event systems, subscriptions | channel.subscribe(observer) |
| Strategy | Choose algorithm at runtime | Multiple ways to do something | cart.setPaymentStrategy(paypal) |
| Command | Encapsulate requests as objects | Undo/redo, queue operations | remote.setCommand(turnOn) |
Anti-Patterns (What NOT to Do)
Anti-patterns are common bad solutions that seem good at first but cause problems:
1. God Object
// ❌ BAD: One class does EVERYTHING
class Application {
public void connectDatabase() { }
public void validateUser() { }
public void sendEmail() { }
public void processPayment() { }
public void generateReport() { }
public void logErrors() { }
// ... 50 more methods
}
// ✅ GOOD: Separate responsibilities
class DatabaseManager { }
class UserValidator { }
class EmailService { }
class PaymentProcessor { }
class ReportGenerator { }
class Logger { }
2. Spaghetti Code
// ❌ BAD: Tangled, hard to follow
if (user != null) {
if (user.isActive()) {
if (!user.isBlocked()) {
if (user.hasPermission("admin")) {
// do something
} else {
// do something else
}
}
}
}
// ✅ GOOD: Early returns, clear flow
if (user == null) return;
if (!user.isActive()) return;
if (user.isBlocked()) return;
if (!user.hasPermission("admin")) return;
// do something
3. Copy-Paste Programming
// ❌ BAD: Duplicated code
public void saveUser(User user) {
Connection conn = DriverManager.getConnection(url);
PreparedStatement stmt = conn.prepareStatement("INSERT INTO users...");
stmt.setString(1, user.getName());
stmt.execute();
stmt.close();
conn.close();
}
public void saveProduct(Product product) {
Connection conn = DriverManager.getConnection(url); // Duplicate!
PreparedStatement stmt = conn.prepareStatement("INSERT INTO products...");
stmt.setString(1, product.getName());
stmt.execute();
stmt.close();
conn.close();
}
// ✅ GOOD: Reusable method
public void save(T entity, String sql, Consumer setter) {
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement stmt = conn.prepareStatement(sql)) {
setter.accept(stmt);
stmt.execute();
}
}
Real-World Framework Examples
Spring Framework
- Singleton: Spring beans are singletons by default
- Factory: BeanFactory creates beans
- Proxy: @Transactional uses proxies
- Template Method: JdbcTemplate, RestTemplate
Java Collections
- Iterator: for (Item item : list) { } uses Iterator pattern
- Decorator: Collections.synchronizedList() wraps a list
- Adapter: Arrays.asList() adapts array to List
Java I/O
// Decorator pattern in action!
BufferedReader reader = new BufferedReader( // Adds buffering
new InputStreamReader( // Converts bytes to chars
new FileInputStream("file.txt") // Reads file
)
);
Best Practices
✅ DO:
- Learn patterns gradually - Don't memorize all 23 at once
- Understand the problem first - Pattern is the solution
- Use patterns when they fit - Not every problem needs a pattern
- Start with common patterns - Singleton, Factory, Strategy, Observer
- Study real frameworks - See how Spring, JUnit use patterns
- Keep it simple - Simple code > complex pattern
- Refactor to patterns - Add patterns as code grows
❌ DON'T:
- Don't force patterns - "I must use a pattern!" is wrong mindset
- Don't over-engineer - Simple problem = simple solution
- Don't use patterns you don't understand - Learn it properly first
- Don't mix too many patterns - Can become confusing
- Don't ignore SOLID principles - Patterns build on these
When to Use Each Pattern
Starting a new project?
- Use Factory for object creation flexibility
- Use Builder for complex configuration objects
- Use Singleton for shared resources (carefully!)
Working with legacy code?
- Use Adapter to integrate old systems
- Use Facade to simplify complex APIs
- Use Decorator to add features without changing code
Building flexible systems?
- Use Strategy for swappable algorithms
- Use Observer for event-driven architecture
- Use Command for undo/redo functionality
Summary
- Design patterns are proven solutions to common problems
- Three categories: Creational, Structural, Behavioral
- Creational: How to create objects (Singleton, Factory, Builder)
- Structural: How to compose objects (Adapter, Decorator, Facade)
- Behavioral: How objects interact (Observer, Strategy, Command)
- Benefits: Code reuse, common vocabulary, proven solutions
- Don't overuse: Simple problem = simple solution
- Learn by doing: Refactor existing code to use patterns
- Study frameworks: See patterns in action (Spring, JUnit)