Design Patterns in Java

Proven solutions to recurring problems — and when NOT to use them

Pattern Categories

Creational

Object creation mechanisms. Singleton, Factory, Builder, Prototype.

Structural

Object composition. Adapter, Decorator, Facade, Proxy, Composite.

Behavioral

Object communication. Strategy, Observer, Command, Template Method.

Architect's Warning

Patterns are tools, not goals. The worst codebases are over-engineered with patterns used for their own sake. Apply a pattern when you have the problem it solves, not preemptively.

Creational Patterns CREATIONAL

Singleton — One Instance, Global Access

Problem: Need exactly one instance of a class (database connection pool, configuration, logger).

# Python: module-level is effectively singleton
# config.py
class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load_config()
        return cls._instance

    def _load_config(self):
        self.db_url = "postgresql://..."

# Or just use a module (simpler)
# config.py
DB_URL = "postgresql://..."
# Importing the module gives same instance
// Classic double-checked locking (thread-safe)
public class Config {
    private static volatile Config instance;
    private final String dbUrl;

    private Config() {
        // Private constructor - can't instantiate from outside
        this.dbUrl = loadFromFile();
    }

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

    public String getDbUrl() {
        return dbUrl;
    }
}

// Usage
Config config = Config.getInstance();
config.getDbUrl();
// RECOMMENDED: Enum singleton (thread-safe, serialization-safe)
public enum Config {
    INSTANCE;

    private final String dbUrl;

    Config() {
        this.dbUrl = loadFromFile();
    }

    public String getDbUrl() {
        return dbUrl;
    }

    private String loadFromFile() {
        return "postgresql://...";
    }
}

// Usage
String url = Config.INSTANCE.getDbUrl();

// Why enum?
// - JVM guarantees single instance
// - Thread-safe by default
// - Serialization works correctly
// - Reflection can't create new instances

// EVEN BETTER: Use Dependency Injection instead!
@Component
public class Config {
    // Spring manages singleton lifecycle
}

When NOT to Use Singleton

Singletons are essentially global state — they make testing hard and hide dependencies. Prefer Dependency Injection (Spring, Guice) which gives you singleton behavior with testability and explicit dependencies.

Factory Method — Delegate Object Creation

Problem: Need to create objects without specifying exact class, or creation logic is complex.

// Simple Factory: static method that creates objects
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);
        };
    }
}

// Usage
Notification notif = NotificationFactory.create("sms");
notif.send("Hello!");

// Real-world Java examples:
List<String> list = List.of("a", "b");     // Factory method
Optional<String> opt = Optional.of("x");   // Factory method
LocalDate date = LocalDate.now();          // Factory method
ExecutorService exec = Executors.newFixedThreadPool(4);  // Factory
// Factory Method: subclasses decide which class to instantiate
public abstract class Document {
    public abstract void open();

    // Factory method - subclasses override
    public abstract Page createPage();

    public void addPage() {
        Page page = createPage();  // Calls subclass implementation
        pages.add(page);
    }
}

public class WordDocument extends Document {
    @Override
    public Page createPage() {
        return new WordPage();  // Word-specific page
    }
}

public class PdfDocument extends Document {
    @Override
    public Page createPage() {
        return new PdfPage();  // PDF-specific page
    }
}

// Client code works with abstract Document
Document doc = getDocument();  // Could be Word or PDF
doc.addPage();  // Creates correct page type automatically
// Abstract Factory: create families of related objects
public interface UIFactory {
    Button createButton();
    TextField createTextField();
    Checkbox createCheckbox();
}

public class MaterialUIFactory implements UIFactory {
    public Button createButton() { return new MaterialButton(); }
    public TextField createTextField() { return new MaterialTextField(); }
    public Checkbox createCheckbox() { return new MaterialCheckbox(); }
}

public class IOSUIFactory implements UIFactory {
    public Button createButton() { return new IOSButton(); }
    public TextField createTextField() { return new IOSTextField(); }
    public Checkbox createCheckbox() { return new IOSCheckbox(); }
}

