10 Classic Spring Boot Design Patterns to Refactor Your Business Code

This article walks through ten classic design patterns—Factory, Proxy, Template Method, Strategy, Observer, Chain of Responsibility, Singleton, Builder, Adapter, and Decorator—showing how they are realized in Spring Boot 3.x with concrete e‑commerce, payment, and user‑system examples, and explains their benefits, trade‑offs, and best‑practice usage.

Cloud Architecture
Cloud Architecture
Cloud Architecture
10 Classic Spring Boot Design Patterns to Refactor Your Business Code

Factory Pattern – Spring IoC

The core idea of the Factory pattern is to define an interface for creating objects and let subclasses decide which concrete class to instantiate. Spring implements this through BeanFactory and ApplicationContext. A typical real‑world example is a payment‑method factory:

public interface PaymentStrategy {
    PaymentType getType();
    PaymentResult pay(Order order);
}

enum PaymentType { WECHAT, ALIPAY, UNIONPAY }

@Component
class WechatPaymentStrategy implements PaymentStrategy {
    @Override public PaymentType getType() { return PaymentType.WECHAT; }
    @Override public PaymentResult pay(Order order) { /* call WeChat API */ }
}

@Component
class PaymentFactory {
    private final Map<PaymentType, PaymentStrategy> strategies;
    public PaymentFactory(List<PaymentStrategy> list) {
        this.strategies = list.stream()
            .collect(Collectors.toMap(PaymentStrategy::getType, s -> s));
    }
    public PaymentStrategy getStrategy(PaymentType type) {
        return Optional.ofNullable(strategies.get(type))
            .orElseThrow(() -> new IllegalArgumentException("Unsupported payment type: " + type));
    }
}

@Service
class OrderPaymentService {
    private final PaymentFactory factory;
    public OrderPaymentService(PaymentFactory factory) { this.factory = factory; }
    @Transactional
    public PaymentResult processPayment(Long orderId, PaymentType type) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
        return factory.getStrategy(type).pay(order);
    }
}

Advantages: decoupling of callers from concrete implementations, easy extensibility (add a new @Component without touching the factory), centralized creation logic, and compliance with the Open‑Closed Principle.

Proxy Pattern – Spring AOP

Proxy provides a surrogate that controls access to another object. Spring creates proxies in two ways:

JDK dynamic proxy – interface‑based.

CGLIB proxy – subclass‑based, used when no interface is present.

Example: a custom @DataPermission annotation that injects a data‑filter into the method arguments.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission { PermissionType value(); }

enum PermissionType { DEPT_ONLY, DEPT_AND_SUB, SELF_ONLY }

@Aspect
@Component
class DataPermissionAspect {
    @Around("@annotation(dp)")
    public Object around(ProceedingJoinPoint pjp, DataPermission dp) throws Throwable {
        Long userId = SecurityContext.getCurrentUserId();
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        DataFilter filter = buildFilter(user, dp.value());
        Object[] args = pjp.getArgs();
        Object[] newArgs = injectFilter(args, filter);
        return pjp.proceed(newArgs);
    }
    // buildFilter and injectFilter omitted for brevity
}

Typical cross‑cutting concerns implemented via proxies: transaction management ( @Transactional), logging, security, caching, performance monitoring, retry mechanisms.

Template Method Pattern – Fixed Business Processes

Template Method defines the skeleton of an algorithm in a method, delegating some steps to subclasses. Spring’s JdbcTemplate and RestTemplate are classic examples.

public class JdbcTemplate {
    public <T> T query(String sql, RowCallbackHandler rch) {
        Connection conn = DataSourceUtils.getConnection(dataSource);
        try {
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            while (rs.next()) { rch.processRow(rs); }
            return result;
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(conn, dataSource);
        }
    }
}

Order processing template:

public abstract class AbstractOrderProcessor {
    public final OrderResult process(OrderContext ctx) {
        validate(ctx);
        checkStock(ctx);
        calculatePrice(ctx);
        riskCheck(ctx);
        createOrder(ctx);
        sendNotification(ctx);
        return buildResult(ctx);
    }
    protected void validate(OrderContext ctx) { /* common checks */ }
    protected void riskCheck(OrderContext ctx) { /* common risk logic */ }
    protected void sendNotification(OrderContext ctx) { /* publish event */ }
    protected abstract void checkStock(OrderContext ctx);
    protected abstract void calculatePrice(OrderContext ctx);
    protected abstract void createOrder(OrderContext ctx);
    protected OrderResult buildResult(OrderContext ctx) { return OrderResult.success(ctx.getOrderId()); }
}

