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
| Advantages | Disadvantages |
|---|---|
| ✅ 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 |
2. What's the difference between Factory and Abstract Factory patterns?¶
View Answer
Quick Comparison
| Aspect | Factory Pattern | Abstract Factory |
|---|---|---|
| Creates | One product type | Families of related products |
| Returns | Single interface | Multiple interfaces |
| Use Case | Simple object creation | Coordinated product families |
| Complexity | Low | High |
| Example | Notification 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();
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
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 ⚠️
| Problem | Impact | Solution |
|---|---|---|
| Hard to test | Cannot mock or inject; couples tests to singleton | Use dependency injection instead |
| Hidden dependencies | Global state makes code hard to understand | Make dependencies explicit in constructor |
| Multi-threading issues | Race conditions if not implemented carefully | Use Bill Pugh or enum approach |
| Concurrency scaling | Single instance bottleneck in distributed systems | Use 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)
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)
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
| Aspect | Observer | Pub/Sub (Event-Driven) |
|---|---|---|
| Coupling | Tightly coupled (direct references) | Loosely coupled (via broker) |
| Communication | Direct method calls | Through message broker |
| Scale | Good for small apps | Better for microservices |
| Performance | Faster (in-process) | Higher latency (network) |
| Example | UI listeners, event handlers | Kafka, RabbitMQ, SNS/SQS |
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
| Aspect | Decorator | Inheritance |
|---|---|---|
| Flexibility | ✅ Runtime composition | ❌ Compile-time |
| Combination | ✅ Any combinations | ❌ Limited hierarchy |
| Complexity | ❌ More objects | ✅ Simpler |
| Maintenance | ✅ Open/Closed | ❌ Explosion of classes |
| Example | MilkDecorator(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)
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