Common Design Patterns in Java: A Beginner's Guide

If you've been writing Java for a while, you've probably heard the term "design patterns" thrown around. Maybe someone on your team said "we should use a Factory here" and you nodded along while quietly Googling it.

Design patterns are reusable solutions to common software design problems. They were popularized by the "Gang of Four" (GoF) book โ€” Design Patterns: Elements of Reusable Object-Oriented Software (1994). The book describes 23 patterns, but you don't need to memorize all of them. In practice, a handful come up again and again.

One important note before we start: not every problem needs a pattern. Over-engineering is a real risk. If a simple function solves your problem, don't wrap it in three interfaces and a factory. Patterns are tools, not mandates.

Let's jump in.

Singleton

The problem: You want exactly one instance of a class to exist. Think database connections, configuration managers, logging โ€” things where having multiple instances would cause chaos.

Java Implementation

The classic approach: private constructor + static getInstance() method.

public class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {
        // private constructor โ€” no one can call new DatabaseConnection()
    }

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

    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Usage
DatabaseConnection db = DatabaseConnection.getInstance();
db.query("SELECT * FROM users");

But wait โ€” this isn't thread-safe. Two threads could both see instance == null at the same time and create two instances. You need synchronization:

public class DatabaseConnection {
    private static volatile DatabaseConnection instance;

    private DatabaseConnection() {}

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

Double-checked locking. volatile. synchronized. Three keywords just to make sure there's only one of something. O_o

The modern Java way? Use an enum. Joshua Bloch (author of Effective Java) calls this the best approach:

public enum DatabaseConnection {
    INSTANCE;

    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Usage
DatabaseConnection.INSTANCE.query("SELECT * FROM users");

Thread-safe, serialization-safe, reflection-safe. Three lines. Why didn't they teach this first?

Python Comparison

In Python, a module is already a singleton. If you import a module twice, Python reuses the same module object:

# db.py
class DatabaseConnection:
    def query(self, sql):
        print(f"Executing: {sql}")

# Module-level instance โ€” this IS your singleton
db = DatabaseConnection()
# main.py
from db import db
db.query("SELECT * FROM users")

That's it. No private constructors, no double-checked locking, no enum tricks. Python modules are singletons by nature.

All that boilerplate in Java โ€” private constructors, volatile fields, synchronized blocks โ€” just to guarantee one instance. In Python you write db = DatabaseConnection() at module level and you're done. This is what I mean when I say Java makes you work for things Python gives you for free.

Factory Method

The problem: You need to create objects, but you don't want the client code to know (or care) about the specific class being instantiated. You want to decouple object creation from object usage.

Java Implementation

// The product interface
public interface Notification {
    void send(String message);
}

// Concrete products
public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Email: " + message);
    }
}

public class SmsNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Push: " + message);
    }
}

