Design Patterns
Dashboard
Topic 32/33
Home › Design Patterns

🧩 Design Patterns with Real-Time Examples

Master the most essential Gang of Four (GoF) design patterns in Java — with real-world scenarios, production-grade code, and Spring Boot integration examples.

What Are Design Patterns?

Design Patterns are proven, reusable solutions to common problems that arise during software design. They were popularized by the Gang of Four (GoF) book and are categorized into three families:

CategoryPurposePatterns Covered
CreationalHow objects are createdSingleton, Factory Method, Builder
StructuralHow objects are composedAdapter, Decorator, Proxy
BehavioralHow objects communicateObserver, Strategy, Template Method
Why Learn Patterns? Every Spring Boot application you write already uses design patterns (Factory, Proxy, Template Method, Singleton, Observer). Understanding them makes you a stronger developer and gives you the vocabulary to discuss architecture in interviews and code reviews.

1. Singleton Pattern (Creational)

Ensures a class has only one instance throughout the application and provides a global point of access to it.

Real-Time Example: Database Connection Pool

In any enterprise application, you want one shared connection pool rather than creating new database connections on every request. The pool is expensive to create and must be shared across all threads.

DatabaseConnectionPool.java Java
public class DatabaseConnectionPool {

    // Volatile ensures visibility across threads
    private static volatile DatabaseConnectionPool instance;
    private final List<Connection> pool;

    // Private constructor prevents external instantiation
    private DatabaseConnectionPool(int poolSize) {
        pool = new ArrayList<>();
        for (int i = 0; i < poolSize; i++) {
            pool.add(createNewConnection());
        }
        System.out.println("Pool created with " + poolSize + " connections");
    }

    // Double-checked locking for thread-safe lazy initialization
    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionPool(10);
                }
            }
        }
        return instance;
    }

    public Connection getConnection() {
        // Return an available connection from the pool
        return pool.remove(pool.size() - 1);
    }

    public void releaseConnection(Connection conn) {
        pool.add(conn);
    }
}
Usage
DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance();
Connection conn = pool.getConnection();
// ... use connection ...
pool.releaseConnection(conn);
Spring Boot: All @Bean definitions are Singletons by default (@Scope("singleton")). Spring manages the single instance for you via the IoC container.

2. Factory Method Pattern (Creational)

Defines an interface for creating objects, but lets subclasses decide which class to instantiate. Decouples object creation from usage.

Real-Time Example: Payment Processing System

An e-commerce platform supports multiple payment methods (Credit Card, UPI, PayPal). The checkout service shouldn’t know the implementation details of each payment processor.

PaymentFactory.java Java
// 1. Common interface
public interface PaymentProcessor {
    void processPayment(double amount);
    String getPaymentMethod();
}

// 2. Concrete implementations
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing $" + amount + " via Credit Card gateway...");
        // Connect to Stripe/Razorpay API
    }
    @Override
    public String getPaymentMethod() { return "CREDIT_CARD"; }
}

public class UpiProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing ₹" + amount + " via UPI...");
        // Connect to UPI gateway
    }
    @Override
    public String getPaymentMethod() { return "UPI"; }
}

// 3. Factory class
public class PaymentProcessorFactory {
    public static PaymentProcessor create(String method) {
        return switch (method.toUpperCase()) {
            case "CREDIT_CARD" -> new CreditCardProcessor();
            case "UPI"         -> new UpiProcessor();
            case "PAYPAL"      -> new PayPalProcessor();
            default -> throw new IllegalArgumentException("Unknown: " + method);
        };
    }
}

// 4. Client code — completely decoupled from implementations
PaymentProcessor processor = PaymentProcessorFactory.create("UPI");
processor.processPayment(1500.00);
Output
Processing ₹1500.0 via UPI...

3. Builder Pattern (Creational)

Separates the construction of a complex object from its representation, allowing the same construction process to create different representations. Perfect for objects with many optional parameters.

Real-Time Example: Order Builder in a Food Delivery App

Order.java Java
public class Order {
    private final String customerId;
    private final String restaurantId;
    private final List<String> items;
    private final String deliveryAddress;
    private final String couponCode;       // optional
    private final String instructions;     // optional
    private final boolean contactless;     // optional

    private Order(Builder builder) {
        this.customerId     = builder.customerId;
        this.restaurantId   = builder.restaurantId;
        this.items           = builder.items;
        this.deliveryAddress = builder.deliveryAddress;
        this.couponCode      = builder.couponCode;
        this.instructions    = builder.instructions;
        this.contactless     = builder.contactless;
    }

