Java Design Patterns: Singleton, Factory, Builder, Observer, Strategy and More
Design patterns are reusable solutions to commonly recurring software design problems. In Java interviews, you are expected to know them by name, explain their intent, identify trade-offs, and implement them. This guide covers the most frequently tested patterns with real Java examples.
Creational Patterns
Singleton
Ensures only one instance of a class exists across the application:
java-- Thread-safe Singleton using double-checked locking public class DatabaseConnectionPool { private static volatile DatabaseConnectionPool instance; private final List<Connection> pool = new ArrayList<>(); private DatabaseConnectionPool() { -- Initialize connection pool for (int i = 0; i < 10; i++) { pool.add(createConnection()); } } public static DatabaseConnectionPool getInstance() { if (instance == null) { synchronized (DatabaseConnectionPool.class) { if (instance == null) { instance = new DatabaseConnectionPool(); } } } return instance; } public Connection getConnection() { ... } } -- Better: enum Singleton (thread-safe, serialization-safe) public enum AppConfig { INSTANCE; private final Properties props = loadProperties(); public String get(String key) { return props.getProperty(key); } } -- Usage AppConfig.INSTANCE.get("database.url");
Factory Method
Define an interface for creating objects, but let subclasses decide which class to instantiate:
java-- Abstract product public interface Notification { void send(String recipient, String message); } -- Concrete products public class EmailNotification implements Notification { public void send(String recipient, String message) { System.out.printf("Email to %s: %s%n", recipient, message); } } public class SmsNotification implements Notification { public void send(String recipient, String message) { System.out.printf("SMS to %s: %s%n", recipient, message); } } public class PushNotification implements Notification { public void send(String recipient, String message) { System.out.printf("Push to %s: %s%n", recipient, message); } } -- Factory public class NotificationFactory { public static Notification create(String type) { return switch (type.toLowerCase()) { case "email" -> new EmailNotification(); case "sms" -> new SmsNotification(); case "push" -> new PushNotification(); default -> throw new IllegalArgumentException("Unknown notification type: " + type); }; } } -- Usage Notification n = NotificationFactory.create("email"); n.send("alice@example.com", "Your order has shipped!");
Builder
Construct complex objects step by step, especially when many optional parameters are involved:
javapublic class HttpRequest { private final String url; private final String method; private final Map<String, String> headers; private final String body; private final int timeoutMs; private final int retries; private HttpRequest(Builder builder) { this.url = builder.url; this.method = builder.method; this.headers = Collections.unmodifiableMap(builder.headers); this.body = builder.body; this.timeoutMs = builder.timeoutMs; this.retries = builder.retries; } public static class Builder { private final String url; -- required private String method = "GET"; -- optional with default private Map<String, String> headers = new HashMap<>(); private String body; private int timeoutMs = 5000; private int retries = 0; public Builder(String url) { this.url = Objects.requireNonNull(url, "URL cannot be null"); } public Builder method(String method) { this.method = method; return this; } public Builder header(String name, String value) { this.headers.put(name, value); return this; } public Builder body(String body) { this.body = body; return this; } public Builder timeout(int ms) { this.timeoutMs = ms; return this; } public Builder retries(int retries) { this.retries = retries; return this; } public HttpRequest build() { if ("POST".equals(method) && body == null) { throw new IllegalStateException("POST request requires a body"); } return new HttpRequest(this); } } } -- Usage HttpRequest request = new HttpRequest.Builder("https://api.example.com/users") .method("POST") .header("Content-Type", "application/json") .header("Authorization", "Bearer token123") .body("{\"name\": \"Alice\"}") .timeout(10_000) .retries(3) .build();
Structural Patterns
Decorator
Add behavior to objects dynamically without modifying their class:
javapublic interface DataSource { void writeData(String data); String readData(); } public class FileDataSource implements DataSource { private final String filename; public FileDataSource(String filename) { this.filename = filename; } public void writeData(String data) { -- Write to file } public String readData() { -- Read from file return ""; } } -- Base decorator public abstract class DataSourceDecorator implements DataSource { protected final DataSource wrapped; public DataSourceDecorator(DataSource source) { this.wrapped = source; } } -- Concrete decorators public class EncryptionDecorator extends DataSourceDecorator { public EncryptionDecorator(DataSource source) { super(source); } public void writeData(String data) { wrapped.writeData(encrypt(data)); } public String readData() { return decrypt(wrapped.readData()); } private String encrypt(String data) { return Base64.getEncoder().encodeToString(data.getBytes()); } private String decrypt(String data) { return new String(Base64.getDecoder().decode(data)); } } public class CompressionDecorator extends DataSourceDecorator { public CompressionDecorator(DataSource source) { super(source); } public void writeData(String data) { wrapped.writeData(compress(data)); } public String readData() { return decompress(wrapped.readData()); } private String compress(String data) { return data; } -- simplified private String decompress(String data) { return data; } -- simplified } -- Usage: stack decorators DataSource source = new FileDataSource("data.txt"); source = new CompressionDecorator(source); source = new EncryptionDecorator(source); source.writeData("Hello World"); -- encrypts(compresses("Hello World")) source.readData(); -- decompresses(decrypts(file contents))
Proxy
Provide a substitute that controls access to another object:
javapublic interface UserService { User getUser(Long id); void updateUser(User user); } -- Real implementation public class UserServiceImpl implements UserService { public User getUser(Long id) { return db.findById(id); } public void updateUser(User user) { db.save(user); } } -- Caching proxy public class CachingUserServiceProxy implements UserService { private final UserService delegate; private final Map<Long, User> cache = new ConcurrentHashMap<>(); public CachingUserServiceProxy(UserService delegate) { this.delegate = delegate; } public User getUser(Long id) { return cache.computeIfAbsent(id, delegate::getUser); } public void updateUser(User user) { delegate.updateUser(user); cache.put(user.getId(), user); -- update cache } } -- Logging proxy public class LoggingUserServiceProxy implements UserService { private final UserService delegate; private final Logger log = LoggerFactory.getLogger(getClass()); public User getUser(Long id) { log.info("Fetching user {}", id); long start = System.currentTimeMillis(); User user = delegate.getUser(id); log.info("Fetched user {} in {}ms", id, System.currentTimeMillis() - start); return user; } public void updateUser(User user) { log.info("Updating user {}", user.getId()); delegate.updateUser(user); } }
Behavioral Patterns
Observer (Event Listener)
Define a one-to-many dependency so when one object changes state, all dependents are notified:
javapublic interface EventListener<T> { void onEvent(T event); } public class EventBus { private final Map<Class<?>, List<EventListener<?>>> listeners = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") public <T> void subscribe(Class<T> eventType, EventListener<T> listener) { listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()) .add(listener); } @SuppressWarnings("unchecked") public <T> void publish(T event) { List<EventListener<?>> eventListeners = listeners.get(event.getClass()); if (eventListeners != null) { for (EventListener listener : eventListeners) { listener.onEvent(event); } } } } -- Events public record OrderPlacedEvent(Long orderId, Long userId, BigDecimal total) {} public record OrderShippedEvent(Long orderId, String trackingNumber) {} -- Listeners public class EmailNotificationService { public void onOrderPlaced(OrderPlacedEvent event) { sendEmail(event.userId(), "Your order #" + event.orderId() + " has been placed!"); } } -- Usage EventBus eventBus = new EventBus(); EmailNotificationService emailService = new EmailNotificationService(); eventBus.subscribe(OrderPlacedEvent.class, emailService::onOrderPlaced); eventBus.subscribe(OrderPlacedEvent.class, event -> analyticsService.track(event)); eventBus.publish(new OrderPlacedEvent(42L, 7L, new BigDecimal("99.99")));
Strategy
Define a family of algorithms, encapsulate each, and make them interchangeable:
javapublic interface SortStrategy { void sort(int[] array); } public class BubbleSortStrategy implements SortStrategy { public void sort(int[] array) { for (int i = 0; i < array.length - 1; i++) { for (int j = 0; j < array.length - i - 1; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } } } public class QuickSortStrategy implements SortStrategy { public void sort(int[] array) { quickSort(array, 0, array.length - 1); } -- quickSort implementation... } 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); } } -- Java 8+ functional style public class PaymentProcessor { private final Map<String, Consumer<Payment>> handlers = new HashMap<>(); public PaymentProcessor() { handlers.put("credit_card", this::processCreditCard); handlers.put("paypal", this::processPayPal); handlers.put("crypto", this::processCrypto); } public void process(Payment payment) { Consumer<Payment> handler = handlers.get(payment.getMethod()); if (handler == null) throw new IllegalArgumentException("Unknown method: " + payment.getMethod()); handler.accept(payment); } private void processCreditCard(Payment p) { System.out.println("Processing credit card: " + p); } private void processPayPal(Payment p) { System.out.println("Processing PayPal: " + p); } private void processCrypto(Payment p) { System.out.println("Processing crypto: " + p); } }
Command
Encapsulate requests as objects, enabling undo, logging, and queuing:
javapublic interface Command { void execute(); void undo(); } public class TextEditor { private StringBuilder content = new StringBuilder(); private final Deque<Command> history = new ArrayDeque<>(); public void executeCommand(Command command) { command.execute(); history.push(command); } public void undo() { if (!history.isEmpty()) { history.pop().undo(); } } -- Inner command classes public class InsertCommand implements Command { private final int position; private final String text; public InsertCommand(int position, String text) { this.position = position; this.text = text; } public void execute() { content.insert(position, text); } public void undo() { content.delete(position, position + text.length()); } } public class DeleteCommand implements Command { private final int start, end; private String deletedText; public DeleteCommand(int start, int end) { this.start = start; this.end = end; } public void execute() { deletedText = content.substring(start, end); content.delete(start, end); } public void undo() { content.insert(start, deletedText); } } } -- Usage TextEditor editor = new TextEditor(); editor.executeCommand(editor.new InsertCommand(0, "Hello")); editor.executeCommand(editor.new InsertCommand(5, " World")); editor.undo(); -- removes " World" editor.undo(); -- removes "Hello"
Common Interview Questions
Q: What is the difference between the Factory Method and Abstract Factory patterns?
Factory Method defines one method for creating one type of object β subclasses decide the concrete type. Abstract Factory provides an interface for creating families of related objects β a concrete factory creates multiple related products. Use Factory Method for one product type; use Abstract Factory when you need multiple related products that must be used together (e.g., UI components for a theme: Button, TextField, Checkbox all from the same factory).
Q: When would you use the Proxy pattern vs the Decorator pattern?
Both wrap an object. Decorator adds behavior to enhance functionality β the client typically knows they are using a decorator. Proxy controls access β the client typically does not know a proxy is involved (it appears to be the real object). Proxies are used for lazy initialization, access control, caching, logging, and remote objects.
Q: What is the difference between the Strategy and State patterns?
Both encapsulate behavior. Strategy encapsulates an algorithm that can be changed externally by the client β the object uses different algorithms. State encapsulates behavior that changes based on the object's internal state β the object itself transitions between states, changing its behavior. In Strategy, the client selects the strategy; in State, the object itself manages state transitions.
Practice Java on Froquiz
Design patterns are tested in mid-level and senior Java interviews. Test your Java knowledge on Froquiz β covering OOP, collections, concurrency, and patterns.
Summary
- Singleton: one instance β use enum for thread safety and serialization safety
- Factory Method: create objects by type without exposing concrete classes
- Builder: construct complex objects with many optional parameters using fluent API
- Decorator: add behavior dynamically by wrapping objects β stackable and composable
- Proxy: control access to an object β caching, logging, security, lazy initialization
- Observer: one-to-many notification β publishers do not know their subscribers
- Strategy: swap algorithms at runtime β favors composition over inheritance
- Command: encapsulate requests as objects β enables undo, queuing, and logging