10 Essential Java Design Patterns Every Developer Should Master (With Code Examples)

    10 Essential Java Design Patterns Every Developer Should Master (With Code Examples)

    Learn 10 essential Java design patterns every developer should know, including Singleton, Factory, Builder, Observer, and more. Each pattern includes simple explanations, real-world examples, and practical Java code.

    default profile

    Shreya Adak

    December 12, 2024

    14 min read

    When you're working on any project, writing clean and reusable code becomes important and that's where design patterns come in. Also, if you're preparing for an interview, there is a high chance you will be asked to explain some design patterns. For that, having a clear understanding of design patterns is like necessary.

    In this article, I have explained each design pattern in an easy and simple way. I have also included code snippets and "when to use" sections for each pattern to help you understand better. So, let's jump into the world of design patterns!

    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:

    public class Main { public static void main(String[] args) { PaymentService service1 = new PaymentService(new CreditCardPayment()); service1.makePayment(2500); PaymentService service2 = new PaymentService(new UPIPayment()); service2.makePayment(1200); } }

    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.

    Conclusion#

    Finally, we have covered 10 of the most popular design patterns that are widely used in real-world projects and often asked in interviews. I hope you now have a clear picture in your mind of how each design pattern works. I also suggest practicing each design pattern on your own to understand it better and apply it effectively in your code.

    Want to Master Spring Boot and Land Your Dream Job?

    Struggling with coding interviews? Learn Data Structures & Algorithms (DSA) with our expert-led course. Build strong problem-solving skills, write optimized code, and crack top tech interviews with ease

    Learn more
    Java
    Java Design Patterns
    Singleton Pattern
    Factory Pattern
    Builder Pattern

    Subscribe to our newsletter

    Read articles from Coding Shuttle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

    More articles