Top 10 Design Patterns Every Java Developer Should Know 🚀

Design patterns are proven solutions to common software design problems. They help make code more readable, reusable, maintainable, and scalable. Java developers use design patterns extensively to follow best practices and improve software architecture.

This article covers 10 essential design patterns that every Java developer should know and how to implement them the right way using best practices.

1. Singleton Pattern (Ensuring a Single Instance)

Use Case: Managing database connections, logging, and configuration settings

The Singleton Pattern ensures that only one instance of a class exists and provides a global access point to it.

Best Practice Implementation (Thread-Safe & Secure Singleton)

public class Singleton {

    // Volatile ensures visibility across threads and prevents instruction reordering
    private static volatile Singleton instance;

    // Private constructor prevents direct instantiation
    private Singleton() {
        if (instance != null) {
            throw new RuntimeException("Use getInstance() to create an object");
        }
    }

    // Double-checked locking for thread safety
    public static Singleton getInstance() {
        if (instance == null) {  // First check (No lock)
            synchronized (Singleton.class) {
                if (instance == null) {  // Second check (With lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Why This is the Best Approach?

Thread-safe using double-checked locking
Prevents reflection-based breaking with an exception
Lazy initialization (object created only when required)

Check out Java Singleton Pattern - Best Ways to Implement Singleton in Java.

2. Factory Pattern (Encapsulating Object Creation)

Use Case: Creating different types of objects dynamically without modifying existing code

The Factory Pattern provides an interface for creating objects without specifying their exact classes. This helps reduce dependencies and improve flexibility.

Best Practice Implementation

interface Vehicle {
    void manufacture();
}

// Concrete Implementations
class Car implements Vehicle {
    public void manufacture() { System.out.println("Manufacturing a Car!"); }
}

class Bike implements Vehicle {
    public void manufacture() { System.out.println("Manufacturing a Bike!"); }
}

// Factory Class
class VehicleFactory {
    public static Vehicle getVehicle(String type) {
        return switch (type.toLowerCase()) {
            case "car" -> new Car();
            case "bike" -> new Bike();
            default -> throw new IllegalArgumentException("Invalid vehicle type!");
        };
    }
}

Why Use Factory Pattern?

Encapsulates object creation logic
Follows Open-Closed Principle (Easy to add new types without modifying existing code)
Reduces dependencies between client and object creation

3. Builder Pattern (Simplifying Complex Object Creation)

Use Case: Constructing complex objects step by step while keeping them immutable

The Builder Pattern is useful when an object has many optional parameters and should remain immutable after creation.

Best Practice Implementation

public class User {
    private final String name;
    private final int age;
    private final String email;

    // Private constructor to enforce Builder usage
    private User(UserBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
    }

    // Static inner Builder class
    public static class UserBuilder {
        private final String name;  // Mandatory field
        private int age;            // Optional
        private String email;        // Optional

        public UserBuilder(String name) { this.name = name; }

        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }

        public UserBuilder email(String email) {
            this.email = email;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

Why Use Builder Pattern?

Encapsulates complex object creation
Immutable objects (thread-safe)
Easy-to-read code

4. Prototype Pattern (Cloning Objects Efficiently)

Use Case: Creating duplicate objects without reinitializing expensive resources

The Prototype Pattern allows cloning an existing object instead of creating a new one from scratch.

Best Practice Implementation

abstract class Prototype implements Cloneable {
    protected String type;

    abstract void create();

    // Clone method for deep copy
    public Prototype clone() {
        try {
            return (Prototype) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException("Cloning not supported");
        }
    }
}

class Circle extends Prototype {
    public Circle() { this.type = "Circle"; }
    void create() { System.out.println("Creating Circle"); }
}

Why Use Prototype Pattern?

Reduces memory usage (reuse existing object)
Improves performance (avoids costly object creation)
Cloning mechanism ensures flexible object creation

5. Adapter Pattern (Bridging Incompatible Interfaces)

Use Case: Allowing two incompatible interfaces to work together

The Adapter Pattern allows an existing class to be used in a different way without modifying its source code.

Best Practice Implementation

interface MediaPlayer {
    void play(String file);
}

class MP3Player implements MediaPlayer {
    public void play(String file) {
        System.out.println("Playing MP3 file: " + file);
    }
}

// Adapter to convert AdvancedMediaPlayer into MediaPlayer
class MediaAdapter implements MediaPlayer {
    private final AdvancedMediaPlayer advancedPlayer = new AdvancedMediaPlayer();

    public void play(String file) {
        advancedPlayer.playAdvancedFormat(file);
    }
}

Why Use an Adapter Pattern?

Allows legacy code reuse
Bridges incompatible interfaces
Follows Open-Closed Principle (extend functionality without modifying existing classes)

6. Decorator Pattern (Dynamically Adding Behavior to Objects)

Use Case: Adding features dynamically to objects at runtime

The Decorator Pattern allows behavior to be added dynamically to individual objects without modifying their class. It follows the Open-Closed Principle by extending functionality without altering existing code.

Best Practice Implementation

// Base Component Interface
interface Coffee {
    String getDescription();
    double cost();
}

// Concrete Component
class SimpleCoffee implements Coffee {
    public String getDescription() { return "Simple Coffee"; }
    public double cost() { return 50; }
}

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

    public String getDescription() { return coffee.getDescription(); }
    public double cost() { return coffee.cost(); }
}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) { super(coffee); }

    public String getDescription() { return super.getDescription() + ", Milk"; }
    public double cost() { return super.cost() + 20; }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) { super(coffee); }

    public String getDescription() { return super.getDescription() + ", Sugar"; }
    public double cost() { return super.cost() + 10; }
}

// Usage
public class DecoratorPatternExample {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);

        System.out.println(coffee.getDescription() + " -> Cost: " + coffee.cost());
    }
}

Why Use the Decorator Pattern?

Enhances flexibility – New features can be added dynamically
Follows Open-Closed Principle – No modification of existing classes
More maintainable than subclassing

7. Observer Pattern (Event-Driven Programming & Notifications)

Use Case: Implementing event-driven systems (e.g., notifications, messaging services)

The Observer Pattern defines a one-to-many relationship, where changes in one object (Subject) notify multiple dependent objects (Observers).

Best Practice Implementation

import java.util.ArrayList;
import java.util.List;

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

// Concrete Observer
class User implements Observer {
    private String name;
    
