Open In App

Design Patterns in Java

Last Updated : 19 Aug, 2025
Comments
Improve
Suggest changes
1 Likes
Like
Report

Design patterns are reusable solutions to commonly occurring problems in software design. They act like blueprints or templates that you can customize and use to solve a particular design problem in your code.

They aren’t code themselves, they describe best practices that can be adapted to solve frequently occurring design challenges especially in object-oriented languages like Java.

Key Benefits of Design Patterns

  • Reusability: Solutions can be applied to similar problems across different projects.
  • Communication: Provides a common vocabulary for developers.
  • Best Practices: Use proven solutions and coding standards.
  • Maintainability: Makes code more organized and easier to modify.
  • Flexibility: Allows for easier adaptation to changing requirements.

Why Learn Design Patterns in Java?

  • Java’s OOP nature makes it perfect to apply design patterns.
  • Used in frameworks like Spring, Hibernate, and Java Collections.
  • Helps in interviews and large-scale project development.

Types of Design Patterns

1. Creational Patterns

These patterns deal with object creation mechanisms, trying to create objects in a manner suitable for the situation. They help make a system independent of how its objects are created, composed, and represented.

Examples: Singleton, Factory, Builder, Prototype

2. Structural Patterns

These patterns deal with object composition and relationships between objects. They make it easier to structure classes and objects in a way that simplifies code maintenance and improves flexibility.

Examples: Adapter, Decorator, Facade, Composite

3. Behavioral Patterns

These patterns focus on communication between objects and the assignment of responsibilities. They help manage how objects interact in a system, making it more efficient and easier to modify.

Examples: Observer, Strategy, Command, Template Method

Popular Design Patterns

1. Singleton Pattern (Creational)

The Singleton pattern ensures that a class has only one instance and provides global access to it. It controls object creation to avoid multiple instances and ensures consistent access to shared resources.

When to use: When you need exactly one instance of a class (e.g., database connection, logger).

Example:

Java
class Singleton {
    // Private static instance
    private static Singleton instance;
    
    // Private constructor to prevent instantiation
    private Singleton() {
        // Initialization code
    }
    
    // Public method to get the instance. If instance is not null, new will not be initialized.
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    public void displayMessage() {
        System.out.println("Hello from Singleton!");
    }
}

public class Main {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        
        s1.displayMessage();
        System.out.println(s1 == s2); // true - same instance
    }
}

Output
Hello from Singleton!
true

Real World Examples: Runtime class in Java, Spring beans with singleton scope.

2. Factory Pattern (Creational)

The Factory pattern creates objects without specifying their exact classes, promoting loose coupling. It encapsulates the object creation logic, making the code more maintainable and scalable.

When to use: When you need to create objects based on certain conditions or parameters.

Example:

Java
// Product interface
interface Animal {
    void makeSound();
}

// Concrete products
class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

// Factory class
class AnimalFactory {
    public static Animal createAnimal(String type) {
        switch (type.toLowerCase()) {
            case "dog":
                return new Dog();
            case "cat":
                return new Cat();
            default:
                throw new IllegalArgumentException("Unknown animal type");
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Animal dog = AnimalFactory.createAnimal("dog");
        Animal cat = AnimalFactory.createAnimal("cat");
        
        dog.makeSound(); // Woof!
        cat.makeSound(); // Meow!
    }
}

Output
Woof!
Meow!

Real World Examples: JDBC DriverManager.getConnection(), Calendar.getInstance().

3. Observer Pattern (Behavioral)

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified automatically.

When to use: When changes to one object require updating multiple dependent objects.

Example:

Java
import java.util.*;

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

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

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

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

// Usage
public class Main {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();
        
        NewsChannel channel1 = new NewsChannel("CNN");
        NewsChannel channel2 = new NewsChannel("BBC");
        
        agency.addObserver(channel1);
        agency.addObserver(channel2);
        
        agency.setNews("Breaking: New Java version released!");
    }
}

Output
CNN received news: Breaking: New Java version released!
BBC received news: Breaking: New Java version released!

Real World Examples: java.util.Observer / Observable (Deprecated in newer versions).

4. Strategy Pattern (Behavioral)

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to use: When you have multiple ways to perform a task and want to choose the algorithm at runtime.

Example:

Java
import java.util.*;

// Strategy interface
interface SortingStrategy {
    void sort(int[] array);
}

// Concrete strategies
class BubbleSort implements SortingStrategy {
    @Override
    public void sort(int[] array) {
        System.out.println("Sorting using Bubble Sort");
        // Implementation would go here
        Arrays.sort(array); // Using built-in for simplicity
    }
}

class QuickSort implements SortingStrategy {
    @Override
    public void sort(int[] array) {
        System.out.println("Sorting using Quick Sort");
        // Implementation would go here
        Arrays.sort(array); // Using built-in for simplicity
    }
}

// Context class
class SortingContext {
    private SortingStrategy strategy;
    
    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void executeStrategy(int[] array) {
        strategy.sort(array);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        int[] data = {5, 2, 8, 1, 9};
        SortingContext context = new SortingContext();
        
        // Use bubble sort
        context.setStrategy(new BubbleSort());
        context.executeStrategy(data);
        
        // Switch to quick sort
        context.setStrategy(new QuickSort());
        context.executeStrategy(data);
    }
}

Output
Sorting using Bubble Sort
Sorting using Quick Sort

Real World Examples: Spring's PlatformTransactionManager, Java Logging API Formatter

How to Choose the Right Pattern

Choosing the right Design Pattern can be complicated or confusing sometimes. Here are some quick steps to help you choosing it.

1. Spot the problem first: Is it about creating objects, structuring them or making them talk nicely?

2. Match the category:

  • Creation issue: Look at Creational Patterns
  • Relationship/structure issue: Check Structural Patterns
  • Communication/behavior issue: Explore Behavioral Patterns

3. Read the intent, not just the code: The “why” matters more than “how.”

4. Don’t overcomplicate: Pick the simplest pattern that solves the problem. You should not use them all just to show off.

5. Weigh pros and cons: Some patterns give flexibility but can add extra layers and complexity.

6. Steal from the best: If Java or Spring already uses a pattern for your case, reuse the idea.

Note: Remember that design patterns are solutions to common problems, not rules that must always be followed. Practice implementing these patterns in small projects, and gradually you'll develop the intuition to know when and how to apply them effectively in larger applications.


Article Tags :

Explore