Skip to content

Design Patterns Interview Questions

Creational, Structural, and Behavioral design patterns - Gang of Four patterns and their real-world applications.


1. What is the Factory Design Pattern and when should you use it?

View Answer

Direct Answer

Factory Pattern: Creates objects without specifying their exact classes. Returns one of several subclasses based on input parameters.

When to Use

  • Object type unknown at compile time: Decide at runtime which class to instantiate
  • Decouple client from concrete classes: Client doesn't know about implementation
  • Complex object creation logic: Multiple steps or conditional logic in construction
  • Adding new types frequently: Easy to extend without modifying client code

Example

// Product Interface
public interface Notification {
    void send(String to, String message);
}

// Concrete Products
public class EmailNotification implements Notification {
    @Override
    public void send(String to, String message) {
        System.out.println("Sending EMAIL to " + to + ": " + message);
    }
}

public class SmsNotification implements Notification {
    @Override
    public void send(String to, String message) {
        System.out.println("Sending SMS to " + to + ": " + message);
    }
}

// Factory
public class NotificationFactory {
    public static Notification create(String channel) {
        if (channel == null) throw new IllegalArgumentException("Channel required");

        switch (channel.toLowerCase()) {
            case "email": return new EmailNotification();
            case "sms":   return new SmsNotification();
            case "push":  return new PushNotification();
            default: throw new IllegalArgumentException("Unknown channel");
        }
    }
}

// Usage
Notification notification = NotificationFactory.create("email");
notification.send("user@example.com", "Welcome!");

Advantages vs Disadvantages

AdvantagesDisadvantages
✅ Decouples client from concrete classes❌ Added complexity for simple cases
✅ Easy to add new product types❌ Factory class grows with each new type
✅ Centralizes object creation logic❌ May violate Open/Closed principle if not careful
✅ Encapsulates instantiation

📚 Learn more: Design Patterns


2. What's the difference between Factory and Abstract Factory patterns?

View Answer

Quick Comparison

AspectFactory PatternAbstract Factory
CreatesOne product typeFamilies of related products
ReturnsSingle interfaceMultiple interfaces
Use CaseSimple object creationCoordinated product families
ComplexityLowHigh
ExampleNotification factory (email/SMS)UI factory (Windows/Mac buttons, menus)

Abstract Factory Example

// Abstract products
public interface Button { void click(); }
public interface Checkbox { void toggle(); }

// Concrete products for Windows
public class WindowsButton implements Button {
    @Override
    public void click() { System.out.println("Windows Button clicked"); }
}

public class WindowsCheckbox implements Checkbox {
    @Override
    public void toggle() { System.out.println("Windows Checkbox toggled"); }
}

// Concrete products for Mac
public class MacButton implements Button {
    @Override
    public void click() { System.out.println("Mac Button clicked"); }
}

public class MacCheckbox implements Checkbox {
    @Override
    public void toggle() { System.out.println("Mac Checkbox toggled"); }
}

// Abstract Factory
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// Concrete Factories
public class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton() { return new WindowsButton(); }

    @Override
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}

public class MacUIFactory implements UIFactory {
    @Override
    public Button createButton() { return new MacButton(); }

    @Override
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

// Usage
UIFactory factory = osType.equals("windows") ? new WindowsUIFactory() : new MacUIFactory();
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();

📚 Learn more: Design Patterns


3. Explain the Builder Design Pattern with a real example

View Answer

Direct Answer

Builder Pattern: Constructs complex objects step-by-step, separating construction from representation. Useful for objects with many optional parameters.

When to Use

  • Many optional parameters: Avoid constructor explosion
  • Readable object creation: Fluent, readable syntax
  • Immutable objects: Build then set final state
  • Complex initialization: Multi-step setup process

Real-World Example: HTTP Request Builder

public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final int timeout;
    private final boolean followRedirects;

    // Private constructor - only builder can create
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = builder.headers;
        this.body = builder.body;
        this.timeout = builder.timeout;
        this.followRedirects = builder.followRedirects;
    }

    // Builder class
    public static class Builder {
        private final String url;  // Required
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body = "";
        private int timeout = 30000;
        private boolean followRedirects = true;

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

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public Builder followRedirects(boolean follow) {
            this.followRedirects = follow;
            return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }
}

// Usage: Clean, readable, type-safe
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
    .method("POST")
    .header("Authorization", "Bearer token123")
    .header("Content-Type", "application/json")
    .body("{\"name\": \"John\"}")
    .timeout(5000)
    .followRedirects(false)
    .build();

Benefits

  • ✅ Fluent, readable syntax
  • ✅ Handles optional parameters cleanly
  • ✅ Immutability (all fields final)
  • ✅ Single Responsibility (Builder separate from HttpRequest)
  • ✅ Validation can be done in build() method

📚 Learn more: Design Patterns


