BETA
Share feedback

Design Patterns

Design patterns are proven solutions to common problems in software design. They represent best practices and provide a shared vocabulary for developers to communicate complex design concepts efficiently.

What are Design Patterns?

Design patterns are reusable solutions to commonly occurring problems in software design. They are templates that can be applied to real-world programming problems, providing:

  • Proven Solutions: Time-tested approaches to common problems
  • Common Vocabulary: Shared language for developers
  • Best Practices: Encapsulated expertise from experienced developers
  • Flexibility: Adaptable solutions for various contexts

Gang of Four (GoF)

The most famous design patterns come from the book "Design Patterns: Elements of Reusable Object-Oriented Software" by the Gang of Four (Gamma, Helm, Johnson, and Vlissides). They identified 23 fundamental patterns that form the foundation of modern software design.

Pattern Categories

Design patterns are typically categorized into three main types:

Creational

Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

• Singleton
• Factory Method
• Abstract Factory
• Builder
• Prototype

Structural

Deal with object composition and relationships between objects to form larger structures.

• Adapter
• Bridge
• Composite
• Decorator
• Facade
• Flyweight
• Proxy

Behavioral

Focus on communication between objects and the assignment of responsibilities.

• Observer
• Strategy
• Command
• State
• Template Method
• Chain of Responsibility
• Visitor

Creational Patterns

1. Singleton Pattern

Ensures a class has only one instance and provides global access to it.

public class DatabaseConnection {
    private static DatabaseConnection instance;
    private static final Object lock = new Object();
    
    private DatabaseConnection() {
        // Private constructor prevents instantiation
    }
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
    
    public void connect() {
        System.out.println("Connected to database");
    }
}

When to use: Database connections, logging, configuration settings, thread pools.

2. Factory Method Pattern

Creates objects without specifying their exact classes.

// Product interface
public interface Vehicle {
    void start();
    void stop();
}

// Concrete products
public class Car implements Vehicle {
    public void start() { System.out.println("Car started"); }
    public void stop() { System.out.println("Car stopped"); }
}

public class Motorcycle implements Vehicle {
    public void start() { System.out.println("Motorcycle started"); }
    public void stop() { System.out.println("Motorcycle stopped"); }
}

// Factory
public class VehicleFactory {
    public static Vehicle createVehicle(String type) {
        switch (type.toLowerCase()) {
            case "car": return new Car();
            case "motorcycle": return new Motorcycle();
            default: throw new IllegalArgumentException("Unknown vehicle type");
        }
    }
}

// Usage
Vehicle vehicle = VehicleFactory.createVehicle("car");
vehicle.start();

3. Builder Pattern

Constructs complex objects step by step.

public class Computer {
    private String CPU;
    private String RAM;
    private String storage;
    private String GPU;
    private boolean hasWiFi;
    
    private Computer(Builder builder) {
        this.CPU = builder.CPU;
        this.RAM = builder.RAM;
        this.storage = builder.storage;
        this.GPU = builder.GPU;
        this.hasWiFi = builder.hasWiFi;
    }
    
    public static class Builder {
        private String CPU;
        private String RAM;
        private String storage;
        private String GPU;
        private boolean hasWiFi;
        
        public Builder setCPU(String CPU) {
            this.CPU = CPU;
            return this;
        }
        
        public Builder setRAM(String RAM) {
            this.RAM = RAM;
            return this;
        }
        
        public Builder setStorage(String storage) {
            this.storage = storage;
            return this;
        }
        
        public Builder setGPU(String GPU) {
            this.GPU = GPU;
            return this;
        }
        
        public Builder setWiFi(boolean hasWiFi) {
            this.hasWiFi = hasWiFi;
            return this;
        }
        
        public Computer build() {
            return new Computer(this);
        }
    }
}

// Usage
Computer computer = new Computer.Builder()
    .setCPU("Intel i7")
    .setRAM("16GB")
    .setStorage("512GB SSD")
    .setGPU("NVIDIA RTX 3080")
    .setWiFi(true)
    .build();

Structural Patterns

1. Adapter Pattern

Allows incompatible interfaces to work together.

// Target interface
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee (existing class with incompatible interface)
public class AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }
    
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }
}

// Adapter
public class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedPlayer;
    
    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            advancedPlayer = new AdvancedMediaPlayer();
        }
    }
    
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedPlayer.playMp4(fileName);
        }
    }
}

// Client
public class AudioPlayer implements MediaPlayer {
    private MediaAdapter mediaAdapter;
    
    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file: " + fileName);
        } else {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        }
    }
}

2. Decorator Pattern

Adds new functionality to objects dynamically without altering their structure.

// Component interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete component
public class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple coffee";
    }
    
    @Override
    public double getCost() {
        return 2.0;
    }
}

// Base decorator
public 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
public 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;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + ", sugar";
    }
    
    @Override
    public double getCost() {
        return coffee.getCost() + 0.2;
    }
}

// Usage
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " costs $" + coffee.getCost());

Behavioral Patterns

1. Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified.

import java.util.*;

// Observer interface
public interface Observer {
    void update(String message);
}

// Subject interface
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

// Concrete subject
public class NewsAgency implements Subject {
    private List<Observer> observers;
    private String news;
    
    public NewsAgency() {
        this.observers = new ArrayList<>();
    }
    
    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
    
    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }
}

// Concrete observer
public class NewsChannel implements Observer {
    private String name;
    
    public NewsChannel(String name) {
        this.name = name;
    }
    
    @Override
    public void update(String news) {
        System.out.println(name + " received news: " + news);
    }
}

// Usage
NewsAgency agency = new NewsAgency();
NewsChannel cnn = new NewsChannel("CNN");
NewsChannel bbc = new NewsChannel("BBC");