    public static class Builder {
        // Required
        private final String customerId;
        private final String restaurantId;
        private final List<String> items;
        private final String deliveryAddress;
        // Optional — with defaults
        private String couponCode   = null;
        private String instructions = "";
        private boolean contactless = false;

        public Builder(String customerId, String restaurantId,
                       List<String> items, String address) {
            this.customerId     = customerId;
            this.restaurantId   = restaurantId;
            this.items           = items;
            this.deliveryAddress = address;
        }

        public Builder coupon(String code) {
            this.couponCode = code; return this;
        }
        public Builder instructions(String note) {
            this.instructions = note; return this;
        }
        public Builder contactless() {
            this.contactless = true; return this;
        }
        public Order build() {
            return new Order(this);
        }
    }
}

// Usage — clean, readable, no telescoping constructors
Order order = new Order.Builder("C-101", "R-42",
        List.of("Butter Chicken", "Naan", "Raita"),
        "123 MG Road, Bangalore")
    .coupon("SAVE20")
    .instructions("Extra spicy, no onions")
    .contactless()
    .build();

4. Observer Pattern (Behavioral)

Defines a one-to-many dependency between objects. When one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.

Real-Time Example: Order Status Notification System

When an order status changes in an e-commerce app, multiple services need to react: send an email, push a notification, update the dashboard, and notify the delivery partner.

OrderEventSystem.java Java
// 1. Observer interface
public interface OrderObserver {
    void onStatusChange(String orderId, String newStatus);
}

// 2. Concrete observers
public class EmailNotifier implements OrderObserver {
    @Override
    public void onStatusChange(String orderId, String status) {
        System.out.println("📧 Email sent: Order " + orderId + " is now " + status);
    }
}

public class PushNotifier implements OrderObserver {
    @Override
    public void onStatusChange(String orderId, String status) {
        System.out.println("🔔 Push notification: Order " + orderId + " → " + status);
    }
}

public class DashboardUpdater implements OrderObserver {
    @Override
    public void onStatusChange(String orderId, String status) {
        System.out.println("📊 Dashboard updated for order " + orderId);
    }
}

// 3. Subject — the order tracking service
public class OrderTracker {
    private final List<OrderObserver> observers = new ArrayList<>();

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

    public void updateStatus(String orderId, String newStatus) {
        System.out.println("Order " + orderId + " status changed to: " + newStatus);
        observers.forEach(o -> o.onStatusChange(orderId, newStatus));
    }
}

// 4. Usage
OrderTracker tracker = new OrderTracker();
tracker.subscribe(new EmailNotifier());
tracker.subscribe(new PushNotifier());
tracker.subscribe(new DashboardUpdater());

tracker.updateStatus("ORD-5001", "SHIPPED");
Output
Order ORD-5001 status changed to: SHIPPED
📧 Email sent: Order ORD-5001 is now SHIPPED
🔔 Push notification: Order ORD-5001 → SHIPPED
📊 Dashboard updated for order ORD-5001
Spring Boot: Use @EventListener and ApplicationEventPublisher for the same pattern. Spring’s event system is a production-grade Observer implementation.

5. Strategy Pattern (Behavioral)

Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. The client code selects the algorithm without knowing its implementation.

Real-Time Example: Shipping Cost Calculator

ShippingStrategy.java Java
// 1. Strategy interface
public interface ShippingStrategy {
    double calculateCost(double weight, double distance);
}

// 2. Concrete strategies
public class StandardShipping implements ShippingStrategy {
    @Override
    public double calculateCost(double weight, double distance) {
        return weight * 0.5 + distance * 0.1;  // 5-7 days
    }
}

public class ExpressShipping implements ShippingStrategy {
    @Override
    public double calculateCost(double weight, double distance) {
        return weight * 1.5 + distance * 0.3 + 50;  // 1-2 days
    }
}

public class SameDayShipping implements ShippingStrategy {
    @Override
    public double calculateCost(double weight, double distance) {
        return weight * 3.0 + distance * 0.8 + 150;  // Same day
    }
}

// 3. Context — uses strategy at runtime
public class ShippingCalculator {
    private ShippingStrategy strategy;

    public void setStrategy(ShippingStrategy strategy) {
        this.strategy = strategy;
    }

    public double getShippingCost(double weight, double distance) {
        return strategy.calculateCost(weight, distance);
    }
}

