Design Patterns in Java: Complete Beginner to Advanced Guide (GoF Catalog 2026)
1. What Are Design Patterns & Why They Matter
Design patterns, popularized by the Gang of Four (GoF) book published in 1994, are proven, reusable solutions to commonly occurring problems in software design. They are not code you copy-paste; they are templates describing how to structure code to solve a specific recurring problem elegantly.
The real power of design patterns lies in shared vocabulary. Saying "let's use the Strategy pattern here" communicates a whole design intent to your team instantly — far more efficiently than describing "we'll have an interface with multiple implementations selected at runtime."
- Proven solutions: Patterns encode decades of collective engineering wisdom.
- Shared vocabulary: Teams communicate design decisions faster and more precisely.
- Reduced cognitive load: Recognizing a pattern immediately frames the problem.
- Testability & flexibility: Patterns typically favor loose coupling and dependency inversion.
The 23 GoF patterns are organized into three categories: Creational (how objects are created), Structural (how objects are composed into larger structures), and Behavioral (how objects communicate and distribute responsibility).
2. The GoF Pattern Catalog: A Mental Map
All 23 GoF patterns organized by category. Learn these groupings — they provide a mental framework for navigating the catalog when you encounter a new design problem.
| Category | Patterns | Key Question |
|---|---|---|
| Creational (5) | Factory Method, Abstract Factory, Builder, Prototype, Singleton | How should objects be created? |
| Structural (7) | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy | How should objects be composed? |
| Behavioral (11) | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor | How should objects communicate? |
3. Creational Patterns: How Objects Are Born
Creational patterns abstract the object creation process, making systems independent of how their objects are created, composed, and represented. They are critical for writing testable, maintainable code.
Factory Method
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate. In Spring Boot, @Bean methods in @Configuration classes are factory methods.
// BAD: new Service() scattered everywhere — tight coupling to concrete type
public class OrderController {
private PaymentService paymentService = new StripePaymentService(); // hard-coded
private NotificationService notifier = new EmailNotificationService(); // hard-coded
}
// GOOD: Factory Method decouples creation from usage
public interface PaymentService {
PaymentResult processPayment(PaymentRequest request);
}
@Component("stripe")
public class StripePaymentService implements PaymentService { ... }
@Component("paypal")
public class PaypalPaymentService implements PaymentService { ... }
// Spring acts as the factory
@Service
@RequiredArgsConstructor
public class OrderService {
private final Map<String, PaymentService> paymentServices;
public PaymentResult pay(String provider, PaymentRequest req) {
return paymentServices.getOrDefault(provider,
paymentServices.get("stripe")).processPayment(req);
}
}Builder Pattern
Builder separates the construction of a complex object from its representation. It directly solves the telescoping constructor problem — when a class needs many optional configuration parameters.
// BAD: Telescoping constructor — which boolean is which?
public Order(String id, String customer, BigDecimal amount,
boolean express, boolean gift, String coupon) { ... }
// Usage is unreadable:
Order o = new Order("O-1", "alice", new BigDecimal("99.99"), true, false, null);
// GOOD: Fluent Builder using Lombok @Builder
@Builder
@Value
public class Order {
String id;
String customerId;
BigDecimal amount;
boolean expressShipping;
boolean giftWrapped;
@Nullable String couponCode;
}
// Usage is self-documenting:
Order order = Order.builder()
.id("O-1")
.customerId("alice")
.amount(new BigDecimal("99.99"))
.expressShipping(true)
.build();Singleton Pattern
Singleton ensures a class has only one instance and provides a global access point. In Spring, all @Bean definitions are singletons by default (scope "singleton"). The Bill Pugh initialization-on-demand holder idiom provides thread-safe lazy initialization without synchronization overhead.
// BAD: Double-checked locking with volatile — error-prone
public class ConfigManager {
private static volatile ConfigManager instance;
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) instance = new ConfigManager();
}
}
return instance;
}
}
// GOOD: Bill Pugh Singleton (thread-safe, lazy, no synchronization cost)
public class ConfigManager {
private ConfigManager() {}
private static class Holder {
static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() { return Holder.INSTANCE; }
}
// BEST for enums: inherently thread-safe, serialization-safe
public enum AppConfig {
INSTANCE;
private final String apiUrl = System.getenv("API_URL");
public String getApiUrl() { return apiUrl; }
}Prototype & Abstract Factory (Brief)
Prototype clones existing objects instead of creating new ones — useful when instantiation is expensive. Implement Cloneable or a copy constructor. Abstract Factory creates families of related objects. Classic example: UI toolkit factory that produces consistent Button, Checkbox, and Dialog for a given OS theme.
// Abstract Factory: vehicle family
public interface VehicleFactory {
Engine createEngine();
Transmission createTransmission();
}
@Component
public class ElectricVehicleFactory implements VehicleFactory {
public Engine createEngine() { return new ElectricMotor(); }
public Transmission createTransmission() { return new SingleSpeedTransmission(); }
}
@Component
public class PetrolVehicleFactory implements VehicleFactory {
public Engine createEngine() { return new CombustionEngine(); }
public Transmission createTransmission() { return new AutomaticTransmission(); }
}4. Structural Patterns: How Objects Are Composed
Structural patterns deal with object composition, creating relationships between objects to form larger structures. They help ensure that if one part of a system changes, the entire system doesn't need to change.
Adapter Pattern
Adapter converts the interface of a class into another interface that clients expect. It's the go-to pattern for integrating legacy systems or third-party libraries without modifying their source.
// BAD: Direct call to legacy payment SDK — couples your domain to a vendor
@Service
public class CheckoutService {
public void checkout(Cart cart) {
LegacyPaymentSDK sdk = new LegacyPaymentSDK("key123");
sdk.makePaymentRequest(cart.total(), cart.currency(), cart.customerId());
}
}
// GOOD: Adapter wraps legacy system behind your domain interface
public interface PaymentGateway {
PaymentResult charge(Money amount, String customerId);
}
@Component
public class LegacyPaymentAdapter implements PaymentGateway {
private final LegacyPaymentSDK legacySdk;
public LegacyPaymentAdapter(@Value("${legacy.payment.key}") String apiKey) {
this.legacySdk = new LegacyPaymentSDK(apiKey);
}
@Override
public PaymentResult charge(Money amount, String customerId) {
boolean success = legacySdk.makePaymentRequest(
amount.amount(), amount.currency(), customerId);
return success ? PaymentResult.success() : PaymentResult.failed("Gateway declined");
}
}Decorator Pattern
Decorator attaches additional responsibilities to objects dynamically. It provides a flexible alternative to subclassing for extending functionality. The classic anti-pattern it solves is subclass explosion.
// BAD: Subclass explosion — 2^n combinations
class EmailNotifier { void notify(String msg) { ... } }
class SMSNotifier extends EmailNotifier { ... }
class SlackNotifier extends EmailNotifier { ... }
class EmailAndSMSNotifier extends SMSNotifier { ... }
class EmailAndSlackNotifier extends SlackNotifier { ... }
// Adding one more channel doubles the class count!
// GOOD: Decorator chain
public interface Notifier {
void notify(String message);
}
@Component("baseNotifier")
public class LoggingNotifier implements Notifier {
public void notify(String message) {
log.info("Notification: {}", message);
}
}
public abstract class NotifierDecorator implements Notifier {
protected final Notifier delegate;
public NotifierDecorator(Notifier delegate) { this.delegate = delegate; }
}
@Component
public class EmailDecorator extends NotifierDecorator {
private final EmailClient emailClient;
public EmailDecorator(Notifier delegate, EmailClient emailClient) {
super(delegate); this.emailClient = emailClient;
}
public void notify(String message) {
emailClient.send(message);
delegate.notify(message); // chain
}
}
// Usage: compose at runtime
Notifier notifier = new SlackDecorator(new EmailDecorator(baseNotifier, emailClient), slackClient);Proxy Pattern
Proxy provides a surrogate or placeholder for another object to control access. Spring AOP uses JDK dynamic proxies (for interface-based) or CGLIB proxies (for class-based) to implement @Transactional, @Cacheable, @Async, and more.
// Spring creates a proxy transparently for @Transactional
@Service
public class OrderService {
@Transactional // Spring wraps this method in a proxy that manages TX lifecycle
public Order createOrder(CreateOrderCommand cmd) {
Order order = orderRepository.save(Order.from(cmd));
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
return order;
}
}
// Virtual Proxy: lazy loading
public class LazyUserProfileProxy implements UserProfile {
private final String userId;
private UserProfile realProfile; // loaded on first access
public String getFullName() {
if (realProfile == null) realProfile = loadFromDatabase(userId);
return realProfile.getFullName();
}
}Facade & Composite (Brief)
Facade provides a simplified interface to a complex subsystem. Spring's @Service layer is a perfect facade: it hides repositories, validators, external API clients, and caches behind a simple domain-oriented API. Composite lets clients treat individual objects and compositions uniformly — ideal for tree structures like menus, file systems, or expression trees.
// Facade: OrderFacade hides internal complexity
@Service
@RequiredArgsConstructor
public class OrderFacade {
private final InventoryService inventory;
private final PricingService pricing;
private final PaymentGateway payment;
private final OrderRepository orderRepository;
private final NotificationService notification;
public OrderConfirmation placeOrder(PlaceOrderRequest req) {
inventory.reserve(req.items());
Money total = pricing.calculate(req.items(), req.coupon());
PaymentResult paymentResult = payment.charge(total, req.customerId());
Order order = orderRepository.save(Order.from(req, total, paymentResult));
notification.sendConfirmation(order);
return OrderConfirmation.from(order);
}
}5. Behavioral Patterns: How Objects Communicate
Behavioral patterns focus on communication between objects — who calls what, how data flows, and how responsibilities are distributed. These are the patterns you'll use most frequently in business logic code.
Strategy Pattern — The King of Behavioral Patterns
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. This is the most-used pattern in Spring Boot applications — any time you have a switch on type to select behavior, Strategy is the answer.
// BAD: Switch on type — every new payment method requires modifying this class (OCP violation)
public BigDecimal calculateShipping(String method, BigDecimal weight) {
return switch (method) {
case "standard" -> weight.multiply(new BigDecimal("0.5"));
case "express" -> weight.multiply(new BigDecimal("2.0"));
case "overnight"-> weight.multiply(new BigDecimal("5.0"));
default -> throw new IllegalArgumentException("Unknown method");
};
}
// GOOD: Strategy interface + Spring @Component implementations
public interface ShippingStrategy {
String method();
BigDecimal calculate(BigDecimal weight);
}
@Component("standard")
public class StandardShipping implements ShippingStrategy {
public String method() { return "standard"; }
public BigDecimal calculate(BigDecimal weight) {
return weight.multiply(new BigDecimal("0.5"));
}
}
@Component("express")
public class ExpressShipping implements ShippingStrategy {
public String method() { return "express"; }
public BigDecimal calculate(BigDecimal weight) {
return weight.multiply(new BigDecimal("2.0"));
}
}
@Service
public class ShippingCalculator {
private final Map<String, ShippingStrategy> strategies;
public ShippingCalculator(List<ShippingStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(ShippingStrategy::method, s -> s));
}
public BigDecimal calculate(String method, BigDecimal weight) {
return Optional.ofNullable(strategies.get(method))
.orElseThrow(() -> new IllegalArgumentException("Unknown method: " + method))
.calculate(weight);
}
}Observer Pattern
Observer defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified. Spring Boot provides first-class support via ApplicationEvent and @EventListener.
// BAD: Direct method calls — OrderService knows about every downstream concern
@Service
public class OrderService {
public void createOrder(Order order) {
orderRepository.save(order);
emailService.sendConfirmation(order); // tight coupling
inventoryService.updateStock(order); // tight coupling
analyticsService.trackOrderCreated(order); // tight coupling
loyaltyService.awardPoints(order); // tight coupling
}
}
// GOOD: Event-driven with Spring ApplicationEvent
public record OrderCreatedEvent(String orderId, String customerId, BigDecimal total) {}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public Order createOrder(CreateOrderCommand cmd) {
Order order = orderRepository.save(Order.from(cmd));
eventPublisher.publishEvent(new OrderCreatedEvent(
order.getId(), order.getCustomerId(), order.getTotal()));
return order;
}
}
@Component
public class OrderEventHandlers {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendConfirmation(event.customerId());
}
@EventListener
@Async
public void updateAnalytics(OrderCreatedEvent event) {
analyticsService.trackOrder(event.orderId(), event.total());
}
}Template Method Pattern
// BAD: Duplicated algorithm skeleton in two classes
class CsvReportGenerator {
void generate() {
fetchData(); formatAsCsv(); writeToFile(); sendEmail(); // duplicated flow
}
}
class PdfReportGenerator {
void generate() {
fetchData(); formatAsPdf(); writeToFile(); sendEmail(); // duplicated flow
}
}
// GOOD: Template Method in abstract base class
public abstract class ReportGenerator {
public final void generate() { // final: algorithm skeleton is fixed
List<?> data = fetchData();
byte[] formatted = format(data);
writeToFile(formatted);
sendNotification();
}
protected abstract List<?> fetchData();
protected abstract byte[] format(List<?> data);
private void writeToFile(byte[] content) { /* common impl */ }
private void sendNotification() { /* common impl */ }
}
@Component
public class CsvReportGenerator extends ReportGenerator {
protected List<?> fetchData() { return reportRepository.findAll(); }
protected byte[] format(List<?> data) { return csvSerializer.serialize(data); }
}Command, Chain of Responsibility & State (Brief)
- Command: Encapsulates a request as an object, enabling undo/redo, queuing, and logging of operations. Each command has an
execute()andundo()method. - Chain of Responsibility: Passes a request along a chain of handlers. Spring Security filter chain is this pattern in production — each filter processes the request or passes it to the next.
- State: Allows an object to alter its behavior when its internal state changes. Ideal for order state machines (PENDING → CONFIRMED → SHIPPED → DELIVERED) where transitions have validation rules.
// State Pattern: Order state machine
public interface OrderState {
void confirm(OrderContext ctx);
void ship(OrderContext ctx);
void deliver(OrderContext ctx);
void cancel(OrderContext ctx);
}
public class PendingState implements OrderState {
public void confirm(OrderContext ctx) { ctx.setState(new ConfirmedState()); }
public void ship(OrderContext ctx) { throw new IllegalStateException("Cannot ship a pending order"); }
public void deliver(OrderContext ctx) { throw new IllegalStateException("Cannot deliver a pending order"); }
public void cancel(OrderContext ctx) { ctx.setState(new CancelledState()); }
}6. Pattern Anti-Patterns: When Patterns Hurt
- Singleton Abuse: Global mutable state defeats dependency injection and makes unit testing nearly impossible. If you need a Singleton, let Spring manage it via
@Bean(default scope). - Overusing Abstract Factory: When Factory Method is sufficient (one product family), Abstract Factory adds unnecessary complexity. Start with the simpler pattern.
- Decorator Over-Chaining: Ten decorators stacked on top of each other produce an unreadable call stack in debugging. Beyond 3-4 layers, consider refactoring to a pipeline processor.
- Template Method with Deep Inheritance: Deep inheritance hierarchies are brittle. If Template Method requires more than two levels of inheritance, prefer Composition + Strategy instead.
- "Pattern Fever": Forcing a pattern onto a simple problem just to demonstrate knowledge is the most common senior-engineer mistake in architecture reviews. Always ask: "What problem does this pattern solve here specifically?"
7. Design Patterns in Spring Boot
Spring Boot is itself a showcase of GoF patterns applied masterfully. Understanding which patterns Spring uses under the hood helps you extend it correctly and debug issues faster.
| Spring Feature | GoF Pattern | Example |
|---|---|---|
| BeanFactory / ApplicationContext | Factory Method | context.getBean() |
| @Configuration + @Bean | Abstract Factory | DataSourceConfig, SecurityConfig |
| RestTemplate / JdbcTemplate | Template Method | exchange(), getForObject() |
| Spring AOP | Proxy | @Transactional, @Cacheable, @Async |
| ApplicationEvent | Observer | @EventListener |
| HandlerMapping | Strategy | RequestMappingHandlerMapping |
| Filter Chain | Chain of Responsibility | Spring Security filters |
| Spring Data | Template Method | JpaRepository.save(), findAll() |
8. Choosing the Right Pattern: Decision Guide
| Problem | Pattern | Why |
|---|---|---|
| Creating objects without specifying exact class | Factory Method | Decouples creation from use |
| Building complex objects step by step | Builder | Prevents telescoping constructors |
| Adding behavior without subclassing | Decorator | Avoids class explosion |
| Multiple algorithms for same task | Strategy | Runtime algorithm selection |
| Notifying multiple objects of change | Observer | Loose coupling between subject and observers |
| One instance only, shared state | Singleton | Controlled shared state |
| Adapting incompatible interface | Adapter | Legacy system integration |
| Simplifying complex subsystem API | Facade | Improved usability for clients |
9. Design Patterns vs Architecture Patterns
Design patterns operate at the class and object level — they describe how to structure code within a module. Architecture patterns operate at the system level — they describe how to structure entire systems and the communication between services.
| Scope | Pattern Type | Examples |
|---|---|---|
| Class/Object level | Design Patterns (GoF) | Strategy, Factory, Observer, Decorator |
| Application structure | Architectural Patterns | Hexagonal Architecture, Clean Architecture, Layered Architecture |
| Data management | Distributed Patterns | CQRS, Event Sourcing, Saga, Outbox |
| System resilience | Microservices Patterns | Circuit Breaker, Bulkhead, Sidecar, Strangler Fig |
10. Interview Insights
Q: What's the difference between Factory Method and Abstract Factory?
Factory Method is a single method that creates one type of product. Subclasses override it to change the created type. Abstract Factory is an interface with multiple factory methods creating a family of related products. Use Factory Method when you need one product; Abstract Factory when you need consistent families (e.g., all UI components for a given theme, all cloud resources for a given provider).
Q: Why is Singleton considered an anti-pattern by many engineers?
Hand-rolled Singletons introduce three problems: (1) They create hidden global mutable state, making code unpredictable; (2) They are hard to swap in tests — you can't inject a mock; (3) They carry implicit dependencies not expressed in method signatures. In Spring Boot, let the IoC container manage Singleton scope via @Bean. The DI container gives you all singleton benefits without the drawbacks.
Q: Explain Strategy vs State pattern — they look very similar.
Strategy selects an algorithm externally — the client chooses which strategy to use, and the object doesn't change its strategy on its own. State changes behavior internally based on the object's current state — the state transitions itself to the next state. Analogy: Strategy is a payment method you choose; State is an order progressing through PENDING → CONFIRMED → SHIPPED automatically based on business events.
Q: How does Spring Boot use the Proxy pattern?
Spring AOP uses the Proxy pattern to wrap beans with cross-cutting concerns transparently. When you annotate a method with @Transactional, Spring generates a proxy (JDK Proxy for interfaces, CGLIB for classes) that intercepts the call, opens a transaction, executes your method, then commits or rolls back. The same mechanism powers @Cacheable, @Async, @Retryable, and @CircuitBreaker. This is why you can't call a @Transactional method from within the same class — self-calls bypass the proxy.
11. FAQ & Key Takeaways
FAQ
Q: Do I need to memorize all 23 GoF patterns?
No. Master the 5 most-used deeply (Strategy, Factory Method, Builder, Observer, Decorator). Know the others well enough to recognize when they apply. You can always look up implementation details.
Q: Is it bad to mix multiple patterns?
No — combining patterns is common and often powerful. Spring's RestTemplate combines Template Method + Strategy. The key is that the combination should reduce, not increase, complexity.
Q: When should I refactor to a pattern vs leaving simple code?
Refactor to a pattern when you see the same kind of change request multiple times (e.g., "add another notification channel"). That repetitive requirement is the signal that a pattern will pay dividends.
Q: Which patterns are most commonly asked in Java interviews?
Strategy, Singleton (and why to avoid it), Factory/Abstract Factory, Builder, Observer, Decorator, and Proxy (especially in the context of Spring AOP) are the most common interview topics.
Key Takeaways
- Design patterns are a shared vocabulary, not a mandatory checklist to apply to every class.
- Master Strategy, Factory Method, Builder, Observer, and Decorator deeply before the rest.
- Spring Boot is built on GoF patterns; understanding this makes you a better Spring developer.
- Pattern over-use ("Pattern Fever") is as harmful as not using patterns at all.
- Distinguish design patterns (class-level) from architecture patterns (system-level).
- Always ask: "What specific problem does this pattern solve here?" before applying it.
Leave a Comment
Related Posts
Creational Design Patterns in Java
Factory Method, Builder, Singleton & More with production Java examples.
Structural Design Patterns in Java
Adapter, Decorator, Proxy, Facade & Composite with Spring Boot examples.
Behavioral Design Patterns in Java
Strategy, Observer, Command, Template Method & more with Java code.