@Component @Order(1)
class NormalOrderProcessor extends AbstractOrderProcessor {
    @Override protected void checkStock(OrderContext ctx) { /* inventory check */ }
    @Override protected void calculatePrice(OrderContext ctx) { /* sum item prices + shipping */ }
    @Override protected void createOrder(OrderContext ctx) { /* persist normal order */ }
}

@Service
class OrderService {
    private final List<AbstractOrderProcessor> processors;
    public OrderService(List<AbstractOrderProcessor> processors) { this.processors = processors; }
    public OrderResult process(OrderContext ctx) {
        AbstractOrderProcessor p = processors.stream()
            .filter(proc -> proc.supports(ctx.getOrderType()))
            .findFirst()
            .orElseThrow(() -> new UnsupportedOrderTypeException(ctx.getOrderType()));
        return p.process(ctx);
    }
}

Benefits: code reuse, standardized workflow, easy extension for new order types, and inversion of control over the process flow.

Strategy Pattern – Multi‑Channel Notification

Strategy encapsulates a family of algorithms and makes them interchangeable.

public interface NotificationStrategy {
    NotificationType getType();
    void send(NotificationContext ctx);
    default boolean supports(NotificationType type) { return getType() == type; }
}

enum NotificationType { SMS, EMAIL, IN_APP, WECHAT_TEMPLATE }

@Component
class SmsNotificationStrategy implements NotificationStrategy {
    @Override public NotificationType getType() { return NotificationType.SMS; }
    @Override public void send(NotificationContext ctx) {
        // build SmsRequest and call smsClient
    }
}

@Component
class NotificationContext {
    private final Map<NotificationType, NotificationStrategy> strategies;
    public NotificationContext(List<NotificationStrategy> list) {
        this.strategies = list.stream()
            .collect(Collectors.toMap(NotificationStrategy::getType, s -> s));
    }
    public void send(NotificationType type, NotificationRequest req) {
        NotificationStrategy s = strategies.get(type);
        if (s == null) throw new IllegalArgumentException("Unsupported type: " + type);
        s.send(NotificationContext.from(req));
    }
}

@Service
class OrderNotificationService {
    private final NotificationContext ctx;
    public void notifyOrderCreated(Order order) {
        NotificationRequest req = NotificationRequest.builder()
            .receiver(order.getUserPhone())
            .templateId("ORDER_CREATED")
            .param("orderId", order.getId())
            .param("amount", order.getAmount())
            .build();
        ctx.send(NotificationType.SMS, req);
        ctx.send(NotificationType.IN_APP, req);
    }
}

Combining Strategy with Factory further removes conditional logic:

@Component
class PaymentStrategyFactory {
    private final Map<PaymentType, PaymentStrategy> map;
    public PaymentStrategyFactory(List<PaymentStrategy> list) {
        this.map = list.stream()
            .collect(Collectors.toMap(PaymentStrategy::getType, Function.identity()));
    }
    public PaymentStrategy getStrategy(PaymentType type) {
        return Optional.ofNullable(map.get(type))
            .orElseThrow(() -> new IllegalArgumentException("Unsupported payment type: " + type));
    }
}

Observer Pattern – Event‑Driven Architecture

Defines a one‑to‑many dependency so that when an object changes state, all dependents are notified.

public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;
    public OrderCreatedEvent(Object source, Order order) { super(source); this.order = order; }
    public Order getOrder() { return order; }
}

@Service
class OrderService {
    private final ApplicationEventPublisher publisher;
    @Transactional
    public Order createOrder(OrderRequest req) {
        Order order = orderRepository.save(req.toEntity());
        publisher.publishEvent(new OrderCreatedEvent(this, order));
        return order;
    }
}

@Component
class OrderEventListener {
    @EventListener @Async
    public void handle(OrderCreatedEvent ev) {
        notificationService.sendOrderCreatedNotification(ev.getOrder());
        inventoryService.deductStock(ev.getOrder());
    }
}

// User‑registration example with multiple observers
public class UserRegisteredEvent extends ApplicationEvent {
    private final User user; private final RegistrationSource source; private final Instant registeredAt;
    public UserRegisteredEvent(Object src, User user, RegistrationSource srcEnum, Instant at) {
        super(src); this.user = user; this.source = srcEnum; this.registeredAt = at; }
    // getters omitted
}

