Java Design Patterns Explained with Real-World Examples

by Didin J. on Oct 31, 2025 Java Design Patterns Explained with Real-World Examples

Learn Java design patterns with real-world examples. Master creational, structural, and behavioral patterns to write clean, reusable, and scalable code.

In software development, it’s common to face recurring design problems — how to create objects efficiently, how to structure relationships between classes, or how to manage communication between components. Rather than reinventing the wheel every time, developers rely on design patterns, proven solutions to common programming challenges.

Design patterns are not code templates or frameworks, but rather best practices refined over decades of experience. They provide a shared vocabulary and structure that help developers design systems that are scalable, maintainable, and easy to understand.

When working in Java, understanding design patterns can elevate your programming skills dramatically. Many popular Java frameworks like Spring, Hibernate, and Jakarta EE are built around these patterns — often without you even realizing it. For instance, the Singleton pattern is used in Spring beans by default, while Factory and Proxy patterns appear throughout the Java ecosystem.

Broadly speaking, design patterns are grouped into three main categories:

  1. Creational Patterns – deal with object creation and instantiation flexibility.

  2. Structural Patterns – focus on class composition and object relationships.

  3. Behavioral Patterns – define how objects communicate and cooperate.

In this tutorial, you’ll learn how each category works through clear, real-world Java examples, from creating reusable services to building extensible user interfaces and implementing flexible strategies for different business scenarios.

By the end, you’ll not only understand how to use design patterns effectively but also how to recognize them in existing Java codebases — a critical skill for any serious developer.


Creational Patterns

Creational patterns focus on the process of object creation. Instead of instantiating classes directly with the new keyword, these patterns give you flexible and reusable ways to create objects while hiding the underlying logic. This makes your code more modular, scalable, and testable.

Let’s explore three of the most common creational patterns in Java with practical, real-world examples.

1. Singleton Pattern

The Singleton pattern ensures that only one instance of a class exists in the application and provides a global access point to it.
It’s widely used in logging, configuration management, and connection pooling.

Example: Application Configuration Manager

public class AppConfig {
    private static AppConfig instance;
    private String appName = "Djamware Design Patterns";
    private String version = "1.0.0";

    // Private constructor prevents instantiation
    private AppConfig() {}

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

    public String getAppName() {
        return appName;
    }

    public String getVersion() {
        return version;
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        AppConfig config = AppConfig.getInstance();
        System.out.println(config.getAppName() + " v" + config.getVersion());
    }
}

When to use:
Use Singleton when a single, shared instance is needed (e.g., a configuration file, logger, or thread pool).

2. Factory Method Pattern

The Factory Method pattern provides an interface for creating objects without specifying their concrete classes.
It’s often used when your code needs to work with multiple related classes but should remain independent of their implementations.

Example: Shape Factory

interface Shape {
    void draw();
}

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

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

class ShapeFactory {
    public static Shape createShape(String type) {
        return switch (type.toLowerCase()) {
            case "circle" -> new Circle();
            case "square" -> new Square();
            default -> throw new IllegalArgumentException("Unknown shape type");
        };
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Shape circle = ShapeFactory.createShape("circle");
        circle.draw();

        Shape square = ShapeFactory.createShape("square");
        square.draw();
    }
}

When to use:
When a class cannot anticipate the type of objects it must create, or when object creation logic should be centralized.

3. Builder Pattern

The Builder pattern simplifies object construction, especially when dealing with complex objects that have multiple optional parameters.
It makes your code more readable and avoids constructors with too many parameters (the telescoping constructor problem).

Example: User Profile Builder

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

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

    public static class Builder {
        private final String name;
        private final String email;
        private int age;
        private String address;

        public Builder(String name, String email) {
            this.name = name;
            this.email = email;
        }

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

        public Builder address(String address) {
            this.address = address;
            return this;
        }

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

    @Override
    public String toString() {
        return String.format("User{name='%s', email='%s', age=%d, address='%s'}",
                name, email, age, address);
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        User user = new User.Builder("John Doe", "[email protected]")
                        .age(30)
                        .address("Jakarta, Indonesia")
                        .build();

        System.out.println(user);
    }
}

When to use:
When you need to create complex objects with multiple optional attributes, while keep your code clean and readable.

🧠 Key Takeaway:
Creational patterns abstract the instantiation process. They make your code flexible and decoupled from specific implementations — a core principle of good software design.


Structural Patterns

Structural patterns focus on how classes and objects are composed to form larger, more flexible structures.
They help ensure that system components work together efficiently, even when their interfaces or implementations differ.

In Java, structural patterns are especially helpful for integrating legacy systems, extending functionality dynamically, and simplifying complex APIs.
Let’s explore three of the most widely used structural patterns with practical, real-world examples.

1. Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by acting as a translator between them.
It’s especially useful when integrating third-party or legacy code into a modern system.

Example: Payment Gateway Adapter