4. What is the Singleton Pattern and when is it problematic?

View Answer

Direct Answer

Singleton: Restricts a class to have only one instance, providing global access to that instance.

Thread-Safe Implementation (Bill Pugh)

public class DatabaseConnection {

    // Private constructor - prevents instantiation
    private DatabaseConnection() {
    }

    // Static inner class - loaded only when needed
    private static class SingletonHelper {
        private static final DatabaseConnection INSTANCE = new DatabaseConnection();
    }

    // Public accessor
    public static DatabaseConnection getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public void connect() {
        System.out.println("Database connected");
    }
}

// Usage
DatabaseConnection db = DatabaseConnection.getInstance();
db.connect();

When Problematic ⚠️

ProblemImpactSolution
Hard to testCannot mock or inject; couples tests to singletonUse dependency injection instead
Hidden dependenciesGlobal state makes code hard to understandMake dependencies explicit in constructor
Multi-threading issuesRace conditions if not implemented carefullyUse Bill Pugh or enum approach
Concurrency scalingSingle instance bottleneck in distributed systemsUse connection pool or service registry

Better Alternative: Dependency Injection

// ✅ PREFERRED: Inject dependency
@Service
public class OrderService {
    private final DatabaseConnection db;

    public OrderService(DatabaseConnection db) {
        this.db = db;  // Injected - testable, mockable
    }

    public void createOrder(Order order) {
        db.connect();
        // Process order
    }
}

// In tests: Easy to mock
@Test
public void testCreateOrder() {
    DatabaseConnection mockDb = mock(DatabaseConnection.class);
    OrderService service = new OrderService(mockDb);
    // Test without touching real DB
}

When Singleton is Appropriate

  • ✅ Logger factories (Log4j, SLF4J)
  • ✅ Thread pools and executors
  • ✅ Database connection pools
  • ✅ Cache managers
  • ❌ Service objects (use DI instead)

📚 Learn more: Design Patterns


5. Explain the Strategy Pattern and provide a use case

View Answer

Direct Answer

Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Lets clients choose the algorithm at runtime.

Real-World Example: Payment Processing

// Strategy interface
public interface PaymentStrategy {
    boolean pay(double amount);
    String getPaymentMethod();
}

// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public boolean pay(double amount) {
        System.out.println("Processing credit card payment: $" + amount);
        return true;
    }

    @Override
    public String getPaymentMethod() {
        return "Credit Card";
    }
}

public class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public boolean pay(double amount) {
        System.out.println("Processing PayPal payment: $" + amount);
        return true;
    }

    @Override
    public String getPaymentMethod() {
        return "PayPal";
    }
}

public class CryptoCurrencyPayment implements PaymentStrategy {
    private String walletAddress;

    public CryptoCurrencyPayment(String walletAddress) {
        this.walletAddress = walletAddress;
    }

    @Override
    public boolean pay(double amount) {
        System.out.println("Processing cryptocurrency payment: $" + amount);
        return true;
    }

    @Override
    public String getPaymentMethod() {
        return "Cryptocurrency";
    }
}

// Context: Uses strategy
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();
    private PaymentStrategy paymentStrategy;

    public void addItem(Item item) {
        items.add(item);
    }

    public void setPaymentMethod(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout() {
        double total = items.stream().mapToDouble(Item::getPrice).sum();

        if (paymentStrategy.pay(total)) {
            System.out.println("✅ Payment successful via " + paymentStrategy.getPaymentMethod());
        } else {
            System.out.println("❌ Payment failed");
        }
    }
}

// Usage: Choose strategy at runtime
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("Book", 25.99));
cart.addItem(new Item("Pen", 5.50));

// Customer chooses payment method
PaymentStrategy paymentMethod = new CreditCardPayment("1234-5678-9012-3456");
cart.setPaymentMethod(paymentMethod);
cart.checkout();

// Later, customer switches method
paymentMethod = new PayPalPayment("customer@paypal.com");
cart.setPaymentMethod(paymentMethod);
cart.checkout();

Benefits

  • ✅ Runtime algorithm selection
  • ✅ Encapsulates behavior variations
  • ✅ Easy to add new strategies
  • ✅ Eliminates if/else chains
  • ✅ Open/Closed Principle compliant

Real-World Uses

  • Sorting algorithms (Collections.sort with Comparator)
  • Payment processing (credit card, PayPal, crypto)
  • Compression strategies (ZIP, GZIP, BZIP2)
  • Export formats (PDF, Excel, CSV)
  • Authentication methods (JWT, OAuth2, Basic Auth)

📚 Learn more: Design Patterns


6. What is the Observer Pattern and when would you use it?

View Answer

Direct Answer

Observer Pattern: Defines one-to-many relationship where many observer objects watch a subject object. When subject changes state, all observers are notified automatically.

Real-World Example: Event Publishing System

