Skip to content

Design Patterns

Design patterns are reusable solutions to common software problems. Three categories: Creational (Singleton, Factory, Builder), Structural (Adapter, Decorator, Proxy), Behavioral (Strategy, Observer, Template Method). In interviews, know Singleton (thread-safe), Factory, Strategy, and Observer well — they appear most frequently.

Category Patterns Purpose
Creational Singleton, Factory, Abstract Factory, Builder, Prototype Object creation
Structural Adapter, Decorator, Proxy, Facade, Composite Object composition
Behavioral Strategy, Observer, Template Method, Command, Iterator Object interaction

Singleton Pattern

Singleton ensures a class has exactly one instance with a global access point. In Java, the best approaches are: enum singleton (Joshua Bloch's recommended — handles serialization and reflection), static holder (lazy, no synchronization cost), or double-checked locking (with volatile). In Spring, all beans are singletons by default — but Spring's singleton is per-container, not per-classloader.

Deep Dive: Thread-Safe Implementations

1. Enum singleton (recommended):

public enum DatabaseConnection {
    INSTANCE;
    public void query(String sql) { ... }
}
// Usage: DatabaseConnection.INSTANCE.query("SELECT ...");

2. Static holder (lazy, no synchronization cost):

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;  // Loaded on first access (JVM guarantees thread-safety)
    }
}

3. Double-checked locking:

