What are Design Patterns?

Design Patterns are proven, reusable solutions to common software design problems.
They provide best practices for structuring your code in a flexible, maintainable way.

✅ Think of them as templates for solving design challenges — not full implementations.


🔧 Types of Design Patterns in Java

Java design patterns are broadly classified into three categories:


1. Creational Patterns

Deal with object creation logic

Pattern Purpose
Singleton Ensures only one instance of a class
Factory Method Creates objects without exposing class logic
Abstract Factory Creates families of related objects
Builder Builds complex objects step by step
Prototype Clones existing objects

2. Structural Patterns

Deal with object composition & relationships

Pattern Purpose
Adapter Converts one interface into another
Bridge Decouples abstraction from implementation
Composite Treats individual and group of objects the same
Decorator Adds new behavior to an object dynamically
Facade Simplifies interface of a complex system
Flyweight Reduces memory usage via object sharing
Proxy Acts as a placeholder or controller

3. Behavioral Patterns

Deal with object interaction and communication

Pattern Purpose
Observer Notifies objects when state changes
Strategy Encapsulates algorithms for runtime switching
Command Encapsulates a request as an object
Iterator Sequential access without exposing structure
Template Method Defines a skeleton, subclasses override steps
State Changes behavior based on internal state
Chain of Responsibility Passes request along a chain
Mediator Centralized communication between objects
Memento Saves and restores object state
Visitor Adds operations without modifying objects
Interpreter Defines a grammar and interpreter

 

1. Singleton Pattern

The Singleton Pattern ensures that a class has only one instance throughout the entire application and provides a global point of access to that instance. Think of a scenario where you have a single database connection or a configuration manager, you don't want to create multiple objects for these, as they can be heavy and may lead to inconsistencies. That’s where Singleton comes into play.

In Java, you implement a Singleton by making the constructor private, so no other class can create its object directly. You then create a static method (usually called getInstance()) which will return the same instance every time it's called.


public class DatabaseConnection {

    private static DatabaseConnection instance;

    // private constructor prevents instantiation
    private DatabaseConnection() {
        System.out.println("Connecting to database...");
    }

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

Now whenever you need a connection:


public class Main {
    public static void main(String[] args) {
        DatabaseConnection db1 = DatabaseConnection.getInstance();
        DatabaseConnection db2 = DatabaseConnection.getInstance();

        System.out.println(db1 == db2); // true
    }
}

Here, db1 and db2 are both pointing to the same object, showing how Singleton avoids multiple object creation. This pattern is useful in logging, configuration classes, and connection pools.

When to use the Singleton Pattern

  • When only one instance of a class should exist across the application.
  • When you need centralized configuration, logging, or caching.
  • For shared resources like database connections or file systems.

2. Factory Pattern

The Factory Pattern helps in creating objects without exposing the object creation logic to the client. Instead of using new keyword in different places, you use a factory class to decide which class object should be created, based on input. This becomes extremely useful when you have multiple subclasses of a parent/interface, and you want to manage object creation centrally.

Imagine a graphic design app where users can draw different shapes like circles, squares, or triangles. Rather than creating each shape manually, a factory can handle it.


interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing Circle");
    }
}

class Square implements Shape {
    public void draw() {
        System.out.println("Drawing Square");
    }
}

class ShapeFactory {
    public static Shape getShape(String type) {
        if ("circle".equalsIgnoreCase(type)) {
            return new Circle();
        } else if ("square".equalsIgnoreCase(type)) {
            return new Square();
        }
        return null;
    }
}

Client usage becomes easy and clean:


public class Main {
    public static void main(String[] args) {
        Shape shape1 = ShapeFactory.getShape("circle");
        shape1.draw(); // Drawing Circle

        Shape shape2 = ShapeFactory.getShape("square");
        shape2.draw(); // Drawing Square
    }
}

 

This pattern is great when the logic to create an object is complex or involves decision-making. It also keeps your code open for extension but closed for modification, which is one of the SOLID principles.