@Component
class WelcomeEmailListener {
    @EventListener @Async
    public void on(UserRegisteredEvent ev) { /* build email with template engine */ }
}

@Component
class NewUserCouponListener {
    @EventListener @Async @Transactional
    public void on(UserRegisteredEvent ev) {
        List<CouponTemplate> tmpl = switch (ev.getSource()) {
            case APP -> couponService.getAppNewUserTemplates();
            case WEB -> couponService.getWebNewUserTemplates();
            case MINI_PROGRAM -> couponService.getMiniProgramNewUserTemplates();
            default -> couponService.getDefaultNewUserTemplates();
        };
        couponService.issueCoupons(ev.getUser().getId(), tmpl);
    }
}

// Conditional listener example
public interface ConditionalEventListener<T extends ApplicationEvent> {
    void onEvent(T event);
    default boolean supports(T event) { return true; }
}

@Component
class HighValueOrderListener implements ConditionalEventListener<OrderCreatedEvent> {
    @Override public void onEvent(OrderCreatedEvent ev) {
        // handle high‑value order
    }
    @Override public boolean supports(OrderCreatedEvent ev) {
        return ev.getOrder().getAmount().compareTo(new BigDecimal("10000")) > 0;
    }
}

@Component
class ConditionalEventDispatcher {
    private final List<ConditionalEventListener<?>> listeners;
    public void dispatch(ApplicationEvent ev) {
        for (ConditionalEventListener listener : listeners) {
            if (listener.supports(ev)) listener.onEvent(ev);
        }
    }
}

Chain of Responsibility – Request Processing Pipeline

Spring MVC’s HandlerInterceptor chain is a classic CoR implementation.

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mv) throws Exception;
    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}

@Configuration
class WebConfig implements WebMvcConfigurer {
    @Override public void addInterceptors(InterceptorRegistry reg) {
        reg.addInterceptor(loggingInterceptor()).addPathPatterns("/**").order(1);
        reg.addInterceptor(authenticationInterceptor()).addPathPatterns("/api/**").excludePathPatterns("/api/public/**").order(2);
        reg.addInterceptor(permissionInterceptor()).addPathPatterns("/api/admin/**").order(3);
        reg.addInterceptor(rateLimitInterceptor()).addPathPatterns("/api/**").order(4);
    }
}

Order‑creation validation chain example:

public interface OrderValidator {
    void validate(OrderContext ctx);
    String name();
    default boolean supports(OrderType type) { return true; }
}

@Component @Order(1) class UserStatusValidator implements OrderValidator { /* checks active/frozen */ }
@Component @Order(2) class InventoryValidator implements OrderValidator { /* stock check */ }
@Component @Order(3) class RiskControlValidator implements OrderValidator { /* risk service */ }
@Component @Order(4) class CouponValidator implements OrderValidator { /* coupon validation */ }

@Component
class OrderValidationChain {
    private final List<OrderValidator> validators;
    public OrderValidationChain(List<OrderValidator> list) {
        this.validators = list.stream()
            .sorted(Comparator.comparingInt(v -> v.getClass().getAnnotation(Order.class).value()))
            .collect(Collectors.toList());
    }
    public void validate(OrderContext ctx) {
        for (OrderValidator v : validators) {
            if (!v.supports(ctx.getOrderType())) continue;
            long start = System.currentTimeMillis();
            v.validate(ctx);
            long cost = System.currentTimeMillis() - start;
            // log validator name and cost
        }
    }
}

@Service
class OrderCreateService {
    private final OrderValidationChain chain;
    private final OrderRepository repo;
    @Transactional
    public Order createOrder(OrderRequest req) {
        OrderContext ctx = OrderContext.from(req);
        chain.validate(ctx);
        Order order = ctx.toEntity();
        return repo.save(order);
    }
}

A configurable version reads enabled validators from application.yml via a ValidationChainConfig bean.

Singleton Pattern – Spring Bean Default Scope

Spring beans are singleton by default, guaranteeing a single instance per container. Stateless services should be singleton; stateful beans can cause thread‑safety issues.

@Component
class UserService { /* stateless singleton */ }

@Component @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
class OrderService { /* explicit singleton */ }