// Usage: entire UI is consistent
public class Application {
    private final UIFactory factory;

    public Application(UIFactory factory) {
        this.factory = factory;  // Inject the factory
    }

    public void createUI() {
        Button btn = factory.createButton();
        TextField field = factory.createTextField();
        // All components match the same style
    }
}

Builder — Complex Object Construction

Problem: Object has many optional parameters, or construction requires multiple steps.

# Python: keyword arguments handle this naturally
class HttpRequest:
    def __init__(self, url, method="GET", headers=None,
                 body=None, timeout=30):
        self.url = url
        self.method = method
        self.headers = headers or {}
        self.body = body
        self.timeout = timeout

# Named parameters = readable
req = HttpRequest(
    url="https://api.example.com",
    method="POST",
    headers={"Content-Type": "application/json"},
    timeout=60
)

# Python doesn't really need the Builder pattern
// Java: Builder pattern provides named parameters + validation
public class HttpRequest {
    private final String url;        // Required
    private final String method;     // Optional, default GET
    private final Map<String, String> headers;
    private final String body;
    private final Duration timeout;

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Map.copyOf(builder.headers);
        this.body = builder.body;
        this.timeout = builder.timeout;
    }

    public static Builder builder(String url) {
        return new Builder(url);
    }

    // Getters...

    public static class Builder {
        private final String url;  // Required - in constructor
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private Duration timeout = Duration.ofSeconds(30);

        private Builder(String url) {
            this.url = Objects.requireNonNull(url);
        }

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

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

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

        public Builder timeout(Duration timeout) {
            this.timeout = timeout;
            return this;
        }

        public HttpRequest build() {
            // Validation here
            if (method.equals("POST") && body == null) {
                throw new IllegalStateException("POST requires body");
            }
            return new HttpRequest(this);
        }
    }
}

// Usage - fluent, readable, validated
HttpRequest request = HttpRequest.builder("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token")
    .body("{\"key\": \"value\"}")
    .timeout(Duration.ofSeconds(60))
    .build();

When to Use Builder

Lombok generates builders automatically: @Builder

Structural Patterns STRUCTURAL

Adapter — Make Incompatible Interfaces Work Together

Problem: You have an existing class, but its interface doesn't match what you need.

// You need this interface
public interface PaymentProcessor {
    PaymentResult charge(Money amount, Card card);
}

// But you have this legacy/third-party class
public class LegacyPaymentGateway {
    public int processPayment(double amountCents, String cardNumber,
                              String expiry, String cvv) {
        // Returns status code
    }
}

// Adapter makes legacy class work with new interface
public class LegacyPaymentAdapter implements PaymentProcessor {
    private final LegacyPaymentGateway gateway;