When to use the Factory Pattern

  • When object creation logic is complex or involves decision-making.
  • When you want to create objects without exposing the actual class.
  • When you need loose coupling between client code and actual implementations.

3. Builder Pattern

The Builder Pattern is perfect when you need to construct complex objects step-by-step, especially if some fields are optional. It helps you avoid the problem of constructor overloading, where you end up writing multiple constructors with different parameter combinations.

Let’s say you’re building a User object where some properties like name and email are required, but others like age, phone, or address are optional. The builder pattern makes this process more readable and less error-prone.


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

    private User(UserBuilder builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
    }

    public static class UserBuilder {
        private String name;
        private String email;
        private int age;

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

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

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

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

    public String toString() {
        return name + " | " + email + " | " + age;
    }
}

You can now build an object like this:


public class Main {
    public static void main(String[] args) {
        User user = new User.UserBuilder()
                .setName("Munaf")
                .setEmail("munaf@example.com")
                .setAge(18)
                .build();

        System.out.println(user);
    }
}

The Builder pattern makes your code cleaner, more readable, and flexible, especially for creating DTOs or response models in large applications.

When to use the Builder Pattern

  • When you need to build complex objects with many optional parameters.
  • When object creation requires a step-by-step process.
  • To avoid constructor overload with many parameters.

4. Prototype Pattern

The Prototype Pattern is used when you want to create a copy of an existing object instead of building a new one from scratch. It’s very useful when object creation is expensive or time-consuming, like when loading from a database or performing deep configuration.

In Java, this pattern uses the clone() method (from Cloneable interface) to make a duplicate of an object.


class Vehicle implements Cloneable {
    private String type;

    public Vehicle(String type) {
        this.type = type;
    }

    public Vehicle clone() {
        try {
            return (Vehicle) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }

    public void setType(String type) {
        this.type = type;
    }

    public String toString() {
        return "Vehicle: " + type;
    }
}

Here’s how it works:

public class Main {
    public static void main(String[] args) {
        Vehicle car = new Vehicle("Car");

        Vehicle carCopy = car.clone();
        carCopy.setType("Bike");

        System.out.println(car);      // Vehicle: Car
        System.out.println(carCopy);  // Vehicle: Bike
    }
}

Notice how cloning saves time by copying an existing object. The Prototype pattern is used in real-world apps where object configuration is heavy, and you want to quickly make a copy and just tweak a few things.

Perfect! Let’s continue our blog with the remaining 4 Java design patterns in the same beginner-friendly, paragraph style — keeping it simple, practical, and code-backed.

When to use the Prototype Pattern

  • When object creation is expensive or time-consuming.
  • When you need to create many copies of similar objects.
  • To clone existing objects instead of building new ones from scratch.

5. Adapter Pattern

The Adapter Pattern is used when you want to make two incompatible interfaces work together. It acts like a bridge or a connector between two classes that otherwise cannot communicate. Imagine you bought a charger from the US, but you’re in India — an adapter allows it to fit the socket here. This design pattern does the same with classes.

In Java, this is commonly used when integrating third-party APIs or legacy code with your new system.

Let’s say you have an AdvancedMediaPlayer which supports only .vlc and .mp4 files, but your application uses a generic MediaPlayer interface.


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

class VlcPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing VLC file: " + fileName);
    }
}

class VlcAdapter implements MediaPlayer {
    private VlcPlayer vlcPlayer;

    public VlcAdapter() {
        vlcPlayer = new VlcPlayer();
    }

    public void play(String audioType, String fileName) {
        if ("vlc".equalsIgnoreCase(audioType)) {
            vlcPlayer.playVlc(fileName);
        }
    }
}

Now we plug this adapter into our application:

class AudioPlayer implements MediaPlayer {
    public void play(String audioType, String fileName) {
        if ("mp3".equalsIgnoreCase(audioType)) {
            System.out.println("Playing MP3 file: " + fileName);
        } else if ("vlc".equalsIgnoreCase(audioType)) {
            MediaPlayer adapter = new VlcAdapter();
            adapter.play(audioType, fileName);
        } else {
            System.out.println("Format not supported");
        }
    }
}