// Thread‑safe configuration manager
@Component @Slf4j
class ConfigManager {
    private final ConcurrentHashMap<String, Object> configs = new ConcurrentHashMap<>();
    private final ConfigRepository repo;
    @PostConstruct
    public void init() {
        repo.findAll().forEach(e -> configs.put(e.getKey(), parseValue(e.getValue())));
        log.info("ConfigManager initialized with {} configs", configs.size());
    }
    public <T> T get(String key, Class<T> type) { Object v = configs.get(key); return v == null ? null : type.cast(v); }
    @Async
    public void refresh(String key) {
        ConfigEntity e = repo.findByKey(key);
        if (e != null) { configs.put(key, parseValue(e.getValue())); log.info("Config refreshed: {}", key); }
    }
}

@Service
class FeatureFlagService {
    private final ConfigManager cfg;
    public boolean isEnabled(String feature) { return cfg.getOrDefault("feature." + feature, false); }
}

Incorrect example (stateful singleton) is avoided because shared mutable fields are not thread‑safe.

Builder Pattern – Complex Object Construction

Using Lombok’s @Builder to create an immutable OrderQuery with validation logic in a custom builder.

@Data @Builder
public class OrderQuery {
    private Long userId; private OrderStatus status; private LocalDateTime startTime; private LocalDateTime endTime;
    private List<Long> productIds; private BigDecimal minAmount; private BigDecimal maxAmount;
    private String keyword; private Integer pageNum; private Integer pageSize; private String sortBy; private String sortOrder;
    public static class OrderQueryBuilder {
        public OrderQuery build() {
            if (pageNum == null || pageNum < 1) pageNum = 1;
            if (pageSize == null || pageSize < 1) pageSize = 20;
            if (pageSize > 100) pageSize = 100;
            if (startTime != null && endTime != null && startTime.isAfter(endTime))
                throw new IllegalArgumentException("startTime must be before endTime");
            if (minAmount != null && maxAmount != null && minAmount.compareTo(maxAmount) > 0)
                throw new IllegalArgumentException("minAmount must be <= maxAmount");
            return new OrderQuery(this);
        }
    }
}

@Component
class OrderQueryDirector {
    public OrderQuery recentOrders(Long userId) {
        return OrderQuery.builder()
            .userId(userId)
            .startTime(LocalDateTime.now().minusMonths(3))
            .endTime(LocalDateTime.now())
            .sortBy("createdAt").sortOrder("DESC")
            .pageSize(50)
            .build();
    }
    public OrderQuery pendingOrders() { /* similar */ }
    public OrderQuery highValueOrders(BigDecimal min) { /* similar */ }
}

Other Classic Patterns in Spring

Adapter – HandlerAdapter adapts different controller types ( RequestMappingHandlerAdapter, HttpRequestHandlerAdapter).

Decorator – StatisticsCache decorates a Cache to add hit/miss statistics.

public class StatisticsCache implements Cache {
    private final Cache delegate;
    private final AtomicLong hitCount = new AtomicLong();
    private final AtomicLong missCount = new AtomicLong();
    @Override public Object get(Object key) {
        Object v = delegate.get(key);
        if (v != null) hitCount.incrementAndGet(); else missCount.incrementAndGet();
        return v;
    }
    public double getHitRate() {
        long total = hitCount.get() + missCount.get();
        return total == 0 ? 0 : (double) hitCount.get() / total;
    }
    // other Cache methods delegate directly
}

Prototype – prototype‑scoped bean for per‑request objects such as a shopping cart.

@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class ShoppingCart { private List<CartItem> items = new ArrayList<>(); /* addItem etc. */ }

@Service
class CartService {
    @Autowired private ObjectProvider<ShoppingCart> cartProvider;
    public ShoppingCart createCart(Long userId) {
        ShoppingCart cart = cartProvider.getIfAvailable();
        cart.setUserId(userId);
        return cart;
    }
}

Comprehensive Case Study – E‑commerce Promotion System

The system combines several patterns:

Strategy – PromotionStrategy (discount, full‑reduction) with getPriority() for ordering.

Chain of Responsibility – PromotionValidator chain (time range, user eligibility).

Factory – PromotionStrategyFactory maps PromotionType to strategy instances.

Template Method – AbstractPromotionProcessor defines the processing flow (load rule, validate, calculate, log, post‑process).

Observer – PromotionAppliedEvent published after processing; listeners update statistics and send notifications.

public interface PromotionStrategy {
    PromotionType getType();
    PromotionResult apply(PromotionContext ctx);
    int getPriority();
}