Suppose your app uses a new PaymentProcessor interface, but you need to integrate an old PayPal SDK that doesn’t match the interface.

// New interface expected by the application
interface PaymentProcessor {
    void pay(double amount);
}

// Legacy PayPal class (cannot be modified)
class PayPal {
    public void makePayment(double amountInUsd) {
        System.out.println("Paid $" + amountInUsd + " using PayPal");
    }
}

// Adapter class
class PayPalAdapter implements PaymentProcessor {
    private final PayPal payPal;

    public PayPalAdapter(PayPal payPal) {
        this.payPal = payPal;
    }

    @Override
    public void pay(double amount) {
        payPal.makePayment(amount);
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PayPalAdapter(new PayPal());
        processor.pay(100.0);
    }
}

When to use:
When you want to reuse an existing class that doesn’t implement the interface your application needs.

2. Decorator Pattern

The Decorator pattern dynamically adds new behavior or responsibilities to objects without modifying their structure.
It’s often used in UI components or stream handling in Java (e.g., BufferedReader, DataInputStream).

Example: Coffee Order with Add-ons

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

class BasicCoffee implements Coffee {
    public String getDescription() {
        return "Basic Coffee";
    }

    public double getCost() {
        return 2.0;
    }
}

// Base Decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

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

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

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

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

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

    public double getCost() {
        return super.getCost() + 0.5;
    }
}

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

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

    public double getCost() {
        return super.getCost() + 0.2;
    }
}

Usage:

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

        System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
    }
}

When to use:
When you need to add functionality to objects dynamically without changing their core class.

3. Facade Pattern

The Facade pattern provides a simplified interface to a complex subsystem.
It hides implementation details and offers a clean API to the client.

Example: Email Sending Facade

// Subsystems
class SmtpServer {
    void connect() { System.out.println("Connecting to SMTP server..."); }
    void disconnect() { System.out.println("Disconnected from SMTP server."); }
}

class EmailBuilder {
    String build(String to, String subject, String body) {
        return "To: " + to + "\nSubject: " + subject + "\n\n" + body;
    }
}

class EmailSender {
    void send(String message) {
        System.out.println("Sending email:\n" + message);
    }
}

// Facade
class EmailServiceFacade {
    private final SmtpServer smtp = new SmtpServer();
    private final EmailBuilder builder = new EmailBuilder();
    private final EmailSender sender = new EmailSender();

    public void sendEmail(String to, String subject, String body) {
        smtp.connect();
        String message = builder.build(to, subject, body);
        sender.send(message);
        smtp.disconnect();
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        EmailServiceFacade emailService = new EmailServiceFacade();
        emailService.sendEmail("[email protected]", "Welcome!", "Thanks for joining Djamware!");
    }
}

When to use:
When dealing with complex subsystems that can benefit from a unified, simplified interface.

🧠 Key Takeaway:
Structural patterns help you compose classes and objects elegantly, keeping systems flexible and easier to maintain — especially when integrating existing code or adding new functionality.


Behavioral Patterns

Behavioral patterns define how objects communicate and collaborate within a system.
They help improve flexibility in carrying out communication between objects and make it easier to assign responsibilities dynamically.

These patterns focus more on interaction and control flow than on object structure.
Let’s go through three of the most common behavioral patterns in Java, each explained with a real-world scenario.

1. Observer Pattern

The Observer pattern defines a one-to-many relationship between objects so that when one object (the subject) changes state, all its dependents (observers) are notified automatically.
It’s commonly used in event-driven systems, GUIs, and real-time notifications.

Example: News Publisher and Subscribers

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

interface Observer {
    void update(String news);
}

interface Subject {
    void addObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

class NewsAgency implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    private String news;

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    @Override
    public void addObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(news);
        }
    }
}

class Subscriber implements Observer {
    private final String name;

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

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

Usage:

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

        Observer alice = new Subscriber("Alice");
        Observer bob = new Subscriber("Bob");

        agency.addObserver(alice);
        agency.addObserver(bob);

        agency.setNews("Breaking: New Java Design Patterns Tutorial Released!");
    }
}

When to use:
When an object’s state changes and you need to automatically notify other objects without tightly coupling them.

2. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
This allows algorithms to vary independently from the clients that use them — perfect for cases like different payment methods, sorting techniques, or compression strategies.

Example: Payment Strategy System

interface PaymentStrategy {
    void pay(double amount);
}

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

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

class PaymentContext {
    private PaymentStrategy strategy;

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

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

Usage:

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

        context.setPaymentStrategy(new CreditCardPayment());
        context.executePayment(100.0);

        context.setPaymentStrategy(new PayPalPayment());
        context.executePayment(250.0);
    }
}

When to use:
When you have multiple ways of performing a task, and you want to switch between them dynamically at runtime.

3. Command Pattern

The Command pattern turns a request into a standalone object, allowing you to parameterize methods with different requests, queue them, or support undo/redo functionality.
It’s widely used in GUI applications, text editors, and task execution systems.