Usage:


public class Main {
    public static void main(String[] args) {
        AudioPlayer player = new AudioPlayer();
        player.play("mp3", "song.mp3");
        player.play("vlc", "video.vlc");
    }
}

This pattern is perfect when you're dealing with legacy code or third-party tools that don’t directly match your interface needs.

When to use the Adapter Pattern

  • When two classes don’t match interfaces but need to work together.
  • When integrating with legacy code or third-party libraries.
  • To make existing classes compatible with new code.

6. Observer Pattern

The Observer Pattern defines a one-to-many relationship between objects. When one object changes, all its dependent objects (observers) are automatically notified and updated. You’ve definitely seen this in real life — for example, subscribing to a YouTube channel. Once the channel posts a video, all subscribers get notified.

This is very common in event-driven systems, UI frameworks, and messaging systems.

Let’s see how this works in Java:

import java.util.*;

interface Observer {
    void update(String message);
}

class Subscriber implements Observer {
    private String name;

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

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

Now we need a Subject that notifies subscribers:

class Channel {
    private List<Observer> subscribers = new ArrayList<>();

    public void subscribe(Observer observer) {
        subscribers.add(observer);
    }

    public void notifyAllSubscribers(String message) {
        for (Observer obs : subscribers) {
            obs.update(message);
        }
    }
}

Usage:


public class Main {
    public static void main(String[] args) {
        Channel codingShuttle = new Channel();

        Subscriber s1 = new Subscriber("Munaf");
        Subscriber s2 = new Subscriber("Ravi");

        codingShuttle.subscribe(s1);
        codingShuttle.subscribe(s2);

        codingShuttle.notifyAllSubscribers("New Spring Boot video is live!");
    }
}

This pattern keeps your code loosely coupled, and is useful in applications where data change needs to be broadcasted — like stock prices, chat apps, and notification systems.

When to use the Observer Pattern

  • When multiple objects need to be notified when one object changes.
  • In event-driven systems like GUIs, chat apps, or notification systems.
  • When implementing publish-subscribe behavior.

7. Strategy Pattern

The Strategy Pattern is all about choosing a behavior at runtime. Instead of writing complex if-else or switch statements, you define multiple strategies (algorithms), and select one dynamically.

Let’s say you’re building a payment system that supports different methods: Credit Card, UPI, or PayPal. Using strategy pattern, you can switch between these strategies easily without changing core logic.


interface PaymentStrategy {
    void pay(double amount);
}

class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid ₹" + amount + " using Credit Card");
    }
}

class UPIPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid ₹" + amount + " using UPI");
    }
}

Now we plug this into a PaymentService class:


class PaymentService { private PaymentStrategy strategy; public PaymentService(PaymentStrategy strategy) { this.strategy = strategy; } public void makePayment(double amount) { strategy.pay(amount); } }

Usage:

class PaymentService {
    private PaymentStrategy strategy;

    public PaymentService(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void makePayment(double amount) {
        strategy.pay(amount);
    }
}

This pattern helps in separating different behaviors, making the code easier to extend and test. You can add a new payment method without touching any existing code.

When to use the Strategy Pattern

  • When you need to choose behavior/algorithm at runtime.
  • To eliminate long if-else or switch statements.
  • When you want to make a class open to extension but closed to modification.

8. Decorator Pattern

The Decorator Pattern is used to dynamically add new functionality to an object without modifying its original code. It’s like adding toppings on a pizza — the base remains the same, but you can add cheese, mushrooms, or paneer to customize it.

Let’s build a simple coffee ordering system where you can add milk, sugar, etc., on top of base coffee.


interface Coffee {
    String getDescription();
    double getCost();
}

class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Simple Coffee";
    }

    public double getCost() {
        return 50;
    }
}

Now let’s add decorators:

class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    public double getCost() {
        return coffee.getCost() + 10;
    }
}

