foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • 5
  • quiz
Java
  • Understand the three categories of design patterns
  • Master common creational patterns: Singleton, Factory, Builder
  • Learn essential structural and behavioral patterns

Introduction to Design Patterns

As a developer, you'll hit the same problems repeatedly. How do I ensure only one instance exists? How do I create objects without specifying exact classes? How do I add functionality without modifying code?

Design patterns are proven solutions. Not code you copy-paste, but templates guiding your design. Once you know them, you can say "use a Factory" or "apply Observer" and everyone gets it.

What Are Design Patterns?

The "Gang of Four" book (1994) identified 23 patterns in three categories:

Category Purpose Examples
Creational How objects are created Singleton, Factory, Builder
Structural How objects are composed Adapter, Decorator, Facade
Behavioral How objects interact Observer, Strategy, Command

Each pattern has:

  • A name (the vocabulary)
  • A problem it solves
  • A solution (the structure)
  • Consequences (trade-offs)

Creational Patterns

Singleton Pattern

Problem: You need exactly one instance of a class - a database connection pool, a configuration manager, a logger.

Solution: Make the constructor private and provide a static method to get the single instance.

public class ConfigurationManager {
    // The single instance
    private static ConfigurationManager instance;
    
    private Map<String, String> settings = new HashMap<>();
    
    // Private constructor prevents direct instantiation
    private ConfigurationManager() {
        loadSettings();
    }
    
    // Global access point
    public static ConfigurationManager getInstance() {
        if (instance == null) {
            instance = new ConfigurationManager();
        }
        return instance;
    }
    
    public String getSetting(String key) {
        return settings.get(key);
    }
    
    private void loadSettings() {
        // Load from file, database, etc.
    }
}

// Usage - always get the same instance
ConfigurationManager config = ConfigurationManager.getInstance();
String dbUrl = config.getSetting("database.url");

Thread-safe version (for multi-threaded applications):

public class ConfigurationManager {
    private static volatile ConfigurationManager instance;
    
    private ConfigurationManager() { }
    
    public static ConfigurationManager getInstance() {
        if (instance == null) {                     // First check (no locking)
            synchronized (ConfigurationManager.class) {
                if (instance == null) {             // Second check (with lock)
                    instance = new ConfigurationManager();
                }
            }
        }
        return instance;
    }
}

Modern approach using enum (simplest and safest):

public enum Logger {
    INSTANCE;  // The single instance
    
    public void log(String message) {
        System.out.println(LocalDateTime.now() + ": " + message);
    }
}

// Usage
Logger.INSTANCE.log("Application started");

Factory Pattern

Problem: You need to create objects, but you don't want to specify the exact class. Or you want to centralize object creation logic.

Solution: Define a method that creates and returns objects based on input parameters.

// Product interface
public interface Notification {
    void send(String message);
}

// Concrete products
public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class SMSNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending push notification: " + message);
    }
}

// Factory
public class NotificationFactory {
    public static Notification createNotification(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailNotification();
            case "sms" -> new SMSNotification();
            case "push" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage - client doesn't know concrete classes
Notification notif = NotificationFactory.createNotification("email");
notif.send("Hello, World!");

Why use Factory?

  • Decouples object creation from usage
  • Easy to add new types without changing client code
  • Can return cached or shared instances
  • Hides complex construction logic

Builder Pattern

Problem: You need to create complex objects with many optional parameters. Constructors become unwieldy.

Solution: Separate the construction process from the representation using a builder object.

public class HttpRequest {
    private final String url;           // Required
    private final String method;        // Required
    private final Map<String, String> headers;
    private final String body;
    private final int timeout;
    private final boolean followRedirects;
    
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = builder.headers;
        this.body = builder.body;
        this.timeout = builder.timeout;
        this.followRedirects = builder.followRedirects;
    }
    
    public static class Builder {
        // Required parameters
        private final String url;
        private final String method;
        
        // Optional parameters with defaults
        private Map<String, String> headers = new HashMap<>();
        private String body = "";
        private int timeout = 30000;
        private boolean followRedirects = true;
        
        public Builder(String url, String method) {
            this.url = url;
            this.method = method;
        }
        
        public Builder header(String name, String value) {
            headers.put(name, value);
            return this;
        }
        
        public Builder body(String body) {
            this.body = body;
            return this;
        }
        
        public Builder timeout(int milliseconds) {
            this.timeout = milliseconds;
            return this;
        }
        
        public Builder followRedirects(boolean follow) {
            this.followRedirects = follow;
            return this;
        }
        
        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }
}

// Usage - clear, readable, flexible
HttpRequest request = new HttpRequest.Builder("https://api.example.com", "POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{\"name\": \"John\"}")
    .timeout(5000)
    .build();

Structural Patterns

Adapter Pattern

Problem: You have a class with one interface, but you need it to work with code expecting a different interface.

Solution: Create a wrapper class that translates between the interfaces.

// Existing class with its own interface
public class LegacyPrinter {
    public void printDocument(String text) {
        System.out.println("Printing: " + text);
    }
}

// Interface your code expects
public interface ModernPrinter {
    void print(Document document);
}

// Adapter makes LegacyPrinter work as ModernPrinter
public class PrinterAdapter implements ModernPrinter {
    private LegacyPrinter legacyPrinter;
    
