Design Patterns (Common)¶
Pattern Categories¶
Creational Patterns¶
1. Singleton¶
Ensures only one instance of a class exists.
// Thread-safe with lazy initialization
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private DatabaseConnection() { }
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}
// Modern approach: Enum Singleton (preferred)
public enum ConfigManager {
INSTANCE;
private final Properties config = new Properties();
public String get(String key) {
return config.getProperty(key);
}
}
// Usage
ConfigManager.INSTANCE.get("database.url");
Use when: Configuration, connection pools, logging, caches. Drawbacks: Global state, testing difficulties, hidden dependencies.
2. Factory Method¶
Creates objects without specifying exact class.
// Product interface
public interface PaymentProcessor {
void process(Payment payment);
}
// Concrete products
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(Payment payment) {
// Process credit card
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(Payment payment) {
// Process PayPal
}
}
// Factory
public class PaymentProcessorFactory {
public static PaymentProcessor create(PaymentType type) {
return switch (type) {
case CREDIT_CARD -> new CreditCardProcessor();
case PAYPAL -> new PayPalProcessor();
case BANK_TRANSFER -> new BankTransferProcessor();
};
}
}
// Usage
PaymentProcessor processor = PaymentProcessorFactory.create(PaymentType.CREDIT_CARD);
processor.process(payment);
Use when: Class instantiation logic is complex, need to decouple client from concrete classes.
3. Abstract Factory¶
Creates families of related objects.
// Abstract products
public interface Button { void render(); }
public interface TextField { void render(); }
// Abstract factory
public interface UIFactory {
Button createButton();
TextField createTextField();
}
// Concrete factories
public class DarkThemeFactory implements UIFactory {
@Override
public Button createButton() { return new DarkButton(); }
@Override
public TextField createTextField() { return new DarkTextField(); }
}
public class LightThemeFactory implements UIFactory {
@Override
public Button createButton() { return new LightButton(); }
@Override
public TextField createTextField() { return new LightTextField(); }
}
// Usage
UIFactory factory = isDarkMode ? new DarkThemeFactory() : new LightThemeFactory();
Button button = factory.createButton();
TextField textField = factory.createTextField();
Use when: System needs to be independent of how products are created, work with multiple product families.
4. Builder¶
Constructs complex objects step by step.
public class Order {
private final String customerId;
private final List<OrderItem> items;
private final Address shippingAddress;
private final PaymentMethod paymentMethod;
private final String notes;
private Order(Builder builder) {
this.customerId = builder.customerId;
this.items = builder.items;
this.shippingAddress = builder.shippingAddress;
this.paymentMethod = builder.paymentMethod;
this.notes = builder.notes;
}
public static class Builder {
// Required
private final String customerId;
private final List<OrderItem> items = new ArrayList<>();
// Optional
private Address shippingAddress;
private PaymentMethod paymentMethod;
private String notes;
public Builder(String customerId) {
this.customerId = customerId;
}
public Builder addItem(OrderItem item) {
items.add(item);
return this;
}
public Builder shippingAddress(Address address) {
this.shippingAddress = address;
return this;
}
public Builder paymentMethod(PaymentMethod method) {
this.paymentMethod = method;
return this;
}
public Builder notes(String notes) {
this.notes = notes;
return this;
}
public Order build() {
if (items.isEmpty()) {
throw new IllegalStateException("Order must have items");
}
return new Order(this);
}
}
}
// Usage
Order order = new Order.Builder("cust-123")
.addItem(new OrderItem("prod-1", 2))
.addItem(new OrderItem("prod-2", 1))
.shippingAddress(address)
.paymentMethod(PaymentMethod.CREDIT_CARD)
.notes("Gift wrap please")
.build();
Use when: Object has many optional parameters, construction requires multiple steps.
5. Prototype¶
Creates objects by cloning existing ones.
public interface Prototype<T> {
T clone();
}
public class DocumentTemplate implements Prototype<DocumentTemplate> {
private String title;
private String content;
private List<String> sections;
private Map<String, String> metadata;
public DocumentTemplate(DocumentTemplate source) {
this.title = source.title;
this.content = source.content;
this.sections = new ArrayList<>(source.sections);
this.metadata = new HashMap<>(source.metadata);
}
@Override
public DocumentTemplate clone() {
return new DocumentTemplate(this);
}
}
// Registry of prototypes
public class TemplateRegistry {
private final Map<String, DocumentTemplate> templates = new HashMap<>();
public void register(String name, DocumentTemplate template) {
templates.put(name, template);
}
public DocumentTemplate create(String name) {
DocumentTemplate template = templates.get(name);
return template != null ? template.clone() : null;
}
}
Use when: Object creation is expensive, need many similar objects.
Structural Patterns¶
6. Adapter¶
Makes incompatible interfaces work together.
// Target interface (what client expects)
public interface PaymentGateway {
PaymentResult charge(String customerId, BigDecimal amount);
}
// Adaptee (third-party SDK with different interface)
public class StripeSDK {
public StripeCharge createCharge(StripeChargeRequest request) {
// Stripe-specific implementation
}
}
// Adapter
public class StripeAdapter implements PaymentGateway {
private final StripeSDK stripeSDK;
public StripeAdapter(StripeSDK stripeSDK) {
this.stripeSDK = stripeSDK;
}
@Override
public PaymentResult charge(String customerId, BigDecimal amount) {
StripeChargeRequest request = new StripeChargeRequest();
request.setCustomer(customerId);
request.setAmount(amount.multiply(new BigDecimal("100")).intValue()); // Convert to cents
request.setCurrency("usd");
StripeCharge charge = stripeSDK.createCharge(request);
return new PaymentResult(
charge.getId(),
charge.getStatus().equals("succeeded")
);
}
}
Use when: Integrating third-party libraries, legacy code migration.
7. Decorator¶
Adds behavior to objects dynamically.
// Component interface
public interface DataSource {
void writeData(String data);
String readData();
}
// Concrete component
public class FileDataSource implements DataSource {
private final String filename;
@Override
public void writeData(String data) {
// Write to file
}
@Override
public String readData() {
// Read from file
}
}
// Base decorator
public abstract class DataSourceDecorator implements DataSource {
protected final DataSource wrappee;
public DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
@Override
public void writeData(String data) {
wrappee.writeData(data);
}
@Override
public String readData() {
return wrappee.readData();
}
}
// Concrete decorators
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(encrypt(data));
}
@Override
public String readData() {
return decrypt(super.readData());
}
}
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(compress(data));
}
@Override
public String readData() {
return decompress(super.readData());
}
}
// Usage - Stack decorators
DataSource source = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource("data.txt")
)
);
source.writeData("sensitive data"); // Encrypted then compressed
Use when: Add responsibilities dynamically, avoid subclass explosion.
8. Proxy¶
Controls access to an object.
// Subject interface
public interface ImageLoader {
void display();
}
// Real subject
public class RealImage implements ImageLoader {
private final String filename;
private byte[] imageData;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk(); // Expensive operation
}
private void loadFromDisk() {
// Load image data
}
@Override
public void display() {
// Display image
}
}
// Virtual Proxy - Lazy loading
public class LazyImageProxy implements ImageLoader {
private final String filename;
private RealImage realImage;
public LazyImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // Load only when needed
}
realImage.display();
}
}
// Protection Proxy - Access control
public class SecureImageProxy implements ImageLoader {
private final RealImage realImage;
private final User currentUser;
@Override
public void display() {
if (!currentUser.hasPermission("VIEW_IMAGES")) {
throw new AccessDeniedException("No permission to view images");
}
realImage.display();
}
}
// Caching Proxy
public class CachingImageProxy implements ImageLoader {
private final String filename;
private RealImage cachedImage;
private long cacheTime;
private static final long CACHE_DURATION = 60000;
@Override
public void display() {
if (cachedImage == null || System.currentTimeMillis() - cacheTime > CACHE_DURATION) {
cachedImage = new RealImage(filename);
cacheTime = System.currentTimeMillis();
}
cachedImage.display();
}
}
Use when: Lazy loading, access control, logging, caching.
9. Facade¶
Provides simplified interface to complex subsystem.
// Complex subsystem classes
public class InventoryService {
public boolean checkAvailability(String productId, int quantity) { }
public void reserve(String productId, int quantity) { }
}
public class PaymentService {
public PaymentResult authorize(String customerId, BigDecimal amount) { }
public void capture(String authorizationId) { }
}
public class ShippingService {
public ShippingQuote getQuote(Address address, List<Item> items) { }
public String createShipment(Order order) { }
}
public class NotificationService {
public void sendOrderConfirmation(Order order) { }
public void sendShippingNotification(Order order, String trackingId) { }
}
// Facade
public class OrderFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final ShippingService shipping;
private final NotificationService notification;
public OrderResult placeOrder(OrderRequest request) {
// 1. Check inventory
for (OrderItem item : request.getItems()) {
if (!inventory.checkAvailability(item.getProductId(), item.getQuantity())) {
return OrderResult.outOfStock(item.getProductId());
}
}
// 2. Reserve inventory
request.getItems().forEach(item ->
inventory.reserve(item.getProductId(), item.getQuantity())
);
// 3. Process payment
PaymentResult paymentResult = payment.authorize(
request.getCustomerId(), request.getTotal());
if (!paymentResult.isSuccessful()) {
return OrderResult.paymentFailed(paymentResult.getError());
}
// 4. Create shipment
String trackingId = shipping.createShipment(order);
// 5. Capture payment
payment.capture(paymentResult.getAuthorizationId());
// 6. Send notifications
notification.sendOrderConfirmation(order);
return OrderResult.success(order.getId(), trackingId);
}
}
Use when: Simplify complex subsystem, reduce coupling, provide entry point.
10. Composite¶
Treats individual objects and compositions uniformly.
// Component
public interface FileSystemNode {
String getName();
long getSize();
void print(String prefix);
}
// Leaf
public class File implements FileSystemNode {
private final String name;
private final long size;
@Override
public long getSize() {
return size;
}
@Override
public void print(String prefix) {
System.out.println(prefix + name + " (" + size + " bytes)");
}
}
// Composite
public class Directory implements FileSystemNode {
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public void add(FileSystemNode node) {
children.add(node);
}
public void remove(FileSystemNode node) {
children.remove(node);
}
@Override
public long getSize() {
return children.stream()
.mapToLong(FileSystemNode::getSize)
.sum();
}
@Override
public void print(String prefix) {
System.out.println(prefix + name + "/");
children.forEach(child -> child.print(prefix + " "));
}
}
// Usage
Directory root = new Directory("root");
Directory src = new Directory("src");
src.add(new File("Main.java", 1024));
src.add(new File("Utils.java", 512));
root.add(src);
root.add(new File("README.md", 256));
root.print(""); // Print entire tree
long totalSize = root.getSize(); // Sum all sizes
Use when: Tree structures, part-whole hierarchies.
Behavioral Patterns¶
11. Strategy¶
Defines family of algorithms, makes them interchangeable.
// Strategy interface
public interface PricingStrategy {
BigDecimal calculatePrice(Order order);
}
// Concrete strategies
public class RegularPricing implements PricingStrategy {
@Override
public BigDecimal calculatePrice(Order order) {
return order.getSubtotal();
}
}
public class PremiumMemberPricing implements PricingStrategy {
private static final BigDecimal DISCOUNT = new BigDecimal("0.10");
@Override
public BigDecimal calculatePrice(Order order) {
return order.getSubtotal().multiply(BigDecimal.ONE.subtract(DISCOUNT));
}
}
public class HolidaySalePricing implements PricingStrategy {
private static final BigDecimal DISCOUNT = new BigDecimal("0.20");
@Override
public BigDecimal calculatePrice(Order order) {
return order.getSubtotal().multiply(BigDecimal.ONE.subtract(DISCOUNT));
}
}
// Context
public class ShoppingCart {
private PricingStrategy pricingStrategy;
private final List<Item> items = new ArrayList<>();
public void setPricingStrategy(PricingStrategy strategy) {
this.pricingStrategy = strategy;
}
public BigDecimal checkout() {
Order order = new Order(items);
return pricingStrategy.calculatePrice(order);
}
}
// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPricingStrategy(user.isPremium() ?
new PremiumMemberPricing() : new RegularPricing());
BigDecimal total = cart.checkout();
Use when: Multiple algorithms for same task, avoid conditionals.
12. Observer¶
Notifies multiple objects about state changes.
// Observer interface
public interface OrderObserver {
void onOrderCreated(Order order);
void onOrderStatusChanged(Order order, OrderStatus oldStatus, OrderStatus newStatus);
}
// Subject
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public Order createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(new Order(request));
observers.forEach(o -> o.onOrderCreated(order));
return order;
}
public void updateStatus(Long orderId, OrderStatus newStatus) {
Order order = orderRepository.findById(orderId).orElseThrow();
OrderStatus oldStatus = order.getStatus();
order.setStatus(newStatus);
orderRepository.save(order);
observers.forEach(o -> o.onOrderStatusChanged(order, oldStatus, newStatus));
}
}
// Concrete observers
public class InventoryObserver implements OrderObserver {
@Override
public void onOrderCreated(Order order) {
order.getItems().forEach(item ->
inventoryService.reserve(item.getProductId(), item.getQuantity())
);
}
@Override
public void onOrderStatusChanged(Order order, OrderStatus oldStatus, OrderStatus newStatus) {
if (newStatus == OrderStatus.CANCELLED) {
order.getItems().forEach(item ->
inventoryService.release(item.getProductId(), item.getQuantity())
);
}
}
}
public class NotificationObserver implements OrderObserver {
@Override
public void onOrderCreated(Order order) {
emailService.sendOrderConfirmation(order);
}
@Override
public void onOrderStatusChanged(Order order, OrderStatus oldStatus, OrderStatus newStatus) {
emailService.sendStatusUpdate(order, newStatus);
}
}
Use when: One-to-many dependencies, event-driven architecture.
13. Command¶
Encapsulates request as an object.
// Command interface
public interface Command {
void execute();
void undo();
}
// Concrete commands
public class AddItemCommand implements Command {
private final ShoppingCart cart;
private final Item item;
public AddItemCommand(ShoppingCart cart, Item item) {
this.cart = cart;
this.item = item;
}
@Override
public void execute() {
cart.add(item);
}
@Override
public void undo() {
cart.remove(item);
}
}
public class RemoveItemCommand implements Command {
private final ShoppingCart cart;
private final Item item;
private int removedQuantity;
@Override
public void execute() {
removedQuantity = cart.getQuantity(item);
cart.remove(item);
}
@Override
public void undo() {
cart.add(item, removedQuantity);
}
}
// Invoker with history
public class CommandHistory {
private final Deque<Command> history = new ArrayDeque<>();
public void execute(Command command) {
command.execute();
history.push(command);
}
public void undo() {
if (!history.isEmpty()) {
Command command = history.pop();
command.undo();
}
}
}
// Usage
CommandHistory history = new CommandHistory();
history.execute(new AddItemCommand(cart, item1));
history.execute(new AddItemCommand(cart, item2));
history.undo(); // Remove item2
Use when: Undo/redo, queuing operations, logging changes.
14. State¶
Alters object behavior when state changes.
// State interface
public interface OrderState {
void confirm(Order order);
void ship(Order order);
void deliver(Order order);
void cancel(Order order);
}
// Concrete states
public class PendingState implements OrderState {
@Override
public void confirm(Order order) {
// Process payment
order.setState(new ConfirmedState());
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public void ship(Order order) {
throw new IllegalStateException("Cannot ship pending order");
}
@Override
public void deliver(Order order) {
throw new IllegalStateException("Cannot deliver pending order");
}
}
public class ConfirmedState implements OrderState {
@Override
public void ship(Order order) {
// Create shipment
order.setState(new ShippedState());
}
@Override
public void cancel(Order order) {
// Refund payment
order.setState(new CancelledState());
}
@Override
public void confirm(Order order) {
throw new IllegalStateException("Already confirmed");
}
@Override
public void deliver(Order order) {
throw new IllegalStateException("Must ship first");
}
}
// Context
public class Order {
private OrderState state = new PendingState();
public void setState(OrderState state) {
this.state = state;
}
public void confirm() { state.confirm(this); }
public void ship() { state.ship(this); }
public void deliver() { state.deliver(this); }
public void cancel() { state.cancel(this); }
}
Use when: Object behavior depends on state, avoid large conditionals.
15. Template Method¶
Defines algorithm skeleton, lets subclasses override steps.
// Abstract class with template method
public abstract class DataProcessor {
// Template method
public final void process() {
readData();
validate();
transform();
save();
notify();
}
protected abstract void readData();
protected abstract void transform();
// Hook methods (optional override)
protected void validate() {
// Default validation
}
protected void save() {
// Default save
}
protected void notify() {
// Optional notification
}
}
// Concrete implementations
public class CSVDataProcessor extends DataProcessor {
private List<String[]> data;
@Override
protected void readData() {
// Parse CSV file
}
@Override
protected void transform() {
// Transform CSV records
}
@Override
protected void validate() {
// CSV-specific validation
super.validate();
}
}
public class JSONDataProcessor extends DataProcessor {
private JsonNode data;
@Override
protected void readData() {
// Parse JSON file
}
@Override
protected void transform() {
// Transform JSON nodes
}
}
Use when: Algorithm structure is fixed, steps vary by implementation.
16. Chain of Responsibility¶
Passes request along chain of handlers.
// Handler interface
public interface RequestHandler {
void setNext(RequestHandler next);
void handle(Request request);
}
// Base handler
public abstract class BaseHandler implements RequestHandler {
protected RequestHandler next;
@Override
public void setNext(RequestHandler next) {
this.next = next;
}
protected void passToNext(Request request) {
if (next != null) {
next.handle(request);
}
}
}
// Concrete handlers
public class AuthenticationHandler extends BaseHandler {
@Override
public void handle(Request request) {
if (!isAuthenticated(request)) {
throw new UnauthorizedException();
}
passToNext(request);
}
}
public class RateLimitHandler extends BaseHandler {
@Override
public void handle(Request request) {
if (isRateLimited(request.getClientId())) {
throw new RateLimitExceededException();
}
passToNext(request);
}
}
public class ValidationHandler extends BaseHandler {
@Override
public void handle(Request request) {
if (!isValid(request)) {
throw new ValidationException();
}
passToNext(request);
}
}
public class ProcessingHandler extends BaseHandler {
@Override
public void handle(Request request) {
// Actual processing
processRequest(request);
}
}
// Chain setup
RequestHandler chain = new AuthenticationHandler();
chain.setNext(new RateLimitHandler());
chain.getNext().setNext(new ValidationHandler());
chain.getNext().getNext().setNext(new ProcessingHandler());
chain.handle(request);
Use when: Multiple handlers for request, handler order matters.
Pattern Selection Guide¶
Common Interview Questions¶
- Singleton vs Static class?
- Singleton: Can implement interfaces, lazy loading, testable
-
Static: No inheritance, always loaded, simpler
-
Factory vs Abstract Factory?
- Factory: Creates one product type
-
Abstract Factory: Creates families of related products
-
Adapter vs Facade?
- Adapter: Makes incompatible interfaces compatible
-
Facade: Simplifies complex subsystem
-
Strategy vs State?
- Strategy: Client chooses algorithm
-
State: Object changes behavior based on internal state
-
Decorator vs Proxy?
- Decorator: Adds behavior
- Proxy: Controls access