// The factory
public class NotificationFactory {
    public static Notification create(String type) {
        return switch (type) {
            case "email" -> new EmailNotification();
            case "sms" -> new SmsNotification();
            case "push" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage
Notification notification = NotificationFactory.create("email");
notification.send("Hello!");

Python Comparison

class EmailNotification:
    def send(self, message):
        print(f"Email: {message}")

class SmsNotification:
    def send(self, message):
        print(f"SMS: {message}")

class PushNotification:
    def send(self, message):
        print(f"Push: {message}")

def create_notification(type_name):
    factories = {
        "email": EmailNotification,
        "sms": SmsNotification,
        "push": PushNotification,
    }
    return factories[type_name]()

# Usage
notification = create_notification("email")
notification.send("Hello!")

In Python, a function with a dictionary lookup does the job. No interface, no factory class. Python classes are first-class objects โ€” you can put them in a dict and call them. Java needs the interface + factory class ceremony because types must be declared upfront. The Factory pattern exists in Java partly because the language doesn't have the flexibility Python does.

Builder

The problem: You have a class with many parameters, some optional. Constructors with 8+ parameters are unreadable. Which argument is which?

// This is painful
User user = new User("Mirat", "mirat@example.com", 33, "Istanbul",
                     true, false, "UTC+3", null);
// What is true? What is false? What's null? Good luck.

Java Implementation

public class User {
    private final String name;
    private final String email;
    private final int age;
    private final String city;
    private final boolean active;
    private final String timezone;

    private User(Builder builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
        this.city = builder.city;
        this.active = builder.active;
        this.timezone = builder.timezone;
    }

    public static class Builder {
        // Required
        private final String name;
        private final String email;

        // Optional with defaults
        private int age = 0;
        private String city = "";
        private boolean active = true;
        private String timezone = "UTC";

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

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

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

        public Builder active(boolean active) {
            this.active = active;
            return this;
        }

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

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

    @Override
    public String toString() {
        return "User{name='%s', email='%s', age=%d, city='%s', active=%b, timezone='%s'}"
            .formatted(name, email, age, city, active, timezone);
    }
}

// Usage โ€” readable and self-documenting
User user = new User.Builder("Mirat", "mirat@example.com")
    .age(33)
    .city("Istanbul")
    .timezone("UTC+3")
    .build();

Method chaining makes it clear what each value is. Much better than the 8-argument constructor.

In real projects, you'd use Lombok to avoid writing all this:

import lombok.Builder;

@Builder
public class User {
    private String name;
    private String email;
    @Builder.Default private int age = 0;
    @Builder.Default private boolean active = true;
}

// Same usage, zero boilerplate
User user = User.builder()
    .name("Mirat")
    .email("mirat@example.com")
    .age(33)
    .build();

Python Comparison

from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int = 0
    city: str = ""
    active: bool = True
    timezone: str = "UTC"

# Usage
user = User(name="Mirat", email="mirat@example.com", age=33, city="Istanbul")

Python has keyword arguments. That's the whole Builder pattern, built into the language. User(name="Mirat", age=33) โ€” self-documenting, readable, no boilerplate. Java doesn't have keyword arguments so you need 60 lines of Builder code (or Lombok) to achieve the same thing. This is probably the clearest example of Java patterns compensating for missing language features.

Observer

The problem: When one object changes state, other objects need to be notified and updated automatically. Think event systems, UI updates, or pub/sub messaging.

Java Implementation

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

// The event
public class PriceChangeEvent {
    private final String product;
    private final double oldPrice;
    private final double newPrice;

    public PriceChangeEvent(String product, double oldPrice, double newPrice) {
        this.product = product;
        this.oldPrice = oldPrice;
        this.newPrice = newPrice;
    }

    public String getProduct() { return product; }
    public double getOldPrice() { return oldPrice; }
    public double getNewPrice() { return newPrice; }
}

// The observer interface
public interface PriceObserver {
    void onPriceChange(PriceChangeEvent event);
}

// The subject (observable)
public class ProductStore {
    private final List<PriceObserver> observers = new ArrayList<>();
    private final Map<String, Double> prices = new HashMap<>();

    public void addObserver(PriceObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(PriceObserver observer) {
        observers.remove(observer);
    }

    public void setPrice(String product, double price) {
        double oldPrice = prices.getOrDefault(product, 0.0);
        prices.put(product, price);

        // Notify all observers
        var event = new PriceChangeEvent(product, oldPrice, price);
        for (PriceObserver observer : observers) {
            observer.onPriceChange(event);
        }
    }
}

// Concrete observers
public class EmailAlert implements PriceObserver {
    @Override
    public void onPriceChange(PriceChangeEvent event) {
        if (event.getNewPrice() < event.getOldPrice()) {
            System.out.println("Email: " + event.getProduct() +
                " price dropped to $" + event.getNewPrice());
        }
    }
}

public class DashboardUpdater implements PriceObserver {
    @Override
    public void onPriceChange(PriceChangeEvent event) {
        System.out.println("Dashboard: Updated " + event.getProduct() +
            " โ†’ $" + event.getNewPrice());
    }
}

// Usage
ProductStore store = new ProductStore();
store.addObserver(new EmailAlert());
store.addObserver(new DashboardUpdater());

store.setPrice("Laptop", 999.99);
store.setPrice("Laptop", 899.99);  // triggers email alert + dashboard update

With Java 8+ lambdas, you can simplify the observer registration:

// Using lambdas instead of creating classes
store.addObserver(event ->
    System.out.println("Price changed: " + event.getProduct()));

Python Comparison

class ProductStore:
    def __init__(self):
        self._observers = []
        self._prices = {}

    def add_observer(self, callback):
        self._observers.append(callback)

    def set_price(self, product, price):
        old_price = self._prices.get(product, 0)
        self._prices[product] = price
        for callback in self._observers:
            callback(product, old_price, price)

# Usage
store = ProductStore()

# Just pass functions โ€” no classes needed
store.add_observer(lambda product, old, new:
    print(f"Email: {product} dropped to ${new}") if new < old else None)

store.add_observer(lambda product, old, new:
    print(f"Dashboard: Updated {product} โ†’ ${new}"))

store.set_price("Laptop", 999.99)
store.set_price("Laptop", 899.99)

In Python, you just pass functions as callbacks. No observer interface, no event class (unless you want one). Functions are first-class citizens, so you don't need an interface just to define "something that can be called."

Strategy

The problem: You have an algorithm that needs to vary. Different situations require different behaviors, and you want to switch between them at runtime without a mess of if/else chains.

Java Implementation

// The strategy interface
public interface PricingStrategy {
    double calculatePrice(double basePrice, int quantity);
}

// Concrete strategies
public class RegularPricing implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice, int quantity) {
        return basePrice * quantity;
    }
}

public class BulkPricing implements PricingStrategy {
    @Override
    public double calculatePrice(double basePrice, int quantity) {
        if (quantity >= 10) {
            return basePrice * quantity * 0.8;  // 20% discount
        }
        return basePrice * quantity;
    }
}

public class SeasonalPricing implements PricingStrategy {
    private final double discountRate;

    public SeasonalPricing(double discountRate) {
        this.discountRate = discountRate;
    }

    @Override
    public double calculatePrice(double basePrice, int quantity) {
        return basePrice * quantity * (1 - discountRate);
    }
}

// The context
public class ShoppingCart {
    private PricingStrategy strategy;

    public ShoppingCart(PricingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(PricingStrategy strategy) {
        this.strategy = strategy;
    }

    public double checkout(double basePrice, int quantity) {
        return strategy.calculatePrice(basePrice, quantity);
    }
}

// Usage
ShoppingCart cart = new ShoppingCart(new RegularPricing());
System.out.println(cart.checkout(100, 5));   // 500.0

cart.setStrategy(new BulkPricing());
System.out.println(cart.checkout(100, 15));  // 1200.0 (20% off)

cart.setStrategy(new SeasonalPricing(0.3));
System.out.println(cart.checkout(100, 5));   // 350.0 (30% off)

Since Java 8+, you can use lambdas instead of creating separate classes:

// Lambda strategies โ€” no need for separate class files
ShoppingCart cart = new ShoppingCart(
    (price, qty) -> price * qty  // regular pricing as lambda
);

cart.setStrategy((price, qty) ->
    qty >= 10 ? price * qty * 0.8 : price * qty  // bulk pricing
);

Python Comparison

def regular_pricing(base_price, quantity):
    return base_price * quantity

def bulk_pricing(base_price, quantity):
    if quantity >= 10:
        return base_price * quantity * 0.8
    return base_price * quantity

def seasonal_pricing(discount_rate):
    def calculate(base_price, quantity):
        return base_price * quantity * (1 - discount_rate)
    return calculate

# Usage โ€” just pass functions
strategy = regular_pricing
print(strategy(100, 5))   # 500.0

strategy = bulk_pricing
print(strategy(100, 15))  # 1200.0

strategy = seasonal_pricing(0.3)
print(strategy(100, 5))   # 350.0

In Python, functions ARE the strategy pattern. You just pass a different function. No interface, no concrete strategy classes, no context class. Java needs all that ceremony because (before Java 8) you couldn't pass functions around โ€” you had to wrap them in objects. Java 8 lambdas closed this gap significantly, but the "interface + concrete classes" approach is still what you'll see in most Java codebases and textbooks.

Adapter

The problem: You have a class with one interface, but you need it to work with a system that expects a different interface. The Adapter wraps the incompatible class and translates its interface.

Java Implementation

Imagine you have an old analytics library that you can't modify, and a new system that expects a different interface:

// Old (third-party) interface โ€” you can't change this
public class OldAnalytics {
    public void trackEvent(String eventName, String jsonData) {
        System.out.println("Old analytics: " + eventName + " โ†’ " + jsonData);
    }
}

// New interface your system expects
public interface Analytics {
    void track(String event, Map<String, Object> properties);
}

// The adapter โ€” bridges old and new
public class OldAnalyticsAdapter implements Analytics {
    private final OldAnalytics oldAnalytics;

    public OldAnalyticsAdapter(OldAnalytics oldAnalytics) {
        this.oldAnalytics = oldAnalytics;
    }

    @Override
    public void track(String event, Map<String, Object> properties) {
        // Convert Map to JSON string for the old API
        String json = new Gson().toJson(properties);
        oldAnalytics.trackEvent(event, json);
    }
}

// Usage
Analytics analytics = new OldAnalyticsAdapter(new OldAnalytics());
analytics.track("page_view", Map.of("url", "/home", "userId", 42));

The adapter wraps OldAnalytics and makes it look like it implements Analytics. Your new code never knows it's talking to an old library.

Python Comparison

class OldAnalytics:
    def track_event(self, event_name, json_data):
        print(f"Old analytics: {event_name} โ†’ {json_data}")

class OldAnalyticsAdapter:
    def __init__(self, old_analytics):
        self._old = old_analytics

    def track(self, event, properties):
        import json
        self._old.track_event(event, json.dumps(properties))

# Usage
analytics = OldAnalyticsAdapter(OldAnalytics())
analytics.track("page_view", {"url": "/home", "user_id": 42})

The adapter pattern works the same in both languages. But in Python, you often need it less thanks to duck typing โ€” if an object has the right methods, it works, regardless of its type. No interface declaration needed. In Java, type compatibility is checked at compile time, so adapters are more frequently necessary.

Real-world example you've probably used: Java's InputStreamReader adapts a byte stream (InputStream) to a character stream (Reader). The entire Java I/O system is built on adapters and decorators wrapping each other. BufferedReader(new InputStreamReader(new FileInputStream("file.txt"))) โ€” three levels of wrapping. It's flexible, but... three levels of wrapping. O_o

Decorator

The problem: You want to add behavior to an object dynamically, without changing its class. Unlike inheritance, which adds behavior at compile time to ALL instances, decorators let you add behavior to individual objects at runtime.

Java Implementation

// Base interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Base implementation
public class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple coffee";
    }

    @Override
    public double getCost() {
        return 2.00;
    }
}

// Decorator base โ€” implements Coffee and wraps a Coffee
public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee wrapped;

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

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

    @Override
    public String getDescription() {
        return wrapped.getDescription() + ", milk";
    }

    @Override
    public double getCost() {
        return wrapped.getCost() + 0.50;
    }
}

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