// Subject
public class OrderService {
    private List<OrderObserver> observers = new ArrayList<>();

    public void subscribe(OrderObserver observer) {
        observers.add(observer);
    }

    public void unsubscribe(OrderObserver observer) {
        observers.remove(observer);
    }

    public void createOrder(Order order) {
        // Save order to database
        System.out.println("✅ Order created: " + order.getId());

        // Notify all observers
        notifyOrderCreated(order);
    }

    private void notifyOrderCreated(Order order) {
        for (OrderObserver observer : observers) {
            observer.onOrderCreated(order);
        }
    }
}

// Observer interface
public interface OrderObserver {
    void onOrderCreated(Order order);
}

// Concrete observers
public class EmailNotificationObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        System.out.println("📧 Sending confirmation email to customer");
    }
}

public class InventoryObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        System.out.println("📦 Updating inventory for items in order");
    }
}

public class PaymentObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        System.out.println("💳 Processing payment for order");
    }
}

public class AnalyticsObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        System.out.println("📊 Logging order event for analytics");
    }
}

// Usage
OrderService orderService = new OrderService();

// Subscribe observers
orderService.subscribe(new EmailNotificationObserver());
orderService.subscribe(new InventoryObserver());
orderService.subscribe(new PaymentObserver());
orderService.subscribe(new AnalyticsObserver());

// Create order - all observers notified automatically
Order order = new Order("ORD-123");
orderService.createOrder(order);
// Output:
// ✅ Order created: ORD-123
// 📧 Sending confirmation email to customer
// 📦 Updating inventory for items in order
// 💳 Processing payment for order
// 📊 Logging order event for analytics

Java's Built-in Observer Mechanism

// Java provides PropertyChangeListener for Observer pattern
public class Order extends Observable {
    private String status;

    public void setStatus(String newStatus) {
        String oldStatus = this.status;
        this.status = newStatus;

        // Notify observers
        setChanged();
        notifyObservers(new StatusChangeEvent(oldStatus, newStatus));
    }
}

// Observer
public class OrderStatusListener implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        StatusChangeEvent event = (StatusChangeEvent) arg;
        System.out.println("Order status changed from " + event.oldStatus + " to " + event.newStatus);
    }
}

Observer vs Pub/Sub

AspectObserverPub/Sub (Event-Driven)
CouplingTightly coupled (direct references)Loosely coupled (via broker)
CommunicationDirect method callsThrough message broker
ScaleGood for small appsBetter for microservices
PerformanceFaster (in-process)Higher latency (network)
ExampleUI listeners, event handlersKafka, RabbitMQ, SNS/SQS

📚 Learn more: Design Patterns


7. Explain the Decorator Pattern with an example

View Answer

Direct Answer

Decorator Pattern: Attaches responsibilities to objects dynamically. Provides a flexible alternative to subclassing for extending functionality.

Real-World Example: Coffee Ordering System

// Component interface
public interface Coffee {
    double getCost();
    String getDescription();
}

// Concrete component
public class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 2.00;
    }

    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
}

// Abstract decorator
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
}

// Concrete decorators
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.50;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }
}

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.10;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }
}

public class CaramelDecorator extends CoffeeDecorator {
    public CaramelDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.75;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Caramel";
    }
}

// Usage: Stack decorators dynamically
Coffee coffee = new SimpleCoffee();  // $2.00

coffee = new MilkDecorator(coffee);      // $2.50
coffee = new SugarDecorator(coffee);     // $2.60
coffee = new CaramelDecorator(coffee);   // $3.35

System.out.println(coffee.getDescription());  // Simple Coffee, Milk, Sugar, Caramel
System.out.println("Cost: $" + coffee.getCost());  // Cost: $3.35

Decorator vs Inheritance

AspectDecoratorInheritance
Flexibility✅ Runtime composition❌ Compile-time
Combination✅ Any combinations❌ Limited hierarchy
Complexity❌ More objects✅ Simpler
Maintenance✅ Open/Closed❌ Explosion of classes
ExampleMilkDecorator(SugarDecorator(coffee))MilkSugarCoffee extends Coffee

Real-World Use Cases

  • Java I/O Streams (BufferedInputStream, DataInputStream)
  • Servlet filters (request/response decoration)
  • UI component styling (add borders, colors, shadows)
  • Logging decorators (add logging to any object)
  • Security decorators (add authentication/authorization)

📚 Learn more: Design Patterns


Additional Design Patterns

See Design Patterns Reference for: - Creational: Prototype, Abstract Factory, Builder, Factory Method, Singleton - Structural: Adapter, Bridge, Composite, Facade, Flyweight, Proxy - Behavioral: Chain of Responsibility, Command, Iterator, Mediator, Memento, State, Strategy, Template Method, Visitor, Interpreter


See also: Gang of Four Design Patterns, Interview Questions Overview