    public LegacyPaymentAdapter(LegacyPaymentGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public PaymentResult charge(Money amount, Card card) {
        // Translate between interfaces
        double cents = amount.toCents();
        int statusCode = gateway.processPayment(
            cents,
            card.getNumber(),
            card.getExpiry(),
            card.getCvv()
        );

        // Translate result
        return switch (statusCode) {
            case 0 -> PaymentResult.success();
            case 1 -> PaymentResult.declined();
            default -> PaymentResult.error("Unknown status: " + statusCode);
        };
    }
}

// Usage - your code uses the clean interface
PaymentProcessor processor = new LegacyPaymentAdapter(legacyGateway);
PaymentResult result = processor.charge(money, card);

Decorator — Add Behavior Dynamically

Problem: Need to add responsibilities to objects without modifying their class.

# Python has built-in decorator syntax
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Returned: {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

# Class-based decorator
class CachingDecorator:
    def __init__(self, service):
        self.service = service
        self.cache = {}

    def get(self, key):
        if key not in self.cache:
            self.cache[key] = self.service.get(key)
        return self.cache[key]
// Base interface
public interface DataSource {
    String read();
    void write(String data);
}

// Concrete implementation
public class FileDataSource implements DataSource {
    private final String filename;

    public String read() { /* read from file */ }
    public void write(String data) { /* write to file */ }
}

// Base decorator - wraps a DataSource
public abstract class DataSourceDecorator implements DataSource {
    protected final DataSource wrapped;

    public DataSourceDecorator(DataSource source) {
        this.wrapped = source;
    }
}

// Encryption decorator
public class EncryptionDecorator extends DataSourceDecorator {
    public EncryptionDecorator(DataSource source) {
        super(source);
    }

    @Override
    public String read() {
        return decrypt(wrapped.read());  // Add behavior
    }

    @Override
    public void write(String data) {
        wrapped.write(encrypt(data));  // Add behavior
    }
}

// Compression decorator
public class CompressionDecorator extends DataSourceDecorator {
    @Override
    public String read() {
        return decompress(wrapped.read());
    }

    @Override
    public void write(String data) {
        wrapped.write(compress(data));
    }
}

// Usage - decorators are stackable!
DataSource source = new FileDataSource("data.txt");
source = new CompressionDecorator(source);   // Add compression
source = new EncryptionDecorator(source);   // Add encryption

source.write("secret data");  // Compressed, then encrypted, then written
source.read();                // Read, decrypted, decompressed

// Real-world: Java I/O streams are decorators!
new BufferedInputStream(
    new GZIPInputStream(
        new FileInputStream("data.gz")
    )
);

Proxy — Control Access to an Object

Problem: Need to control access, add lazy loading, caching, or logging.

// Interface
public interface Image {
    void display();
}

// Real implementation (expensive to create)
public class HighResImage implements Image {
    private final byte[] data;

    public HighResImage(String path) {
        this.data = loadFromDisk(path);  // Expensive!
    }

    public void display() {
        renderToScreen(data);
    }
}

// LAZY LOADING PROXY
public class LazyImageProxy implements Image {
    private final String path;
    private HighResImage realImage;  // Loaded on demand

    public LazyImageProxy(String path) {
        this.path = path;  // Just store path, don't load yet
    }

    public void display() {
        if (realImage == null) {
            realImage = new HighResImage(path);  // Load now
        }
        realImage.display();
    }
}

// CACHING PROXY
public class CachingUserService implements UserService {
    private final UserService delegate;
    private final Cache<String, User> cache;

    @Override
    public User getUser(String id) {
        return cache.computeIfAbsent(id, delegate::getUser);
    }
}

// LOGGING PROXY (via Java dynamic proxy)
UserService loggingProxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    (proxy, method, args) -> {
        log.info("Calling: {}", method.getName());
        Object result = method.invoke(realService, args);
        log.info("Returned: {}", result);
        return result;
    }
);

// Spring uses proxies extensively for:
// - @Transactional (wrap method in transaction)
// - @Cacheable (cache method results)
// - @Async (run method in separate thread)

Behavioral Patterns BEHAVIORAL

Strategy — Interchangeable Algorithms

Problem: Need to switch between different algorithms or behaviors at runtime.

# Python: just pass functions (first-class functions)
def sort_by_name(users):
    return sorted(users, key=lambda u: u.name)

def sort_by_age(users):
    return sorted(users, key=lambda u: u.age)

def process_users(users, sort_strategy):
    sorted_users = sort_strategy(users)
    # ...

process_users(users, sort_by_name)
process_users(users, sort_by_age)

# Python doesn't need Strategy classes - functions are enough
// Strategy interface
public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;

    public void pay(BigDecimal amount) {
        // Charge credit card
    }
}

public class PayPalPayment implements PaymentStrategy {
    private final String email;

    public void pay(BigDecimal amount) {
        // PayPal payment
    }
}

public class CryptoPayment implements PaymentStrategy {
    public void pay(BigDecimal amount) {
        // Crypto payment
    }
}

// Context uses the strategy
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;

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

    public void checkout() {
        BigDecimal total = calculateTotal();
        paymentStrategy.pay(total);  // Uses selected strategy
    }
}

// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("4111..."));
cart.checkout();

cart.setPaymentStrategy(new PayPalPayment("user@email.com"));
cart.checkout();

// Modern Java: lambdas work if strategy has single method
cart.setPaymentStrategy(amount -> System.out.println("Paid: " + amount));

Observer — Event Notification

Problem: Object needs to notify other objects of state changes without tight coupling.

// Observer interface
public interface OrderEventListener {
    void onOrderPlaced(Order order);
    void onOrderShipped(Order order);
    void onOrderCancelled(Order order);
}

// Subject (Observable)
public class OrderService {
    private final List<OrderEventListener> listeners = new CopyOnWriteArrayList<>();

    public void addListener(OrderEventListener listener) {
        listeners.add(listener);
    }

    public void removeListener(OrderEventListener listener) {
        listeners.remove(listener);
    }

    public void placeOrder(Order order) {
        // Business logic
        saveOrder(order);

        // Notify all observers
        listeners.forEach(l -> l.onOrderPlaced(order));
    }
}

// Concrete observers
public class EmailNotifier implements OrderEventListener {
    public void onOrderPlaced(Order order) {
        sendEmail(order.getCustomer(), "Order confirmed!");
    }
    // ... other methods
}

public class InventoryUpdater implements OrderEventListener {
    public void onOrderPlaced(Order order) {
        decrementStock(order.getItems());
    }
    // ... other methods
}

// Usage
OrderService orderService = new OrderService();
orderService.addListener(new EmailNotifier());
orderService.addListener(new InventoryUpdater());
orderService.addListener(new AnalyticsTracker());

orderService.placeOrder(order);  // All listeners notified
// Modern approach: Spring Events (decoupled, async-capable)

// Event class
public record OrderPlacedEvent(Order order, Instant timestamp) {}

// Publisher
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public void placeOrder(Order order) {
        saveOrder(order);
        eventPublisher.publishEvent(new OrderPlacedEvent(order, Instant.now()));
    }
}

// Listeners (completely decoupled from publisher)
@Component
public class EmailNotifier {
    @EventListener
    public void handleOrderPlaced(OrderPlacedEvent event) {
        sendEmail(event.order().getCustomer(), "Order confirmed!");
    }
}

@Component
public class InventoryUpdater {
    @EventListener
    @Async  // Run in separate thread!
    public void handleOrderPlaced(OrderPlacedEvent event) {
        decrementStock(event.order().getItems());
    }
}

// Benefits over classic Observer:
// - No need to manually register listeners
// - Can be async
// - Transactional events possible
// - Listeners can be conditional

Template Method — Algorithm Skeleton

Problem: Define algorithm structure, but let subclasses customize specific steps.

// Abstract class defines the algorithm skeleton
public abstract class DataProcessor {

    // Template method - defines algorithm structure
    // Marked FINAL so subclasses can't change the flow
    public final void process() {
        Data raw = readData();         // Step 1
        Data validated = validate(raw);  // Step 2
        Data processed = transform(validated);  // Step 3
        save(processed);                 // Step 4
        notifyComplete();                // Step 5 (hook)
    }

    // Abstract methods - subclasses MUST implement
    protected abstract Data readData();
    protected abstract Data transform(Data data);

    // Concrete methods - shared implementation
    protected Data validate(Data data) {
        if (data == null) {
            throw new IllegalArgumentException("Data cannot be null");
        }
        return data;
    }

    protected abstract void save(Data data);

    // Hook - optional override (empty default)
    protected void notifyComplete() {
        // Default: do nothing
    }
}

// Concrete implementation
public class CsvDataProcessor extends DataProcessor {
    @Override
    protected Data readData() {
        return parseCsvFile();  // CSV-specific
    }

    @Override
    protected Data transform(Data data) {
        return cleanCsvData(data);  // CSV-specific
    }

    @Override
    protected void save(Data data) {
        saveToDatabase(data);
    }
}