    @Override
    public String getDescription() {
        return wrapped.getDescription() + ", sugar";
    }

    @Override
    public double getCost() {
        return wrapped.getCost() + 0.25;
    }
}

// Usage โ€” stack decorators
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new SugarDecorator(coffee);  // double sugar

System.out.println(coffee.getDescription());  // Simple coffee, milk, sugar, sugar
System.out.println(coffee.getCost());          // 3.00

Each decorator wraps the previous one, adding its own behavior. You can stack them in any order and any number of times.

Python Comparison

Important: Java's decorator pattern and Python's @decorator syntax are completely different concepts.

Java's decorator pattern wraps objects to add behavior:

# Java-style decorator pattern in Python
class SimpleCoffee:
    def get_description(self):
        return "Simple coffee"

    def get_cost(self):
        return 2.00

class MilkDecorator:
    def __init__(self, coffee):
        self._coffee = coffee

    def get_description(self):
        return self._coffee.get_description() + ", milk"

    def get_cost(self):
        return self._coffee.get_cost() + 0.50

# Usage
coffee = SimpleCoffee()
coffee = MilkDecorator(coffee)

Python's @decorator syntax wraps functions:

# Python's @ decorators โ€” wrapping functions, not objects
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.2f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # prints: slow_function took 1.00s

