Design patterns represent the practical application of design principles—battle-tested solutions for recurring problems in software development. Mastering these patterns enables you to tackle common development challenges with proven approaches, resulting in cleaner and more maintainable code.
This guide focuses on the 10 most frequent used patterns among the classic 23 design patterns, categorized into three main types: creational, structural, and behavioral. Each pattern includes its core principles, applicable scenarios, and production-ready Java implementations.
Pattern Overview: Classification and Objectives
Design patterns are classified by the type of problem they solve, with a shared goal of achieving decoupling, reusability, and extensibility:
| Category | Core Objective | Common Patterns |
|---|---|---|
| Creational | Encapsulate object creation logic | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| Structural | Optimize class/object composition | Proxy, Decorator, Adapter, Bridge, Composite, Flyweight |
| Behavioral | Optimize object interaction/responsibility | Observer, Strategy, Template Method, Iterator, Command |
Deep Dive: High-Frequency Design Patterns
Creational Patterns: Mastering Object Creation
Singleton Pattern
Principle: Ensure a class has only one instance in the entire application, with a global access point.
Use Cases: Configuration managers, connection pools, logging objects—anywhere you need to prevent redundant resource creation.
Core Implementation: Private constructor combined with a static method or variable controlling instance creation.
Java Implementation (Thread-Safe Lazy Initialization):
/**
* Lazy Singleton: Creates instance on demand with thread-safe locking
* Pros: Memory efficient, thread-safe; Cons: Slight performance overhead from synchronization
*/
public final class ConfigurationManager {
// volatile ensures visibility across threads and prevents instruction reordering
private static volatile ConfigurationManager instance;
// Private constructor prevents external instantiation
private ConfigurationManager() {
// Add initialization logic here
}
// Double-checked locking reduces lock contention for better performance
public static ConfigurationManager getInstance() {
if (instance == null) { // First check: avoids synchronization on every call
synchronized (ConfigurationManager.class) {
if (instance == null) { // Second check: prevents race condition
instance = new ConfigurationManager();
}
}
}
return instance;
}
public String getSetting(String key) {
return "Setting value for: " + key;
}
}
// Usage Example
class SingletonDemo {
public static void main(String[] args) {
ConfigurationManager config1 = ConfigurationManager.getInstance();
ConfigurationManager config2 = ConfigurationManager.getInstance();
System.out.println(config1 == config2); // true - same instance
config1.getSetting("database.url");
}
}
Factory Method Pattern
Principle: Define an interface for creating objects, letting subclasses decide which class to instantiate. This defers object creation to subclasses.
Use Cases: When you have multiple product types and need to extend with new types—the pattern adheres to the Open/Closed Principle.
Core Roles: Abstract Factory (creation interface), Concrete Factory (implementation), Abstract Product, Concrete Product.
Java Implementation (Payment Gateway):
// 1. Abstract Product: Payment interface
public interface PaymentGateway {
void processPayment(double amount);
}
// 2. Concrete Product: Credit Card payment
public class CreditCardPayment implements PaymentGateway {
@Override
public void processPayment(double amount) {
System.out.println("Credit card payment: $" + amount);
}
}
// 3. Concrete Product: PayPal payment
public class PayPalPayment implements PaymentGateway {
@Override
public void processPayment(double amount) {
System.out.println("PayPal payment: $" + amount);
}
}
// 4. Abstract Factory: Payment factory interface
public interface PaymentFactory {
PaymentGateway createPayment();
}
// 5. Concrete Factory: Credit Card payment factory
public class CreditCardFactory implements PaymentFactory {
@Override
public PaymentGateway createPayment() {
return new CreditCardPayment();
}
}
// 6. Concrete Factory: PayPal payment factory
public class PayPalFactory implements PaymentFactory {
@Override
public PaymentGateway createPayment() {
return new PayPalPayment();
}
}
// Usage Example
class FactoryMethodDemo {
public static void main(String[] args) {
// Create credit card payment
PaymentFactory cardFactory = new CreditCardFactory();
PaymentGateway cardPayment = cardFactory.createPayment();
cardPayment.processPayment(150.00);
// Create PayPal payment - adding new types requires no code changes
PaymentFactory paypalFactory = new PayPalFactory();
PaymentGateway paypalPayment = paypalFactory.createPayment();
paypalPayment.processPayment(200.00);
}
}
Builder Pattern
Principle: Separate the construction of complex objects from their representation, allowing step-by-step object building with reusable construction logic.
Use Cases: Objects with many properties and optional parameters—orders, user profiles, vehicle configurations.
Java Implementation (Complex Object Construction):
/**
* Complex object with required and optional properties
*/
public class UserProfile {
// Required properties (immutable)
private final String username;
private final String password;
// Optional properties with defaults
private final String phoneNumber;
private final String emailAddress;
private final int age;
// Private constructor - only accessible via Builder
private UserProfile(Builder builder) {
this.username = builder.username;
this.password = builder.password;
this.phoneNumber = builder.phoneNumber;
this.emailAddress = builder.emailAddress;
this.age = builder.age;
}
// Static nested Builder class
public static class Builder {
// Required properties (no defaults)
private final String username;
private final String password;
// Optional properties with defaults
private String phoneNumber = "";
private String emailAddress = "";
private int age = 0;
// Builder constructor enforces required properties
public Builder(String username, String password) {
this.username = username;
this.password = password;
}
// Fluent setters for optional properties
public Builder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Builder emailAddress(String emailAddress) {
this.emailAddress = emailAddress;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
// Final build method
public UserProfile build() {
return new UserProfile(this);
}
}
@Override
public String toString() {
return "UserProfile{" +
"username='" + username + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
", emailAddress='" + emailAddress + '\'' +
", age=" + age +
'}';
}
}
// Usage Example
class BuilderDemo {
public static void main(String[] args) {
// Step-by-step construction: required + selected optional properties
UserProfile user = new UserProfile.Builder("john_doe", "secure123")
.phoneNumber("+1-555-0123")
.age(30)
.build();
System.out.println(user);
// Output: UserProfile{username='john_doe', phoneNumber='+1-555-0123', emailAddress='', age=30}
}
}
Structural Patterns: Optimizing Object Composition
Proxy Pattern
Principle: Provide a surrogate or placeholder object that controls access to the original target object, enabling additional logic before or after access.
Use Cases: Access control, logging, caching, remote calls (RPC), lazy initialization.
Classification: Static Proxy (determined at compile time) vs Dynamic Proxy (generated at runtime).
Java Implementation (Logging Proxy):
// 1. Subject interface: Business contract
public interface DocumentService {
void generateDocument(String documentId);
}
// 2. Real Subject: Actual business implementation
public class DocumentServiceImpl implements DocumentService {
@Override
public void generateDocument(String documentId) {
System.out.println("Generating document: " + documentId);
}
}
// 3. Proxy: Adds enhanced behavior to target object
public class DocumentServiceProxy implements DocumentService {
private final DocumentService targetService;
public DocumentServiceProxy(DocumentService targetService) {
this.targetService = targetService;
}
@Override
public void generateDocument(String documentId) {
// Pre-processing: Logging
System.out.println("[TRACE] Document generation started, ID: " + documentId);
// Delegate to target object
targetService.generateDocument(documentId);
// Post-processing: Logging
System.out.println("[TRACE] Document generation completed, ID: " + documentId);
}
}
// Usage Example
class ProxyDemo {
public static void main(String[] args) {
// Target object (real implementation)
DocumentService realService = new DocumentServiceImpl();
// Proxy object (with enhanced behavior)
DocumentService proxyService = new DocumentServiceProxy(realService);
// Calls through proxy automatically include logging
proxyService.generateDocument("DOC-2024-001");
}
}
Decorator Pattern
Principle: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to inheritance for extending functionality.
Use Cases: Adding functionality to objects at runtime, especially when functionality combinations vary (IO streams, coffee beverages, UI components).
Key Distinction: Decorators focus on enhancing object functionality, while proxies focus on controlling access.
Java Implementation (Beverage Customization):
// 1. Component interface: Beverage contract
public interface Beverage {
double calculatePrice();
String getDescription();
}
// 2. Concrete Component: Base beverage (Espresso)
public class Espresso implements Beverage {
@Override
public double calculatePrice() {
return 15.00; // Base price
}
@Override
public String getDescription() {
return "Espresso";
}
}
// 3. Abstract Decorator: Base for add-ons
public abstract class BeverageDecorator implements Beverage {
protected Beverage wrappedBeverage;
public BeverageDecorator(Beverage beverage) {
this.wrappedBeverage = beverage;
}
}
// 4. Concrete Decorator: Adds Milk
public class MilkDecorator extends BeverageDecorator {
public MilkDecorator(Beverage beverage) {
super(beverage);
}
@Override
public double calculatePrice() {
return wrappedBeverage.calculatePrice() + 2.50; // Milk adds $2.50
}
@Override
public String getDescription() {
return wrappedBeverage.getDescription() + " + Milk";
}
}
// 5. Concrete Decorator: Adds Caramel
public class CaramelDecorator extends BeverageDecorator {
public CaramelDecorator(Beverage beverage) {
super(beverage);
}
@Override
public double calculatePrice() {
return wrappedBeverage.calculatePrice() + 1.50; // Caramel adds $1.50
}
@Override
public String getDescription() {
return wrappedBeverage.getDescription() + " + Caramel";
}
}
// Usage Example
class DecoratorDemo {
public static void main(String[] args) {
// Base beverage
Beverage coffee = new Espresso();
System.out.println(coffee.getDescription() + ": $" + coffee.calculatePrice());
// Add milk
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + ": $" + coffee.calculatePrice());
// Add caramel (stack decorators for combined effects)
coffee = new CaramelDecorator(coffee);
System.out.println(coffee.getDescription() + ": $" + coffee.calculatePrice());
}
}
Adapter Pattern
Principle: Convert the interface of a class into what clients expect. This pattern enables incompatible interfaces to work together.
Use Cases: Integrating third-party components, reusing existing classes with incompatible interfaces (legacy system integration).
Core Roles: Target Interface (what clients expect), Adaptee (existing interface), Adapter (interface converter).
Java Implementation (Legacy System Integration):
// 1. Target Interface: Modern system contract
public interface ModernPaymentProcessor {
void executePayment(double amount);
}
// 2. Adaptee: Legacy payment system (cannot be modified)
public class LegacyPaymentGateway {
// Legacy interface has different method signature
public void chargeCustomer(double amountInCents) {
System.out.println("Legacy gateway charging: " + (amountInCents / 100) + " dollars");
}
}
// 3. Adapter: Bridges legacy and modern interfaces
public class PaymentAdapter implements ModernPaymentProcessor {
private final LegacyPaymentGateway legacyGateway;
public PaymentAdapter(LegacyPaymentGateway legacyGateway) {
this.legacyGateway = legacyGateway;
}
@Override
public void executePayment(double amount) {
// Convert dollars to cents for legacy system
double amountInCents = amount * 100;
legacyGateway.chargeCustomer(amountInCents);
}
}
// Usage Example
class AdapterDemo {
public static void main(String[] args) {
// Legacy system object
LegacyPaymentGateway legacySystem = new LegacyPaymentGateway();
// Adapter makes legacy system compatible with modern interface
ModernPaymentProcessor processor = new PaymentAdapter(legacySystem);
// Client code uses modern interface
processor.executePayment(99.99);
}
}
Behavioral Patterns: Optimizing Object Interaction
Observer Pattern
Principle: Define a one-to-many dependency between objects. When one object (Subject) changes state, all dependent objects (Observers) are automatically notified and updated.
Use Cases: Event notification systems, message broadcasting, pub/sub architectures (order status updates triggering warehouse and shipping actions).
Java Implementation (Event Notification System):
import java.util.ArrayList;
import java.util.List;
// 1. Subject: Observable entity
interface OrderObservable {
void addObserver(OrderListener observer);
void removeObserver(OrderListener observer);
void notifyAllListeners(String orderId, String status);
}
// 2. Concrete Subject: Observable order service
class OrderManager implements OrderObservable {
private final List<OrderListener> listeners = new ArrayList<>();
@Override
public void addObserver(OrderListener observer) {
listeners.add(observer);
}
@Override
public void removeObserver(OrderListener observer) {
listeners.remove(observer);
}
@Override
public void notifyAllListeners(String orderId, String status) {
for (OrderListener listener : listeners) {
listener.onOrderStatusChange(orderId, status);
}
}
// Method that triggers state changes
public void updateOrderStatus(String orderId, String newStatus) {
System.out.println("Order " + orderId + " status changed to: " + newStatus);
notifyAllListeners(orderId, newStatus);
}
}
// 3. Observer interface: Contract for listeners
interface OrderListener {
void onOrderStatusChange(String orderId, String status);
}
// 4. Concrete Observer: Warehouse notification
class WarehouseNotification implements OrderListener {
@Override
public void onOrderStatusChange(String orderId, String status) {
if ("PAID".equals(status)) {
System.out.println("WAREHOUSE: Order " + orderId + " paid - preparing shipment");
}
}
}
// 5. Concrete Observer: Email notification
class EmailNotification implements OrderListener {
@Override
public void onOrderStatusChange(String orderId, String status) {
if ("PAID".equals(status)) {
System.out.println("EMAIL: Sending confirmation for order " + orderId);
}
}
}
// Usage Example
class ObserverDemo {
public static void main(String[] args) {
// Create observable subject
OrderManager orderManager = new OrderManager();
// Register observers
orderManager.addObserver(new WarehouseNotification());
orderManager.addObserver(new EmailNotification());
// Trigger state change - all observers are notified automatically
orderManager.updateOrderStatus("ORD-12345", "PAID");
}
}
Strategy Pattern
Principle: Define a family of algorithms, encapsulate each one, and make them interchangeable. The algorithm can vary independently from clients that use it.
Use Cases: Multiple algorithms/rules that may switch at runtime—sorting strategies, payment methods, discount calculations, routing algorithms.
Java Implementation (Dynamic Pricing Engine):
// 1. Strategy interface: Pricing algorithm contract
interface PricingStrategy {
double calculateFinalPrice(double originalPrice);
}
// 2. Concrete Strategy: Percentage discount
class PercentageDiscount implements PricingStrategy {
private final double discountRate; // e.g., 0.2 for 20% off
public PercentageDiscount(double discountRate) {
this.discountRate = discountRate;
}
@Override
public double calculateFinalPrice(double originalPrice) {
return originalPrice * (1 - discountRate);
}
}
// 3. Concrete Strategy: Fixed amount off
class FixedAmountOff implements PricingStrategy {
private final double discountAmount;
public FixedAmountOff(double discountAmount) {
this.discountAmount = discountAmount;
}
@Override
public double calculateFinalPrice(double originalPrice) {
return Math.max(0, originalPrice - discountAmount);
}
}
// 4. Context: Shopping cart that uses strategies
class ShoppingCart {
private PricingStrategy selectedStrategy;
public void setPricingStrategy(PricingStrategy strategy) {
this.selectedStrategy = strategy;
}
public double checkout(double cartTotal) {
if (selectedStrategy == null) {
return cartTotal;
}
return selectedStrategy.calculateFinalPrice(cartTotal);
}
}
// Usage Example
class StrategyDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
double originalPrice = 200.00;
// Apply percentage discount
cart.setPricingStrategy(new PercentageDiscount(0.15)); // 15% off
System.out.println("After 15% discount: $" + cart.checkout(originalPrice)); // $170.00
// Switch to fixed amount off (no need to modify ShoppingCart)
cart.setPricingStrategy(new FixedAmountOff(25.00));
System.out.println("After $25 off: $" + cart.checkout(originalPrice)); // $175.00
}
}
Template Method Pattern
Principle: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. This allows subclasses to redefine certain steps without changing the algorithm's structure.
Use Cases: When multiple subclasses share common workflow but differ in specific steps—authentication flows, data processing pipelines, report generation.
Java Implementation (Authentication Workflow):
// 1. Abstract Class: Defines the authentication algorithm skeleton
public abstract class AuthenticationFlow {
// Template method: Fixed authentication process (final prevents override)
public final void authenticate(String username, String credentials) {
if (!validateInputs(username, credentials)) {
System.out.println("Invalid input format");
return;
}
if (verifyCredentials(username, credentials)) {
handleSuccessfulLogin();
} else {
handleFailedLogin();
}
}
// Fixed step: Input validation
private boolean validateInputs(String username, String credentials) {
return username != null && !username.isBlank()
&& credentials != null && !credentials.isBlank();
}
// Variable step: Credential verification (abstract - implemented by subclasses)
protected abstract boolean verifyCredentials(String username, String credentials);
// Fixed steps: Success and failure handlers
private void handleSuccessfulLogin() {
System.out.println("Authentication successful - redirecting to dashboard");
}
private void handleFailedLogin() {
System.out.println("Invalid credentials - please try again");
}
}
// 2. Concrete Class: Username/Password authentication
class UsernamePasswordAuth extends AuthenticationFlow {
@Override
protected boolean verifyCredentials(String username, String credentials) {
// Database lookup simulation
return "admin".equals(username) && "password123".equals(credentials);
}
}
// 3. Concrete Class: OAuth authentication
class OAuthAuthentication extends AuthenticationFlow {
@Override
protected boolean verifyCredentials(String username, String credentials) {
// OAuth token validation simulation
return "admin".equals(username) && "oauth_token_valid".equals(credentials);
}
}
// Usage Example
class TemplateMethodDemo {
public static void main(String[] args) {
// Username/password authentication
AuthenticationFlow auth = new UsernamePasswordAuth();
auth.authenticate("admin", "password123"); // Success
// OAuth authentication
AuthenticationFlow oauth = new OAuthAuthentication();
oauth.authenticate("admin", "oauth_token_valid"); // Success
}
}
Practical Guidelines and Common Pitfalls
Core Usage Principles
-
Choose Based on Need: Don't force patterns where simple code suffices. A factory with only one product adds unnecessary complexity.
-
Align with Design Principles: All patterns embody SOLID principles. The Strategy pattern adheres to Open/Closed Principle; the Singleton pattern supports Single Responsibility.
-
Prefer Composition: Most design patterns rely on composition/aggregation rather than inheritance (Decorator, Observer).
Common Pitfalls to Avoid
-
Overengineering: Don't apply patterns when simpler solutions work. Abstract factories in small projects create unnecessary complexity.
-
Pattern Confusion:
- Proxy vs Decorator: Proxy controls access; Decorator enhances functionality.
- Adapter vs Decorator: Adapter fixes incompatible interfaces; Decorator adds features.
-
Performance Considerations: Lazy-loading singletons with synchronization and multi-layer decorator chains can impact performance under high load.