3 Real-World Design Patterns in Java: Factory, Observer, and Strategy

3 Real-World Design Patterns in Java: Factory, Observer, and Strategy

6
Photorealistic close-up of a modern software developer's workspace, code displayed on a high-resolution monitor in a dark mode IDE, soft ambient lighting, focus on structure and architecture, 4k high quality.

The Singleton pattern allows for one instance and global access, making it useful for logging or configuration. However, relying solely on Singleton limits architectural flexibility. To build robust production systems, you need patterns that handle object creation, state changes, and algorithmic selection efficiently.

Three practical patterns used frequently in frameworks like Spring and real-world production code are Factory Method, Observer, and Strategy.

Factory Method Pattern

The Factory Method pattern solves the problem of hardcoding class instantiations. If your application creates objects based on user input or configuration, using logic like if type equals A return new ClassA() creates a maintenance burden. This logic tends to spread across the application, making updates difficult.

The Factory Method pushes object creation into subclasses. The main code does not need to know which specific class is being instantiated.

minimalist hand-drawn whiteboard sketchy ink style diagram representing the Factory Method pattern, showing an abstract creator class connecting to different concrete sub-classes, arrows indicating inheritance and object creation, no text.

In this structure, a Product interface defines object behavior. The Creator class defines a factory method but leaves the implementation to the ConcreteCreator subclass. This allows you to swap creators (e.g., swapping a production creator for a mock creator) without changing the core logic.

Implementation

Here is how to implement the Factory Method in Java to decouple object creation.

java
// 1. The Product Interface
// Defines what the objects can actually do.
interface Product {
    void doSomething();
}

// 2. Concrete Product
// The actual object implementation we want to create.
class ConcreteProduct implements Product {
    @Override
    public void doSomething() {
        System.out.println("ConcreteProduct is working.");
    }
}

// 3. Abstract Creator
// Declares the factory method but leaves implementation to subclasses.
abstract class Creator {
    // This is the factory method
    public abstract Product factoryMethod();

    // Business logic that relies on the product
    public void triggerOperation() {
        Product product = factoryMethod();
        product.doSomething();
    }
}

// 4. Concrete Creator
// Implements the factory method to return a specific product.
class ConcreteCreator extends Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProduct();
    }
}

// Usage
public class FactoryDemo {
    public static void main(String[] args) {
        // We use the abstract reference to trigger behavior.
        // The specific type is determined by the ConcreteCreator.
        Creator creator = new ConcreteCreator();
        creator.triggerOperation();
    }
}

This approach mirrors how Spring handles bean injection. Spring does not hardcode class names; it allows configuration to determine which implementation to inject, keeping code testable and decoupled.

Observer Pattern

When you have a data source or system component (the Subject) and multiple other parts of the app need to react to its changes, direct calling is inefficient. Hardcoding dependencies creates tight coupling.

The Observer pattern allows a Subject to notify many dependent Observers automatically when state changes. The Subject does not need to know who the Observers are or what they do. It simply broadcasts a notification.

minimalist hand-drawn whiteboard sketchy ink style diagram representing the Observer pattern, showing a central subject node broadcasting signals to multiple surrounding observer nodes, conveying a one-to-many dependency, no text.

This pattern is fundamental to event-driven systems, UI frameworks, and real-time dashboards like Grafana.

Implementation

The Subject interface handles attaching, detaching, and notifying observers. The Observer interface defines the update method.

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

// 1. Observer Interface
// Defines the method to be called when the subject changes.
interface Observer {
    void update(String message);
}

// 2. Concrete Observer
// Reacts to the notification.

    private String name;


        this.name = name;
    }

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

// 3. Subject Interface
// Methods to manage observers.
interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notifyObservers();
}

// 4. Concrete Subject (Publisher)

class NewPublisher implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String mainState;

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

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

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

    // Logic to change state and trigger notification
    public void publishNewContent(String content) {
        this.mainState = content;
        notifyObservers();
    }
}

// Usage
public class ObserverDemo {
    public static void main(String[] args) {
        NewPublisher publisher = new NewPublisher();




        publisher.attach(sub1);
        publisher.attach(sub2);

        // Simulating a state change

        publisher.publishNewContent("New Video Released");
    }
}

Strategy Pattern

Applications often require supporting multiple algorithms that vary at runtime, such as sorting products by price versus rating, or choosing a GPS route (fastest vs. shortest).

A common mistake is using massive if-else blocks to handle this logic. This violates the Open-Closed Principle because adding a new algorithm requires modifying existing, tested code. The Strategy pattern encapsulates each algorithm in its own class, making them interchangeable.

minimalist hand-drawn whiteboard sketchy ink style diagram representing the Strategy pattern, showing a main context box with three interchangeable algorithm blocks that can be swapped in and out, arrows indicating modularity, no text.

The Context class holds a reference to a Strategy interface. When an operation is needed, the Context delegates the logic to the currently active strategy.

Implementation

This example uses a payment processing system where the method of payment can change dynamically.

java
// 1. Strategy Interface
// Defines the common operation for all algorithms.
interface PaymentStrategy {
    void pay(int amount);
}

// 2. Concrete Strategy A
class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using Credit Card.");
    }
}

// 3. Concrete Strategy B
class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Paid " + amount + " using PayPal.");
    }
}

// 4. Context
// Maintains a reference to a Strategy object.
class PaymentContext {
    private PaymentStrategy strategy;

    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
    
    // Allows swapping the strategy at runtime
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

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

// Usage
public class StrategyDemo {
    public static void main(String[] args) {
        // Start with Credit Card strategy
        PaymentContext context = new PaymentContext(new CreditCardPayment());
        context.executePayment(100);

        // Switch to PayPal strategy at runtime
        // No logic inside PaymentContext needs to change.
        context.setStrategy(new PayPalPayment());
        context.executePayment(200);
    }
}

This works like a mobile phone and SIM cards. The phone is the context, and the SIM card is the strategy. You can swap SIM cards (strategies) to change the carrier behavior without modifying the phone itself.

Summary

These three patterns provide specific architectural solutions:

  • Factory Method: Decouples object creation from business logic.
  • Observer: Enables reactive systems through one-to-many notifications.
  • Strategy: Allows swapping behaviors or algorithms at runtime without altering the context.

Mastering these improves code modularity and maintainability. While other patterns like Decorator, Command, and Adapter are also valuable, these three form the backbone of many scalable Java applications.