// 4. Usage — switch algorithms at runtime
ShippingCalculator calc = new ShippingCalculator();
calc.setStrategy(new ExpressShipping());
System.out.println("Express: ₹" + calc.getShippingCost(2.5, 100));

calc.setStrategy(new SameDayShipping());
System.out.println("Same-Day: ₹" + calc.getShippingCost(2.5, 100));
Output
Express: ₹83.75
Same-Day: ₹237.5

6. Adapter Pattern (Structural)

Allows incompatible interfaces to work together by wrapping one interface with another that the client expects. Acts as a bridge between two incompatible systems.

Real-Time Example: Third-Party Payment Gateway Integration

Your application has a standard PaymentGateway interface, but a new third-party provider (RazorpaySDK) has a completely different API. An adapter makes them compatible.

PaymentAdapter.java Java
// Your application's expected interface
public interface PaymentGateway {
    boolean charge(String customerId, double amount);
}

// Third-party SDK with incompatible API
public class RazorpaySDK {
    public String createOrder(int amountInPaise, String currency) {
        System.out.println("Razorpay: Order created for " + amountInPaise + " paise");
        return "order_RZP_" + System.currentTimeMillis();
    }
    public boolean capturePayment(String orderId) {
        System.out.println("Razorpay: Payment captured for " + orderId);
        return true;
    }
}

// Adapter — bridges your interface with Razorpay's API
public class RazorpayAdapter implements PaymentGateway {
    private final RazorpaySDK razorpay = new RazorpaySDK();

    @Override
    public boolean charge(String customerId, double amount) {
        // Convert rupees to paise and adapt the call
        int paise = (int) (amount * 100);
        String orderId = razorpay.createOrder(paise, "INR");
        return razorpay.capturePayment(orderId);
    }
}

// Client code works with your standard interface
PaymentGateway gateway = new RazorpayAdapter();
gateway.charge("CUST-42", 999.00);
Output
Razorpay: Order created for 99900 paise
Razorpay: Payment captured for order_RZP_1719561234567

7. Decorator Pattern (Structural)

Adds new behavior to objects dynamically by wrapping them, without modifying the original class. Each decorator adds one layer of functionality.

Real-Time Example: Coffee Ordering System (Starbucks-style)

CoffeeDecorator.java Java
// Base interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Base coffee
public class Espresso implements Coffee {
    public String getDescription() { return "Espresso"; }
    public double getCost() { return 150.0; }
}

// Abstract decorator
public abstract class CoffeeDecorator implements Coffee {
    protected final Coffee coffee;
    public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}

// Concrete decorators — each adds one feature
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee c) { super(c); }
    public String getDescription() { return coffee.getDescription() + ", Milk"; }
    public double getCost() { return coffee.getCost() + 30.0; }
}

public class CaramelDecorator extends CoffeeDecorator {
    public CaramelDecorator(Coffee c) { super(c); }
    public String getDescription() { return coffee.getDescription() + ", Caramel"; }
    public double getCost() { return coffee.getCost() + 50.0; }
}

// Stack decorators dynamically!
Coffee myOrder = new CaramelDecorator(new MilkDecorator(new Espresso()));
System.out.println(myOrder.getDescription() + " → ₹" + myOrder.getCost());
Output
Espresso, Milk, Caramel → ₹230.0
In Spring: Java’s BufferedReader(new InputStreamReader(new FileInputStream(...))) is the classic Decorator example. Spring uses this pattern extensively in its Filter chains and HttpServletRequestWrapper.

8. Proxy Pattern (Structural)

Provides a surrogate or placeholder for another object to control access to it. Common uses: lazy loading, access control, logging, and caching.

Real-Time Example: Caching Proxy for API Calls

WeatherServiceProxy.java Java
public interface WeatherService {
    String getForecast(String city);
}

// Real service — expensive API call
public class RealWeatherService implements WeatherService {
    @Override
    public String getForecast(String city) {
        System.out.println("🌐 Calling external weather API for " + city + "...");
        // Simulates HTTP call taking 2 seconds
        return "Sunny, 32°C in " + city;
    }
}

// Caching Proxy — avoids repeated expensive calls
public class CachingWeatherProxy implements WeatherService {
    private final WeatherService realService = new RealWeatherService();
    private final Map<String, String> cache = new HashMap<>();