@Component @Order(1)
class DiscountPromotionStrategy implements PromotionStrategy {
    @Override public PromotionType getType() { return PromotionType.DISCOUNT; }
    @Override public PromotionResult apply(PromotionContext ctx) {
        DiscountRule r = ctx.getDiscountRule();
        BigDecimal discounted = ctx.getAmount().multiply(r.getDiscountRate());
        return PromotionResult.builder()
            .type(PromotionType.DISCOUNT)
            .originalAmount(ctx.getAmount())
            .finalAmount(discounted)
            .discount(ctx.getAmount().subtract(discounted))
            .build();
    }
    @Override public int getPriority() { return 1; }
}

@Component @Order(2)
class FullReductionPromotionStrategy implements PromotionStrategy { /* similar */ }

public interface PromotionValidator {
    void validate(PromotionContext ctx);
    default boolean supports(PromotionType type) { return true; }
}

@Component @Order(1) class TimeRangeValidator implements PromotionValidator { /* checks start/end */ }
@Component @Order(2) class UserEligibilityValidator implements PromotionValidator { /* checks level, new‑user flag */ }

@Component
class PromotionStrategyFactory {
    private final Map<PromotionType, PromotionStrategy> map;
    public PromotionStrategyFactory(List<PromotionStrategy> list) {
        this.map = list.stream().collect(Collectors.toMap(PromotionStrategy::getType, s -> s));
    }
    public PromotionStrategy getStrategy(PromotionType type) {
        return Optional.ofNullable(map.get(type))
            .orElseThrow(() -> new IllegalArgumentException("Unsupported promotion type: " + type));
    }
    public List<PromotionStrategy> getAllStrategies() {
        return map.values().stream()
            .sorted(Comparator.comparingInt(PromotionStrategy::getPriority))
            .collect(Collectors.toList());
    }
}

public abstract class AbstractPromotionProcessor {
    public final PromotionResult process(PromotionContext ctx) {
        loadRule(ctx);
        validate(ctx);
        PromotionResult res = calculateDiscount(ctx);
        logPromotion(ctx, res);
        postProcess(ctx, res);
        return res;
    }
    protected void loadRule(PromotionContext ctx) { /* fetch rule from repository */ }
    protected void validate(PromotionContext ctx) { validationChain.validate(ctx); }
    protected abstract PromotionResult calculateDiscount(PromotionContext ctx);
    protected void logPromotion(PromotionContext ctx, PromotionResult res) {
        log.info("Promotion applied: type={}, userId={}, discount={}", res.getType(), ctx.getUserId(), res.getDiscount());
    }
    protected void postProcess(PromotionContext ctx, PromotionResult res) { /* optional */ }
}

@Service
class PromotionService {
    private final PromotionStrategyFactory factory;
    private final ValidationChain validationChain;
    private final ApplicationEventPublisher publisher;
    public PromotionResult applyPromotions(ApplyPromotionRequest req) {
        PromotionContext ctx = PromotionContext.from(req);
        List<PromotionStrategy> strategies = factory.getAllStrategies();
        BigDecimal amount = req.getAmount();
        List<PromotionResult> results = new ArrayList<>();
        for (PromotionStrategy s : strategies) {
            if (!s.getType().equals(ctx.getPromotionType())) continue;
            ctx.setAmount(amount);
            PromotionResult r = s.apply(ctx);
            if (r.isApplied()) {
                results.add(r);
                amount = r.getFinalAmount();
                if (!ctx.getRule().isStackable()) break;
            }
        }
        publisher.publishEvent(new PromotionAppliedEvent(this, ctx.getUserId(), results, amount));
        return PromotionResult.combine(results, amount);
    }
}

Guidelines for Applying Design Patterns in Spring

When to use which pattern :

Complex object creation → Factory or Builder.

Algorithm selection at runtime → Strategy.

Fixed workflow with variable steps → Template Method.

Decoupling sender and receiver → Observer or Chain of Responsibility.

Controlling access, adding cross‑cutting concerns → Proxy (AOP).

Global unique instance → Singleton (default bean scope).

Design principles :

YAGNI – avoid over‑design.

Prefer composition over inheritance.

Program to interfaces, not implementations.

Open‑Closed Principle – extend without modifying existing code.

Single Responsibility – each class has one reason to change.

Applying these patterns through Spring’s IoC container, AOP framework, event mechanism, and bean scopes leads to clean, maintainable, and production‑ready code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsStrategy Patternspring-bootFactory Patterntemplate methodsingletonObserver PatternBuilder
Cloud Architecture
Written by

Cloud Architecture

Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.