class SugarDecorator implements Coffee {
    private Coffee coffee;

    public SugarDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

    public double getCost() {
        return coffee.getCost() + 5;
    }
}

Usage:

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

        System.out.println("Order: " + coffee.getDescription());
        System.out.println("Cost: ₹" + coffee.getCost());
    }
}

Output:

Order: Simple Coffee, Milk, Sugar
Cost: ₹65.0

The Decorator pattern is very powerful when you want to add features without altering the existing code, and is commonly used in logging, security, and user interface designs.

When to use the Decorator Pattern

  • When you want to add responsibilities/features dynamically to an object.
  • To extend functionality without modifying the original class.
  • For flexible and reusable wrappers (e.g., UI components, logging).

9. Proxy Pattern

The Proxy Pattern provides a placeholder or substitute for another object to control access to it. Think of it like a receptionist at a company. You don’t directly talk to the boss — the receptionist acts as a middle layer to either pass the message or deny access.

This pattern is especially useful in cases like lazy initialization, access control, caching, or logging. You wrap the original object with another class (the proxy) and intercept calls to it.

Let’s take an example of an Internet interface where access to certain websites is restricted.


interface Internet {
    void connectTo(String serverHost) throws Exception;
}

Now the actual internet class:


class RealInternet implements Internet {
    public void connectTo(String serverHost) {
        System.out.println("Connecting to " + serverHost);
    }
}

Let’s create a proxy that adds restrictions:


import java.util.*;

class ProxyInternet implements Internet {
    private Internet internet = new RealInternet();
    private static List<String> bannedSites;

    static {
        bannedSites = new ArrayList<>();
        bannedSites.add("facebook.com");
        bannedSites.add("instagram.com");
    }

    public void connectTo(String serverHost) throws Exception {
        if (bannedSites.contains(serverHost.toLowerCase())) {
            throw new Exception("Access Denied to " + serverHost);
        }
        internet.connectTo(serverHost);
    }
}

Usage:


public class Main {
    public static void main(String[] args) {
        Internet net = new ProxyInternet();

        try {
            net.connectTo("google.com");        // Allowed
            net.connectTo("facebook.com");      // Blocked
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

This pattern is commonly used in frameworks like Spring AOP, where proxy classes wrap services to add logging, security, or transactional behavior.

When to use the Proxy Pattern

  • When you want to control access to another object.
  • For lazy loading, caching, logging, or security checks.
  • When adding a middle layer between client and real object.

10. Command Pattern

The Command Pattern is used to encapsulate a request as an object, allowing us to parameterize clients with different requests, queue them, or log them. Think of it like placing an order at a restaurant — the waiter (command object) takes the order and passes it to the kitchen (receiver) without the customer needing to know how it’s cooked.

This is very useful in UI buttons, undo/redo operations, task scheduling, and even remote controls.

Let’s see an example of a remote control turning devices ON/OFF.


interface Command {
    void execute();
}

class Light {
    public void turnOn() {
        System.out.println("Light turned ON");
    }

    public void turnOff() {
        System.out.println("Light turned OFF");
    }
}

Now we create command objects:

class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOn();
    }
}

class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOff();
    }
}

And finally, a simple remote control to trigger these:


class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

Usage:


public class Main {
    public static void main(String[] args) {
        Light livingRoomLight = new Light();
        Command on = new LightOnCommand(livingRoomLight);
        Command off = new LightOffCommand(livingRoomLight);

        RemoteControl remote = new RemoteControl();

        remote.setCommand(on);
        remote.pressButton();  // Light turned ON

        remote.setCommand(off);
        remote.pressButton();  // Light turned OFF
    }
}

This pattern is helpful when you need to separate the object that invokes a command from the one that knows how to perform it, making your code more flexible and modular.

When to use the Command Pattern

  • When you need to encapsulate actions/operations as objects.
  • For undo/redo, task queues, or remote execution.
  • When you want to decouple the sender and receiver of a request.

Source : https://www.codingshuttle.com/blogs/top-8-design-patterns-in-java/

 

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.