public class JsonDataProcessor extends DataProcessor {
    @Override
    protected Data readData() {
        return parseJsonFile();  // JSON-specific
    }

    @Override
    protected Data transform(Data data) {
        return normalizeJson(data);  // JSON-specific
    }

    @Override
    protected void save(Data data) {
        saveToDatabase(data);
    }

    @Override
    protected void notifyComplete() {
        sendSlackNotification();  // Custom hook implementation
    }
}

// Usage - algorithm flow is guaranteed
DataProcessor processor = new CsvDataProcessor();
processor.process();  // Always: read → validate → transform → save → notify

Dependency Injection — Inversion of Control

Problem: Classes shouldn't create their own dependencies (hard to test, tightly coupled).

// BAD: Class creates its own dependencies
public class OrderService {
    private final OrderRepository repository = new PostgresOrderRepository();
    private final EmailService emailService = new SmtpEmailService();
    private final PaymentGateway payment = new StripePaymentGateway();

    public void placeOrder(Order order) {
        repository.save(order);
        payment.charge(order.getTotal());
        emailService.send(order.getCustomer(), "Order confirmed");
    }
}

// Problems:
// - Can't test without real database, payment, email
// - Can't swap implementations (e.g., MockPaymentGateway)
// - Hard-coded dependencies = tight coupling
// GOOD: Dependencies are injected (passed in)
public class OrderService {
    private final OrderRepository repository;
    private final EmailService emailService;
    private final PaymentGateway payment;

    // Constructor injection - dependencies are explicit
    public OrderService(
            OrderRepository repository,
            EmailService emailService,
            PaymentGateway payment) {
        this.repository = repository;
        this.emailService = emailService;
        this.payment = payment;
    }

    public void placeOrder(Order order) {
        repository.save(order);
        payment.charge(order.getTotal());
        emailService.send(order.getCustomer(), "Order confirmed");
    }
}

// Production
OrderService service = new OrderService(
    new PostgresOrderRepository(),
    new SmtpEmailService(),
    new StripePaymentGateway()
);

// Testing - inject mocks!
OrderService testService = new OrderService(
    new InMemoryOrderRepository(),
    mock(EmailService.class),
    new MockPaymentGateway()
);
// Spring handles wiring automatically
@Service
public class OrderService {
    private final OrderRepository repository;
    private final EmailService emailService;
    private final PaymentGateway payment;

    // Spring auto-injects matching beans
    public OrderService(
            OrderRepository repository,
            EmailService emailService,
            PaymentGateway payment) {
        this.repository = repository;
        this.emailService = emailService;
        this.payment = payment;
    }
}

// Dependencies are beans too
@Repository
public class PostgresOrderRepository implements OrderRepository { }

@Service
public class SmtpEmailService implements EmailService { }

@Component
public class StripePaymentGateway implements PaymentGateway { }

// In tests, override with test beans
@TestConfiguration
public class TestConfig {
    @Bean
    public PaymentGateway paymentGateway() {
        return new MockPaymentGateway();
    }
}

DI is THE Most Important Pattern

Dependency Injection is the foundation of testable, maintainable enterprise code. It's not about frameworks — it's about designing classes that receive their dependencies rather than creating them. Spring/Guice just automate the wiring.

Patterns Quick Reference

PatternWhen to UseJava Example
SingletonSingle instance needed (config, pools)Enum singleton, Spring @Component
FactoryDecouple object creation from usageList.of(), Optional.of()
BuilderComplex object with many optional paramsStringBuilder, HttpRequest.Builder
AdapterMake incompatible interfaces work togetherArrays.asList(), InputStreamReader
DecoratorAdd behavior without changing classBufferedInputStream, Collections.unmodifiableList()
ProxyControl access, lazy loading, cachingSpring @Transactional, @Cacheable
StrategySwap algorithms at runtimeComparator, Collector
ObserverEvent-driven communicationPropertyChangeListener, Spring Events
Template MethodDefine algorithm skeleton, customize stepsAbstractList, HttpServlet