Example: Remote Control Command System

interface Command {
    void execute();
}

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

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

class TurnOnLightCommand implements Command {
    private final Light light;

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

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

class TurnOffLightCommand implements Command {
    private final Light light;

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

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

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 light = new Light();

        Command turnOn = new TurnOnLightCommand(light);
        Command turnOff = new TurnOffLightCommand(light);

        RemoteControl remote = new RemoteControl();

        remote.setCommand(turnOn);
        remote.pressButton();

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

When to use:
When you need to decouple a request sender from the object that acts, or to implement features like undo, redo, or queueing.

🧠 Key Takeaway:
Behavioral patterns emphasize flexibility in communication and control flow, making your Java applications more modular and adaptable to change — especially as complexity grows.


Best Practices for Using Design Patterns

Understanding design patterns is one thing — using them effectively and wisely in real-world projects is another.
Misusing or overusing patterns can lead to overly complex and hard-to-maintain code. The key is balance and intent.

Here are some best practices to help you apply design patterns in Java projects the right way.

1. Understand the Problem Before Choosing a Pattern

Design patterns are meant to solve problems, not create them.
Before picking one, ask yourself:

  • What problem am I trying to solve?

  • Does this pattern actually simplify the design?

  • Can this be done in a simpler way?

📘 Example:
If your application only ever needs one configuration instance, then Singleton makes sense.
But if you find yourself forcing a Singleton just because “it’s a common pattern,” you’re likely overengineering.

2. Don’t Overuse Patterns

It’s tempting to use patterns everywhere once you’ve learned them, but that often leads to pattern-itis — excessive abstraction and unnecessary indirection.

Keep your code simple:

  • If the problem doesn’t exist yet, don’t solve it prematurely.

  • Start with straightforward code; refactor later when patterns naturally emerge.

💡 Tip:
Write the simplest working solution first. As your application grows, introduce patterns when you clearly see repetition or complexity that needs managing.

3. Combine Patterns When Needed

Many robust systems use multiple design patterns together to achieve flexibility and maintainability.
This isn’t a bad thing — as long as each pattern serves a clear purpose.

📘 Example:
A Factory can create Strategy objects dynamically based on runtime configuration, while a Singleton might manage the factory instance.
Used together, they create a powerful, extensible structure.

4. Learn How Frameworks Use Patterns

Most Java frameworks are built on design patterns. Recognizing them in the wild helps deepen your understanding.

For instance:

  • Spring Framework uses Singleton, Factory, Proxy, and Template Method patterns extensively.

  • Hibernate uses DAO and Builder patterns for data access and configuration.

  • Java I/O classes are a classic case of the Decorator pattern (BufferedReader, InputStreamReader, etc.).

When you can identify patterns in these frameworks, you’ll gain better control over customization and debugging.

5. Refactor Toward Patterns, Don’t Start With Them

Good architecture evolves over time. Instead of forcing patterns at the start, refactor your code when a pattern fits naturally.

Steps to follow:

  1. Write the simplest possible implementation.

  2. Identify pain points (duplication, complexity, tight coupling).

  3. Refactor toward an appropriate pattern.

This approach ensures patterns are solutions to real problems, not theoretical exercises.

6. Keep Patterns as Tools, Not Rules

Patterns are guidelines, not laws.
They help you think systematically but shouldn’t restrict creativity or problem-solving.
If a pattern makes your code more complex or less readable, don’t use it.

As the famous saying goes:

“Design patterns are discovered, not invented.”

🧠 Key Takeaway:
Design patterns are powerful tools that bring structure and clarity to software development.
Used wisely, they improve readability, reusability, and collaboration — but they should always serve the problem, not the other way around.


Conclusion and Next Steps

Design patterns are one of the most valuable concepts a Java developer can master.
They bridge the gap between theory and practical software architecture, providing proven ways to write code that’s maintainable, extensible, and easier to understand.

In this tutorial, you’ve explored:

  • Creational patterns, which handle flexible object creation

  • Structural patterns, which focus on class and object composition

  • Behavioral patterns, which define object communication and control flow

By studying and applying these patterns, you gain the ability to recognize common problems and implement elegant, reusable solutions.
You’ll also find that many popular Java frameworks — such as Spring, Hibernate, and Jakarta EE — are built around these very same design principles.

What’s Next

To continue your journey, try:

  • Refactoring one of your existing Java projects using some of the patterns you’ve learned.

  • Studying how patterns appear in the Spring Framework (e.g., Singleton Beans, Factory Beans, Template Methods).

  • Exploring related topics like SOLID principles, Clean Architecture, and Domain-Driven Design (DDD) to further strengthen your design skills.

With consistent practice, you’ll develop an intuitive sense of when (and when not) to use a design pattern — the mark of a seasoned software engineer.

You can find the full source code on our GitHub.

That's just the basics. If you need more deep learning about Spring Boot, you can take the following cheap course:

Thanks!