agency.addObserver(cnn);
agency.addObserver(bbc);
agency.setNews("Breaking: New design pattern discovered!");

2. Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

// Strategy interface
public interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategies
public 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);
    }
}

public 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: " + email);
    }
}

// Context
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    public void checkout(double amount) {
        paymentStrategy.pay(amount);
    }
}

// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(100.0);

cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(50.0);

3. Command Pattern

Encapsulates a request as an object, allowing you to parameterize clients with different requests.

// Command interface
public interface Command {
    void execute();
    void undo();
}

// Receiver
public 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
public class LightOnCommand implements Command {
    private Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.turnOn();
    }
    
    @Override
    public void undo() {
        light.turnOff();
    }
}

public class LightOffCommand implements Command {
    private Light light;
    
    public LightOffCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void execute() {
        light.turnOff();
    }
    
    @Override
    public void undo() {
        light.turnOn();
    }
}

// Invoker
public 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) {
            lastCommand.undo();
        }
    }
}

// Usage
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);

RemoteControl remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton(); // Light is ON
remote.pressUndo();   // Light is OFF

Modern Patterns

1. Dependency Injection

Provides dependencies to an object rather than having it create them itself.

// Service interface
public interface EmailService {
    void sendEmail(String to, String message);
}

// Service implementation
public class GmailService implements EmailService {
    @Override
    public void sendEmail(String to, String message) {
        System.out.println("Sending email via Gmail to: " + to);
    }
}

// Client class
public class UserService {
    private EmailService emailService;
    
    // Constructor injection
    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    public void registerUser(String email) {
        // Registration logic...
        emailService.sendEmail(email, "Welcome!");
    }
}

// Usage
EmailService emailService = new GmailService();
UserService userService = new UserService(emailService);
userService.registerUser("user@example.com");

2. Repository Pattern

Encapsulates data access logic and provides a more object-oriented view of the persistence layer.

// Entity
public class User {
    private Long id;
    private String name;
    private String email;
    
    // Constructors, getters, setters...
}

// Repository interface
public interface UserRepository {
    User findById(Long id);
    List<User> findAll();
    User save(User user);
    void delete(Long id);
    List<User> findByEmail(String email);
}

// Repository implementation
public class DatabaseUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        // Database query logic
        return new User(); // Simplified
    }
    
    @Override
    public List<User> findAll() {
        // Database query logic
        return new ArrayList<>(); // Simplified
    }
    
    @Override
    public User save(User user) {
        // Database save logic
        return user; // Simplified
    }
    
    @Override
    public void delete(Long id) {
        // Database delete logic
    }
    
    @Override
    public List<User> findByEmail(String email) {
        // Database query logic
        return new ArrayList<>(); // Simplified
    }
}

Pattern Selection Guidelines

Choosing the Right Pattern

Consider these factors when selecting patterns:

  1. Problem Context: What specific problem are you trying to solve?
  2. Flexibility Requirements: How much change do you anticipate?
  3. Performance Constraints: Are there speed or memory limitations?
  4. Team Familiarity: How well does your team know the pattern?
  5. Maintenance Overhead: Will the pattern make code harder to maintain?
  6. Testing Requirements: Does the pattern support easy testing?

Anti-Patterns to Avoid

1. God Object

A class that knows too much or does too much.

// Bad: God object
public class OrderManager {
    public void validateOrder() { /* ... */ }
    public void calculateTax() { /* ... */ }
    public void processPayment() { /* ... */ }
    public void sendEmail() { /* ... */ }
    public void updateInventory() { /* ... */ }
    public void generateReport() { /* ... */ }
}

// Good: Separated responsibilities
public class OrderValidator { /* ... */ }
public class TaxCalculator { /* ... */ }
public class PaymentProcessor { /* ... */ }
public class EmailService { /* ... */ }
public class InventoryManager { /* ... */ }
public class ReportGenerator { /* ... */ }

2. Singleton Abuse

Using Singleton when a regular class would suffice.

// Bad: Unnecessary singleton
public class MathUtils {
    private static MathUtils instance;
    
    public static MathUtils getInstance() {
        if (instance == null) {
            instance = new MathUtils();
        }
        return instance;
    }
    
    public int add(int a, int b) {
        return a + b;
    }
}

// Good: Static utility class
public class MathUtils {
    private MathUtils() {} // Prevent instantiation
    
    public static int add(int a, int b) {
        return a + b;
    }
}

Best Practices

  1. Don't force patterns: Use patterns to solve actual problems, not for the sake of using patterns
  2. Start simple: Begin with the simplest solution and refactor to patterns when needed
  3. Understand the trade-offs: Every pattern has benefits and drawbacks
  4. Consider alternatives: Sometimes a simple solution is better than a complex pattern
  5. Document your decisions: Explain why you chose a particular pattern
  6. Test thoroughly: Patterns can introduce complexity that needs testing
  7. Keep it readable: Code should be understandable by other developers

Interview Preparation

Common Questions:

  1. Explain the Singleton pattern and its thread-safety issues
  2. When would you use Factory vs Builder pattern?
  3. What's the difference between Adapter and Facade patterns?
  4. How does the Observer pattern support loose coupling?
  5. Explain the Strategy pattern with a real-world example

Practice Exercises:

  1. Implement a logging system using multiple patterns
  2. Design a media player using Strategy and Factory patterns
  3. Create a notification system using Observer pattern
  4. Build a file processing system using Command pattern

Next Steps

Continue your learning journey:

  1. SOLID Principles - Learn design principles
  2. Parking Lot System - Apply patterns in practice
  3. System Design Projects - Build real systems

Master these patterns through practice and real-world application! 🚀