Skip to content

Design Patterns (Common)

Pattern Categories

Design 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

Pattern Selection Guide


Common Interview Questions

  1. Singleton vs Static class?
  2. Singleton: Can implement interfaces, lazy loading, testable
  3. Static: No inheritance, always loaded, simpler

  4. Factory vs Abstract Factory?

  5. Factory: Creates one product type
  6. Abstract Factory: Creates families of related products

  7. Adapter vs Facade?

  8. Adapter: Makes incompatible interfaces compatible
  9. Facade: Simplifies complex subsystem

  10. Strategy vs State?

  11. Strategy: Client chooses algorithm
  12. State: Object changes behavior based on internal state

  13. Decorator vs Proxy?

  14. Decorator: Adds behavior
  15. Proxy: Controls access