    public User(String name) { this.name = name; }

    public void update(String message) {
        System.out.println(name + " received notification: " + message);
    }
}

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

// Concrete Subject
class NotificationService implements Observable {
    private List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer) { observers.add(observer); }
    public void removeObserver(Observer observer) { observers.remove(observer); }
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

// Usage
public class ObserverPatternExample {
    public static void main(String[] args) {
        NotificationService service = new NotificationService();

        Observer user1 = new User("Alice");
        Observer user2 = new User("Bob");

        service.addObserver(user1);
        service.addObserver(user2);

        service.notifyObservers("New video uploaded!");
    }
}

Why Use the Observer Pattern?

Decouples subjects and observers
Event-driven design enhances scalability
Easy to extend (add/remove observers dynamically)

8. Strategy Pattern (Encapsulating Different Behaviors Dynamically)

Use Case: Defining multiple algorithms and switching between them at runtime

The Strategy Pattern allows a class's behavior to be selected at runtime. This pattern defines a family of algorithms, encapsulates them, and makes them interchangeable.

Best Practice Implementation

// Strategy Interface
interface PaymentStrategy {
    void pay(int amount);
}

// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card.");
    }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal.");
    }
}

// Context Class
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// Usage
public class StrategyPatternExample {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPaymentStrategy(new CreditCardPayment());
        cart.checkout(500);

        cart.setPaymentStrategy(new PayPalPayment());
        cart.checkout(300);
    }
}

Why Use the Strategy Pattern?

Encapsulates varying algorithms
Follows Open-Closed Principle – Easily extendable
Promotes flexibility and code reuse

9. Command Pattern (Encapsulating Requests as Objects)

Use Case: Implementing undo/redo functionality and queue-based processing

The Command Pattern encapsulates requests as objects, allowing them to be stored, queued, and executed later.

Best Practice Implementation

// Command Interface
interface Command {
    void execute();
}

// Receiver
class Light {
    public void turnOn() { System.out.println("Light turned ON!"); }
    public void turnOff() { System.out.println("Light turned OFF!"); }
}

// Concrete Commands
class TurnOnCommand implements Command {
    private Light light;
    public TurnOnCommand(Light light) { this.light = light; }
    public void execute() { light.turnOn(); }
}

class TurnOffCommand implements Command {
    private Light light;
    public TurnOffCommand(Light light) { this.light = light; }
    public void execute() { light.turnOff(); }
}

// Invoker
class RemoteControl {
    private Command command;
    public void setCommand(Command command) { this.command = command; }
    public void pressButton() { command.execute(); }
}

// Usage
public class CommandPatternExample {
    public static void main(String[] args) {
        Light light = new Light();
        Command turnOn = new TurnOnCommand(light);
        Command turnOff = new TurnOffCommand(light);

        RemoteControl remote = new RemoteControl();
        remote.setCommand(turnOn);
        remote.pressButton();

        remote.setCommand(turnOff);
        remote.pressButton();
    }
}

Why Use the Command Pattern?

Encapsulates requests as objects
Easily extendable (new commands can be added)
Supports undo/redo functionality

10. Proxy Pattern (Controlling Access to Objects Efficiently)

Use Case: Implementing lazy loading, access control, and caching

The Proxy Pattern provides a surrogate for another object to control access.

Best Practice Implementation

interface Service {
    void request();
}

// Real Service
class RealService implements Service {
    public void request() {
        System.out.println("Processing request in Real Service...");
    }
}

// Proxy
class ServiceProxy implements Service {
    private RealService realService;

    public void request() {
        if (realService == null) {
            realService = new RealService();
        }
        System.out.println("Proxy controlling access...");
        realService.request();
    }
}

// Usage
public class ProxyPatternExample {
    public static void main(String[] args) {
        Service service = new ServiceProxy();
        service.request();
    }
}

Why Use the Proxy Pattern?

Enhances security (access control)
Lazy loading improves performance
Encapsulates additional functionality (e.g., caching, logging)

Conclusion

✅ Singleton Pattern – Ensures a single instance with thread-safety
✅ Factory Pattern – Encapsulates object creation and improves flexibility
✅ Builder Pattern – Simplifies object construction while ensuring immutability
✅ Prototype Pattern – Enables efficient object duplication without performance overhead
✅ Adapter Pattern – Bridges incompatible interfaces and allows integration
Decorator – Dynamically adding functionality
Observer – Event-driven programming
Strategy – Flexible algorithm selection
Command – Encapsulating commands for later execution
Proxy – Controlling object access efficiently

Mastering these 10 design patterns will make you a better Java developer! 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare