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:
- Allocate memory
- Assign reference to
instance← other threads see non-null - 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:
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:
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?