Master 8 Design Patterns in Spring Boot with Real-World Examples
This article walks through eight essential design patterns—Factory Method, Prototype, Adapter, Decorator, Observer, Strategy, Template Method, and Chain of Responsibility—showing concrete Spring Boot 3.5.0 code examples, explaining the problems each pattern solves, and highlighting their practical advantages for clean, extensible backend development.
Environment : Spring Boot 3.5.0
1. Introduction
Design patterns are unavoidable in Java development and appear everywhere in Spring Boot projects. Understanding only the concepts is insufficient; the real challenge is applying them to concrete business scenarios. This article presents eight common patterns with real Spring Boot cases, explaining the problems each pattern addresses, the rationale behind the design, and how to use them correctly.
2. Factory Method Pattern
The Factory Method defines an interface for creating objects, letting subclasses decide which concrete class to instantiate. Spring Boot’s BeanFactory is a typical example.
// PaymentProcessor interface
public interface PaymentProcessor {
void processPayment(double amount);
}
@Component("alipay")
public class AlipayProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) { /* ... */ }
}
@Component("weixin")
public class WeixinProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) { /* ... */ }
}
// Factory
@Component
public class PaymentProcessorFactory {
private final Map<String, PaymentProcessor> processors;
public PaymentProcessor getProcessor(String type) {
PaymentProcessor processor = processors.get(type);
if (processor == null) {
throw new IllegalArgumentException("No payment processor found for type: " + type);
}
return processor;
}
}
@Service
public class OrderService {
private final PaymentProcessorFactory processorFactory;
public void placeOrder(String paymentType, double amount) {
PaymentProcessor processor = processorFactory.getProcessor(paymentType);
processor.processPayment(amount);
// handle the rest of the order …
}
}Loose coupling: client code is separated from concrete implementations.
Open‑Closed Principle: adding new payment methods requires only a new implementation and registration.
Encapsulates object‑creation logic for unified management.
3. Prototype Pattern
Prototype creates new objects by copying an existing prototype instead of direct instantiation, useful when object creation is costly. Spring’s prototype bean scope is a concrete application.
// Prototype bean: task context
@Component
@Scope("prototype")
public class TaskContext {
private final String taskId;
private final LocalDateTime createdAt;
private final TaskValidator validator;
public TaskContext(TaskValidator validator) {
this.validator = validator;
this.taskId = UUID.randomUUID().toString();
this.createdAt = LocalDateTime.now();
}
public void validateAndExecute(String data) {
if (validator.isValid(data)) {
System.out.println("Task [" + taskId + "] executed at " + createdAt + " with data: " + data);
} else {
System.out.println("Task [" + taskId + "] validation failed.");
}
}
public String getTaskId() { return taskId; }
}
@Service
public class TaskValidator {
public boolean isValid(String data) { return data != null && !data.isEmpty(); }
}
@Service
public class TaskExecutionService {
private final ObjectFactory<TaskContext> taskContextFactory;
public void processMultipleTasks(List<String> dataList) {
for (String data : dataList) {
TaskContext context = taskContextFactory.getObject(); // new prototype each call
context.validateAndExecute(data);
}
}
}
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final TaskExecutionService taskExecutionService;
@PostMapping("/batch")
public String runBatch() {
List<String> data = Arrays.asList("data1", "data2", "data3");
taskExecutionService.processMultipleTasks(data);
return "batch processed";
}
}Avoids repeated initialization code.
Provides a template for configuring pre‑defined objects.
Reduces subclass count by cloning to create object variants.
4. Adapter Pattern
Adapter converts one interface into another expected by the client, enabling incompatible classes to work together. Spring Boot’s MVC adapters and third‑party integrations frequently use this pattern.
// Legacy payment service interface
public interface LegacyPaymentService {
boolean processPayment(String accountNumber, double amount, String currency);
String getTransactionStatus(String transactionId);
}
@Service("legacyPaymentService")
public class LegacyPaymentServiceImpl implements LegacyPaymentService {
@Override
public boolean processPayment(String accountNumber, double amount, String currency) {
System.out.println("Processing payment using legacy system");
return true;
}
@Override
public String getTransactionStatus(String transactionId) { return "COMPLETED"; }
}
// New payment gateway interface
public interface ModernPaymentGateway {
PaymentResponse pay(PaymentRequest request);
TransactionStatus checkStatus(String reference);
}
public class PaymentRequest { /* fields omitted for brevity */ }
public class PaymentResponse { /* fields omitted for brevity */ }
public enum TransactionStatus { PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED }
@Component
public class LegacyPaymentAdapter implements ModernPaymentGateway {
private final LegacyPaymentService legacyService;
@Override
public PaymentResponse pay(PaymentRequest request) {
boolean result = legacyService.processPayment(request.getCustomerId(),
request.getAmount().doubleValue(), request.getCurrencyCode());
PaymentResponse response = new PaymentResponse();
response.setSuccessful(result);
response.setReferenceId(UUID.randomUUID().toString());
response.setMessage(result ? "Payment processed successfully" : "Payment failed");
return response;
}
@Override
public TransactionStatus checkStatus(String reference) {
String status = legacyService.getTransactionStatus(reference);
return switch (status) {
case "COMPLETED" -> TransactionStatus.COMPLETED;
case "FAILED" -> TransactionStatus.FAILED;
case "IN_PROGRESS" -> TransactionStatus.PROCESSING;
default -> TransactionStatus.PENDING;
};
}
}
@Service
public class CheckoutService {
private final ModernPaymentGateway paymentGateway;
public void processCheckout(Cart cart, String customerId) {
PaymentRequest request = new PaymentRequest();
request.setCustomerId(customerId);
request.setAmount(cart.getTotal());
request.setCurrencyCode("USD");
PaymentResponse response = paymentGateway.pay(request);
// further processing …
}
}Reuses existing code, avoiding duplicate development.
Enables smooth transition, allowing old and new systems to run in parallel.
Decouples client code from concrete implementations.
5. Decorator Pattern
Decorator adds responsibilities to an object dynamically, offering a more flexible alternative to inheritance. Spring Boot’s @Cacheable and similar annotations are practical uses.
// Core notification service
public interface NotificationService { void send(String message, String recipient); }
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String message, String recipient) {
System.out.println("Sending email to " + recipient + ": " + message);
// actual email logic …
}
}
// Abstract decorator
public abstract class NotificationDecorator implements NotificationService {
protected NotificationService wrapped;
public NotificationDecorator(NotificationService wrapped) { this.wrapped = wrapped; }
}
@Component
public class LoggingNotificationDecorator extends NotificationDecorator {
private final Logger logger = LoggerFactory.getLogger(LoggingNotificationDecorator.class);
public LoggingNotificationDecorator(NotificationService wrapped) { super(wrapped); }
@Override
public void send(String message, String recipient) {
logger.info("Start sending notification to {}", recipient);
long start = System.currentTimeMillis();
wrapped.send(message, recipient);
long end = System.currentTimeMillis();
logger.info("Notification sent, took {} ms", (end - start));
}
}
@Component
public class RetryNotificationDecorator extends NotificationDecorator {
private final Logger logger = LoggerFactory.getLogger(RetryNotificationDecorator.class);
private final int maxRetries;
public RetryNotificationDecorator(@Qualifier("loggingNotificationDecorator") NotificationService wrapped,
@Value("${notification.max-retries:3}") int maxRetries) {
super(wrapped);
this.maxRetries = maxRetries;
}
@Override
public void send(String message, String recipient) {
int attempts = 0;
boolean sent = false;
while (!sent && attempts < maxRetries) {
try {
attempts++;
wrapped.send(message, recipient);
sent = true;
} catch (Exception e) {
logger.warn("Send failed (attempt {}): {}", attempts, e.getMessage());
if (attempts >= maxRetries) { logger.error("Max retries reached, aborting"); throw e; }
try { Thread.sleep((long) Math.pow(2, attempts) * 100); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); }
}
}
}
}
@Component
public class EncryptionNotificationDecorator extends NotificationDecorator {
public EncryptionNotificationDecorator(@Qualifier("retryNotificationDecorator") NotificationService wrapped) { super(wrapped); }
@Override
public void send(String message, String recipient) {
String encrypted = encrypt(message);
wrapped.send(encrypted, recipient);
}
private String encrypt(String message) { return "ENCRYPTED[" + message + "]"; }
}
@Configuration
public class NotificationConfig {
@Bean
public NotificationService loggingNotificationDecorator(@Qualifier("emailNotificationService") NotificationService emailService) {
return new LoggingNotificationDecorator(emailService);
}
@Bean
public NotificationService retryNotificationDecorator(@Qualifier("loggingNotificationDecorator") NotificationService loggingDecorator,
@Value("${notification.max-retries:3}") int maxRetries) {
return new RetryNotificationDecorator(loggingDecorator, maxRetries);
}
@Primary
@Bean
public NotificationService notificationService(@Qualifier("retryNotificationDecorator") NotificationService retryDecorator) {
return new EncryptionNotificationDecorator(retryDecorator);
}
}
@Service
public class UserService {
private final NotificationService notificationService;
public void notifyUser(User user, String message) {
// call chain: encryption → retry → logging → email
notificationService.send(message, user.getEmail());
}
}Maintains interface consistency while adding features dynamically.
Follows the Open‑Closed Principle; existing code stays untouched.
Allows composition of multiple enhancements as needed.
6. Observer Pattern
Observer defines a one‑to‑many dependency; when the subject changes, all observers are notified. Spring Boot’s event mechanism (ApplicationEvent & ApplicationListener) is a textbook example.
// Custom event
public class UserRegisteredEvent extends ApplicationEvent {
private final User user;
public UserRegisteredEvent(Object source, User user) { super(source); this.user = user; }
public User getUser() { return user; }
}
@Service
public class UserRegistrationService {
private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;
@Transactional
public User registerUser(UserRegistrationDto dto) {
User user = new User();
user.setUsername(dto.getUsername());
user.setEmail(dto.getEmail());
user.setPassword(encodePassword(dto.getPassword()));
user.setRegistrationDate(LocalDateTime.now());
User saved = userRepository.save(user);
eventPublisher.publishEvent(new UserRegisteredEvent(this, saved));
return saved;
}
private String encodePassword(String password) { return "{bcrypt}" + password; }
}
@Component
public class WelcomeEmailListener implements ApplicationListener<UserRegisteredEvent> {
private final EmailService emailService;
@Override
public void onApplicationEvent(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.getUser());
}
}
@Component
public class UserProfileInitializer implements ApplicationListener<UserRegisteredEvent> {
private final ProfileService profileService;
@Override
public void onApplicationEvent(UserRegisteredEvent event) {
profileService.createInitialProfile(event.getUser());
}
}
@Component
public class MarketingSubscriptionHandler {
private final MarketingService marketingService;
@EventListener
@Async
public void handleUserRegistered(UserRegisteredEvent event) {
marketingService.addUserToDefaultNewsletters(event.getUser());
}
}
@Configuration
@EnableAsync
public class AsyncConfig { }Low coupling: the publisher does not need to know the listeners.
Supports one‑to‑many notifications.
Enables asynchronous processing and event distribution.
Adding a new listener extends functionality without touching existing code.
7. Strategy Pattern
Strategy encapsulates a family of algorithms, making them interchangeable and allowing algorithm changes without affecting clients. Spring Boot frequently uses it for configurable policies such as caching or authentication.
public interface DiscountStrategy {
BigDecimal applyDiscount(BigDecimal amount, User user);
boolean isApplicable(User user, ShoppingCart cart);
}
@Component
public class NewUserDiscountStrategy implements DiscountStrategy {
@Value("${discount.new-user.percentage:10}") private int discountPercentage;
@Override
public BigDecimal applyDiscount(BigDecimal amount, User user) {
BigDecimal factor = BigDecimal.valueOf(discountPercentage).divide(BigDecimal.valueOf(100));
return amount.subtract(amount.multiply(factor));
}
@Override
public boolean isApplicable(User user, ShoppingCart cart) {
return user.getRegistrationDate().isAfter(LocalDateTime.now().minusDays(30));
}
}
@Component
public class PremiumMemberDiscountStrategy implements DiscountStrategy {
@Value("${discount.premium-member.percentage:15}") private int discountPercentage;
@Override
public BigDecimal applyDiscount(BigDecimal amount, User user) {
BigDecimal factor = BigDecimal.valueOf(discountPercentage).divide(BigDecimal.valueOf(100));
return amount.subtract(amount.multiply(factor));
}
@Override
public boolean isApplicable(User user, ShoppingCart cart) {
return "PREMIUM".equals(user.getMembershipLevel());
}
}
@Component
public class LargeOrderDiscountStrategy implements DiscountStrategy {
@Value("${discount.large-order.threshold:1000}") private BigDecimal threshold;
@Value("${discount.large-order.percentage:5}") private int discountPercentage;
@Override
public BigDecimal applyDiscount(BigDecimal amount, User user) {
BigDecimal factor = BigDecimal.valueOf(discountPercentage).divide(BigDecimal.valueOf(100));
return amount.subtract(amount.multiply(factor));
}
@Override
public boolean isApplicable(User user, ShoppingCart cart) {
return cart.getTotalAmount().compareTo(threshold) >= 0;
}
}
@Service
public class DiscountService {
private final List<DiscountStrategy> discountStrategies;
public BigDecimal calculateDiscountedAmount(BigDecimal original, User user, ShoppingCart cart) {
DiscountStrategy best = findBestDiscountStrategy(user, cart);
return best != null ? best.applyDiscount(original, user) : original;
}
private DiscountStrategy findBestDiscountStrategy(User user, ShoppingCart cart) {
BigDecimal bestDiscount = BigDecimal.ZERO;
DiscountStrategy best = null;
for (DiscountStrategy s : discountStrategies) {
if (s.isApplicable(user, cart)) {
BigDecimal discounted = s.applyDiscount(cart.getTotalAmount(), user);
BigDecimal diff = cart.getTotalAmount().subtract(discounted);
if (diff.compareTo(bestDiscount) > 0) { bestDiscount = diff; best = s; }
}
}
return best;
}
}Algorithms can be modified independently of the client.
Eliminates large conditional statements.
Adding a new strategy extends functionality with minimal impact.
Improves code reuse.
8. Template Method Pattern
Template Method defines the skeleton of an algorithm in a base class, delegating variable steps to subclasses. Spring Boot’s JdbcTemplate and RestTemplate are classic examples.
public abstract class AbstractPaymentTemplate {
public final void pay(BigDecimal money) {
verifyAccount();
doPay(money);
sendNotify();
recordLog();
}
private void verifyAccount() { System.out.println("校验账户信息"); }
private void sendNotify() { System.out.println("推送支付结果通知"); }
private void recordLog() { System.out.println("记录支付流水日志"); }
protected abstract void doPay(BigDecimal money);
}
public class WechatPayment extends AbstractPaymentTemplate {
@Override
protected void doPay(BigDecimal money) { System.out.println("调用微信支付接口,扣款金额:" + money); }
}
public class AlipayPayment extends AbstractPaymentTemplate {
@Override
protected void doPay(BigDecimal money) { System.out.println("调用支付宝支付接口,扣款金额:" + money); }
}Encapsulates invariant parts while allowing subclasses to customize variable steps.
Reduces code duplication by extracting common logic.
Controls extension points, following the Hollywood principle.
9. Chain of Responsibility Pattern
Chain of Responsibility builds a processing chain where a request traverses handlers until one handles it. Spring Boot’s filter chain is a practical illustration.
public abstract class RequestHandler {
protected RequestHandler next;
public void setNext(RequestHandler next) { this.next = next; }
public abstract void handle(RequestInfo request);
}
@Component
public class ParamCheckHandler extends RequestHandler {
@Override
public void handle(RequestInfo request) {
if (request.getRequestUri() == null) { throw new RuntimeException("请求地址不能为空,链路终止"); }
System.out.println("【参数校验】通过");
if (next != null) next.handle(request);
}
}
@Component
public class TokenCheckHandler extends RequestHandler {
@Override
public void handle(RequestInfo request) {
String token = request.getToken();
if (token == null || token.length() < 10) { throw new RuntimeException("Token无效,链路终止"); }
System.out.println("【Token校验】通过");
if (next != null) next.handle(request);
}
}
@Component
public class AuthCheckHandler extends RequestHandler {
@Override
public void handle(RequestInfo request) {
if ("guest".equals(request.getUserId())) { throw new RuntimeException("访客无访问权限,链路终止"); }
System.out.println("【权限校验】通过,放行接口");
}
}
@Configuration
public class HandlerChainConfig {
private final ParamCheckHandler paramCheckHandler;
private final TokenCheckHandler tokenCheckHandler;
private final AuthCheckHandler authCheckHandler;
@PostConstruct
public void buildChain() {
paramCheckHandler.setNext(tokenCheckHandler);
tokenCheckHandler.setNext(authCheckHandler);
}
}
@Service
public class RequestService {
private final ParamCheckHandler paramCheckHandler;
public void processRequest(RequestInfo request) { paramCheckHandler.handle(request); }
}Algorithm steps are decoupled; each handler focuses on a single concern.
Allows flexible addition, removal, or reordering of processing stages.
Improves maintainability by avoiding monolithic conditional logic.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