    @Override
    public String getForecast(String city) {
        if (cache.containsKey(city)) {
            System.out.println("⚡ Cache HIT for " + city);
            return cache.get(city);
        }
        String result = realService.getForecast(city);
        cache.put(city, result);
        return result;
    }
}

WeatherService service = new CachingWeatherProxy();
System.out.println(service.getForecast("Mumbai"));   // API call
System.out.println(service.getForecast("Mumbai"));   // Cache hit!
Output
🌐 Calling external weather API for Mumbai...
Sunny, 32°C in Mumbai
⚡ Cache HIT for Mumbai
Sunny, 32°C in Mumbai
Spring AOP: Spring’s @Transactional, @Cacheable, and @Async annotations all work through dynamic proxies — the Proxy pattern is at the very core of the Spring Framework.

9. Template Method Pattern (Behavioral)

Defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall structure.

Real-Time Example: Report Generation Pipeline

An application generates reports in multiple formats (PDF, Excel, HTML). The overall process is the same (fetch data → process → format → export), but the formatting step differs per output type.

ReportGenerator.java Java
// Abstract template
public abstract class ReportGenerator {

    // Template method — defines the algorithm skeleton
    public final void generateReport() {
        fetchData();
        processData();
        formatReport();   // Subclasses implement this
        exportReport();   // Subclasses implement this
        System.out.println("✅ Report generation complete!
");
    }

    private void fetchData() {
        System.out.println("📊 Fetching data from database...");
    }
    private void processData() {
        System.out.println("⚙️ Processing and aggregating data...");
    }

    // Steps that subclasses must implement
    protected abstract void formatReport();
    protected abstract void exportReport();
}

// Concrete: PDF report
public class PdfReportGenerator extends ReportGenerator {
    protected void formatReport() {
        System.out.println("📄 Formatting as PDF with headers and charts...");
    }
    protected void exportReport() {
        System.out.println("💾 Saving report.pdf to /reports/");
    }
}

// Concrete: Excel report
public class ExcelReportGenerator extends ReportGenerator {
    protected void formatReport() {
        System.out.println("📊 Formatting as Excel with sheets and formulas...");
    }
    protected void exportReport() {
        System.out.println("💾 Saving report.xlsx to /reports/");
    }
}

// Usage
ReportGenerator pdf = new PdfReportGenerator();
pdf.generateReport();

ReportGenerator excel = new ExcelReportGenerator();
excel.generateReport();
Output
📊 Fetching data from database...
⚙️ Processing and aggregating data...
📄 Formatting as PDF with headers and charts...
💾 Saving report.pdf to /reports/
✅ Report generation complete!

📊 Fetching data from database...
⚙️ Processing and aggregating data...
📊 Formatting as Excel with sheets and formulas...
💾 Saving report.xlsx to /reports/
✅ Report generation complete!
Spring Boot: JdbcTemplate, RestTemplate, and JmsTemplate are all implementations of the Template Method pattern — the template handles boilerplate (connections, error handling), and you supply the custom logic via callbacks.

Quick Reference: All Patterns at a Glance

PatternCategoryOne-Line SummarySpring Boot Usage
SingletonCreationalOne instance for the entire application@Bean (default scope)
FactoryCreationalCreate objects without exposing creation logicBeanFactory, FactoryBean
BuilderCreationalStep-by-step construction of complex objectsUriComponentsBuilder, Lombok @Builder
ObserverBehavioralNotify dependents when state changes@EventListener, ApplicationEvent
StrategyBehavioralSwap algorithms at runtimeInjecting different @Service implementations
AdapterStructuralBridge incompatible interfacesHandlerAdapter, JpaRepository wrapping Hibernate
DecoratorStructuralAdd behavior dynamically by wrappingFilter chains, HttpServletRequestWrapper
ProxyStructuralControl access with a surrogate@Transactional, @Cacheable, AOP proxies
Template MethodBehavioralDefine algorithm skeleton, defer stepsJdbcTemplate, RestTemplate

🎯 Frequently Asked Interview Questions

Q1: How to implement a thread-safe Bill Pugh Singleton pattern in Java?
Answer: Use a static inner helper class containing the singleton instance. The inner class is loaded into memory only when getInstance() is invoked, guaranteeing thread safety without synchronization overhead.
Q2: How does Spring implement the Proxy Pattern?
Answer: Spring AOP uses JDK Dynamic Proxies for interfaces and CGLIB byte-code enhancement proxies for concrete classes to inject cross-cutting concerns like @Transactional and @Cacheable.