public class Singleton {
    private static volatile Singleton instance;  // volatile prevents instruction reordering

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                   // First check (no lock)
            synchronized (Singleton.class) {
                if (instance == null) {            // Second check (with lock)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Deep Dive: Why Volatile Matters in Double-Checked Locking

Without volatile, the JVM may reorder instructions during object construction:

  1. Allocate memory
  2. Assign reference to instance ← other threads see non-null
  3. Call constructor ← but object isn't fully initialized yet!

volatile prevents this reordering, ensuring the object is fully constructed before the reference is published.

Deep Dive: Spring Singleton vs GoF Singleton
GoF Singleton Spring Singleton
Scope One per JVM/classloader One per Spring container
Creation Static method Bean factory
Multiple containers Still one instance One per container
Testing Hard to mock Easy to mock (DI)

Factory Method Pattern

Factory Method delegates object creation to a method or subclass, decoupling the client from concrete classes. The client asks for an object by type/criteria, and the factory decides which class to instantiate. In Spring, FactoryBean<T>, @Bean methods, and BeanFactory are all factory patterns.

Deep Dive: Factory Method Example
// Product interface
public interface Notification {
    void send(String message);
}

public class EmailNotification implements Notification {
    public void send(String message) { /* send email */ }
}

public class SmsNotification implements Notification {
    public void send(String message) { /* send SMS */ }
}

// Factory
public class NotificationFactory {
    public static Notification create(String type) {
        return switch (type) {
            case "EMAIL" -> new EmailNotification();
            case "SMS" -> new SmsNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage — client doesn't know concrete classes
Notification notif = NotificationFactory.create("EMAIL");
notif.send("Hello");
Deep Dive: Factory vs Abstract Factory
Factory Method Abstract Factory
Creates One product type Family of related products
Method vs Class Single method Interface with multiple factory methods
Example NotificationFactory.create("EMAIL") UIFactory.createButton(), UIFactory.createCheckbox()

Builder Pattern

Builder constructs complex objects step by step, separating construction from representation. Useful when a class has many optional parameters — avoids telescoping constructors. Returns an immutable object. Lombok's @Builder annotation generates this automatically.

Deep Dive: Builder Example
public class User {
    private final String name;
    private final String email;
    private final int age;
    private final String phone;

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

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

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

        public Builder age(int age) { this.age = age; return this; }
        public Builder phone(String phone) { this.phone = phone; return this; }
        public User build() { return new User(this); }
    }
}

// Usage — readable, flexible
User user = new User.Builder("John", "john@test.com")
    .age(25)
    .phone("123-456-7890")
    .build();

Shortcut with Lombok:

@Builder
@Getter
public class User {
    private final String name;
    private final String email;
    @Builder.Default private int age = 0;
    private String phone;
}


Strategy Pattern

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. The context object delegates behavior to a strategy interface. This is the OCP in action — add new algorithms without modifying existing code. In Spring, DI + interfaces naturally implement the Strategy pattern.

Deep Dive: Strategy Example
// Strategy interface
public interface SortStrategy {
    void sort(int[] array);
}

public class QuickSort implements SortStrategy {
    public void sort(int[] array) { /* quicksort */ }
}

public class MergeSort implements SortStrategy {
    public void sort(int[] array) { /* mergesort */ }
}

// Context — delegates to strategy
public class Sorter {
    private SortStrategy strategy;

    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

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

    public void sort(int[] array) {
        strategy.sort(array);
    }
}

// Usage — swap algorithms at runtime
Sorter sorter = new Sorter(new QuickSort());
sorter.sort(data);
sorter.setStrategy(new MergeSort());
sorter.sort(data);

In Spring — just inject what you need:

@Service
public class PaymentService {
    private final PaymentGateway gateway; // Strategy interface

    public PaymentService(PaymentGateway gateway) {
        this.gateway = gateway;  // Spring injects the right implementation
    }
}


Observer Pattern

Observer notifies multiple subscribers about state changes in a publisher (pub-sub). The publisher doesn't know who its subscribers are — it just fires events. In Spring, this is built-in: ApplicationEventPublisher + @EventListener. Used for: user registration flows, audit logging, cache invalidation.

Deep Dive: Observer Example
// Observer
public interface EventListener {
    void onEvent(String event);
}

// Subject (publisher)
public class EventPublisher {
    private final List<EventListener> listeners = new ArrayList<>();

    public void subscribe(EventListener listener) {
        listeners.add(listener);
    }

    public void publish(String event) {
        listeners.forEach(l -> l.onEvent(event));
    }
}

// Usage
EventPublisher publisher = new EventPublisher();
publisher.subscribe(event -> System.out.println("Email: " + event));
publisher.subscribe(event -> System.out.println("Log: " + event));
publisher.publish("User registered");
Deep Dive: Observer in Spring
// 1. Define event
public record UserRegisteredEvent(User user) {}

// 2. Publish event
@Service
public class UserService {
    private final ApplicationEventPublisher publisher;
    public void register(User user) {
        // ... save user
        publisher.publishEvent(new UserRegisteredEvent(user));
    }
}

// 3. Listen to event — decoupled from publisher
@Component
public class WelcomeEmailListener {
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        sendWelcomeEmail(event.user());
    }
}

@Component
public class AuditLogListener {
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        logAuditEntry(event.user());
    }
}

Decorator Pattern

Decorator adds behavior to objects dynamically without modifying them. It wraps the original object with a decorator that implements the same interface, adding functionality before/after delegating to the wrapped object. Classic Java example: BufferedReader(new InputStreamReader(new FileInputStream(...))).

Deep Dive: Decorator Example
public interface DataSource {
    String readData();
    void writeData(String data);
}

public class FileDataSource implements DataSource {
    public String readData() { return "raw data"; }
    public void writeData(String data) { /* write to file */ }
}

// Decorator — adds encryption
public class EncryptionDecorator implements DataSource {
    private final DataSource wrapped;
    public EncryptionDecorator(DataSource source) { this.wrapped = source; }
    public String readData() { return decrypt(wrapped.readData()); }
    public void writeData(String data) { wrapped.writeData(encrypt(data)); }
}

// Decorator — adds compression
public class CompressionDecorator implements DataSource {
    private final DataSource wrapped;
    public CompressionDecorator(DataSource source) { this.wrapped = source; }
    public String readData() { return decompress(wrapped.readData()); }
    public void writeData(String data) { wrapped.writeData(compress(data)); }
}

// Stack decorators — compress → encrypt → write
DataSource source = new CompressionDecorator(
    new EncryptionDecorator(
        new FileDataSource()));
source.writeData("hello");

Proxy Pattern

Proxy controls access to an object by providing a surrogate. Types: virtual proxy (lazy loading), protection proxy (access control), remote proxy (network calls). In Spring, AOP uses proxies: @Transactional, @Cacheable, @Async all work through proxy wrapping.

Deep Dive: Proxy Example
public interface ImageLoader {
    void display();
}

// Real object (expensive to create)
public class HighResImage implements ImageLoader {
    public HighResImage(String url) { loadFromDisk(url); } // Slow!
    public void display() { /* render */ }
}

// Proxy — lazy loading, only loads real image on first use
public class ImageProxy implements ImageLoader {
    private HighResImage realImage;
    private final String url;

    public ImageProxy(String url) { this.url = url; }

    public void display() {
        if (realImage == null) {
            realImage = new HighResImage(url);  // Load on first access
        }
        realImage.display();
    }
}
Deep Dive: Proxy in Spring AOP

Spring creates proxies around beans to add cross-cutting concerns:

  • @Transactional → proxy starts/commits/rollbacks transaction
  • @Cacheable → proxy checks cache before calling method
  • @Async → proxy runs method in a separate thread

Two proxy types:

  • JDK Dynamic Proxy — interface-based (default when bean implements an interface)
  • CGLIB Proxy — subclass-based (default for classes without interfaces)

Gotcha: Self-invocation (this.method()) bypasses the proxy — the annotation won't work.


Common Interview Questions

Common Interview Questions
  • Explain the Singleton pattern. How do you make it thread-safe?
  • What is the Factory pattern? When would you use it?
  • What is the difference between Factory and Abstract Factory?
  • Explain the Strategy pattern with an example.
  • How is the Observer pattern used in Spring?
  • What is the Decorator pattern? Give a Java standard library example.
  • How does Spring use the Proxy pattern?
  • What is the Template Method pattern?
  • What is the Builder pattern? When is it useful?
  • Name design patterns used in the Spring Framework.
  • Why is volatile needed in Double-Checked Locking?