    public PrinterAdapter(LegacyPrinter legacyPrinter) {
        this.legacyPrinter = legacyPrinter;
    }
    
    @Override
    public void print(Document document) {
        // Translate the call
        String text = document.getContent();
        legacyPrinter.printDocument(text);
    }
}

// Usage
LegacyPrinter legacy = new LegacyPrinter();
ModernPrinter printer = new PrinterAdapter(legacy);
printer.print(new Document("Hello"));  // Works with your modern code

Decorator Pattern

Problem: You want to add functionality to objects without modifying their class.

Solution: Wrap objects in decorator classes that add behavior before/after delegating to the wrapped object.

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

// Basic implementation
public class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple coffee";
    }
    
    @Override
    public double getCost() {
        return 2.00;
    }
}

// Base decorator
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
}

// Concrete decorators
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", with milk";
    }
    
    @Override
    public double getCost() {
        return decoratedCoffee.getCost() + 0.50;
    }
}

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

// Usage - stack decorators
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

System.out.println(coffee.getDescription());  // Simple coffee, with milk, with sugar
System.out.println(coffee.getCost());         // 2.75

Behavioral Patterns

Strategy Pattern

Problem: You have multiple algorithms/behaviors and want to select one at runtime.

Solution: Define a family of algorithms, encapsulate each one, and make 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 + " with credit card " + 
                          cardNumber.substring(12));
    }
}

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

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

// Usage - switch strategies at runtime
ShoppingCart cart = new ShoppingCart();

cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(100.00);

cart.setPaymentStrategy(new PayPalPayment("user@email.com"));
cart.checkout(50.00);

Observer Pattern

Problem: When one object changes, you need to notify multiple other objects automatically.

Solution: Define a subscription mechanism where observers register to be notified of changes.

// Observer interface
public interface Observer {
    void update(String event, Object data);
}

// Subject (Observable)
public class NewsAgency {
    private List<Observer> observers = new ArrayList<>();
    private String latestNews;
    
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    public void setNews(String news) {
        this.latestNews = news;
        notifyObservers();
    }
    
    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update("news", latestNews);
        }
    }
}

// Concrete observers
public class EmailSubscriber implements Observer {
    private String email;
    
    public EmailSubscriber(String email) {
        this.email = email;
    }
    
    @Override
    public void update(String event, Object data) {
        System.out.println("Sending email to " + email + ": " + data);
    }
}

public class MobileSubscriber implements Observer {
    private String phoneNumber;
    
    public MobileSubscriber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }
    
    @Override
    public void update(String event, Object data) {
        System.out.println("Sending SMS to " + phoneNumber + ": " + data);
    }
}

// Usage
NewsAgency agency = new NewsAgency();
agency.addObserver(new EmailSubscriber("user@email.com"));
agency.addObserver(new MobileSubscriber("555-1234"));

agency.setNews("Breaking: Java 21 released!");
// Both subscribers are notified automatically

Choosing the Right Pattern

Problem Pattern Key Benefit
Need exactly one instance Singleton Global access, controlled instantiation
Object creation varies Factory Decouples creation from usage
Many optional parameters Builder Readable, flexible construction
Incompatible interfaces Adapter Integrates existing code
Add behavior dynamically Decorator Flexible extension without inheritance
Interchangeable algorithms Strategy Runtime behavior selection
Automatic notifications Observer Loose coupling between objects

Design patterns are solutions to recurring problems—not code templates. Learn the names so you can communicate ("use a Factory"). Don't force patterns where they're not needed. Understand the problem first. Patterns often work together (Factory creating Strategy objects). Modern Java features (lambdas, records) can simplify some patterns.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service