Same word, completely different concepts. Java decorators wrap objects with new behavior (Milk wrapping Coffee). Python's @decorator wraps functions with new behavior. They solve similar problems (adding behavior without modifying the original) but at different levels. Don't confuse them โ€” I did when I first saw Java decorators.

The classic real-world Java decorator example: I/O streams. new BufferedReader(new InputStreamReader(new FileInputStream("file.txt"))) โ€” each class decorates the previous one, adding buffering, character encoding, etc. It's the decorator pattern all the way down.

When NOT to Use Patterns

This is maybe the most important section. Design patterns are solutions to specific problems. If you don't have the problem, you don't need the pattern.

Signs you might be over-engineering:

  • You're adding a Factory for a class that's only created in one place
  • You're writing a Builder for a class with 3 fields
  • You're implementing Observer when a simple method call would do
  • You're using Strategy when you only have one algorithm
  • You're creating an Adapter when you could just modify the interface (if you own the code)

There's a joke: "A junior developer solves problems with code. A mid-level developer solves problems with design patterns. A senior developer solves problems with the simplest code that works." I've seen codebases with AbstractFactoryBuilderStrategyObserver classes that could have been a single function. Don't be that person. Use patterns when they genuinely reduce complexity, not when they add it.

Quick Reference

Pattern Problem Java Approach Python Alternative
Singleton One instance only enum or private constructor Module-level variable
Factory Decouple object creation Interface + factory class Function + dict
Builder Many constructor params Builder class + chaining Keyword arguments
Observer Event notification Observer interface + list Callback functions
Strategy Swappable algorithms Strategy interface + classes First-class functions
Adapter Interface mismatch Wrapper class Duck typing (often unnecessary)
Decorator Dynamic behavior addition Wrapper + interface @decorator (different concept)

A common thread: many of these patterns exist in Java to work around things that Python gives you natively โ€” first-class functions, keyword arguments, duck typing, module-level singletons. That doesn't make the patterns bad โ€” they provide structure and compile-time safety. But it does explain why Python code tends to be shorter.