Design Patterns

Proven Solutions to Common Programming Problems

← Back to Index

What Are Design Patterns?

Think of design patterns like IKEA furniture instructions:

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

  1. Creational Patterns: How to create objects (Singleton, Factory, Builder)
  2. Structural Patterns: How to compose objects (Adapter, Decorator, Facade)
  3. 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

Java Collections

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)