🧩 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:
| Category | Purpose | Patterns Covered |
|---|---|---|
| Creational | How objects are created | Singleton, Factory Method, Builder |
| Structural | How objects are composed | Adapter, Decorator, Proxy |
| Behavioral | How objects communicate | Observer, Strategy, Template Method |
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.
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); } }
DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance(); Connection conn = pool.getConnection(); // ... use connection ... pool.releaseConnection(conn);
@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.
// 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);
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
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.
// 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");
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
@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
// 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));
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.
// 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);
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)
// 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());
Espresso, Milk, Caramel → ₹230.0
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
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!
🌐 Calling external weather API for Mumbai... Sunny, 32°C in Mumbai ⚡ Cache HIT for Mumbai Sunny, 32°C in Mumbai
@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.
// 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();
📊 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!
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
| Pattern | Category | One-Line Summary | Spring Boot Usage |
|---|---|---|---|
| Singleton | Creational | One instance for the entire application | @Bean (default scope) |
| Factory | Creational | Create objects without exposing creation logic | BeanFactory, FactoryBean |
| Builder | Creational | Step-by-step construction of complex objects | UriComponentsBuilder, Lombok @Builder |
| Observer | Behavioral | Notify dependents when state changes | @EventListener, ApplicationEvent |
| Strategy | Behavioral | Swap algorithms at runtime | Injecting different @Service implementations |
| Adapter | Structural | Bridge incompatible interfaces | HandlerAdapter, JpaRepository wrapping Hibernate |
| Decorator | Structural | Add behavior dynamically by wrapping | Filter chains, HttpServletRequestWrapper |
| Proxy | Structural | Control access with a surrogate | @Transactional, @Cacheable, AOP proxies |
| Template Method | Behavioral | Define algorithm skeleton, defer steps | JdbcTemplate, RestTemplate |
🎯 Frequently Asked Interview Questions
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.
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.