10 Years In, I Discovered Spring Boot + Decorator Pattern for Elegant Extension
After years of piling logging, caching, and security directly into Spring Boot services, the author shows how applying the Decorator pattern—implemented via dedicated @Service beans and @Primary/@Qualifier wiring—creates a clean, extensible chain that respects the Open/Closed Principle, while outlining practical steps, trade‑offs, and pitfalls.
Problem: Bloated Service Layer
When a Service class starts with only core business logic, developers often add logging, caching, permission checks, and monitoring directly inside the same class as requirements grow. The method becomes packed with side‑effects, polluting business logic, increasing modification risk, violating the Open/Closed Principle (OCP), and raising maintenance cost.
// Bloated service example
@Service
public class BloatedProductService implements ProductService {
private final ProductRepository productRepository;
private final Logger logger = LoggerFactory.getLogger(BloatedProductService.class);
private final CacheManager cacheManager;
private final SecurityService securityService;
@Override
public Product getProductById(String id) {
logger.info("开始查询商品 {}", id);
securityService.authorize("read:product", id);
Product product = getFromCache(id);
if (product != null) {
return product;
}
product = productRepository.findById(id).orElse(null);
if (product != null) {
putInCache(id, product);
}
return product;
}
}Business logic is polluted.
Modification risk increases.
Violates OCP.
Maintenance cost skyrockets.
Open/Closed Principle
Software should be "open for extension, closed for modification".
Decorator Pattern
The Decorator pattern enhances an object's behavior without changing its source code by wrapping the original object with one or more decorator classes, each adding a specific concern.
Enhance functionality while keeping the original object unchanged.
The chain can be visualised as Original Service → Logging Decorator → Caching Decorator .
Step‑by‑Step Implementation in Spring Boot
1. Define Core Interface and Simple Implementation
// src/main/java/com/icoderoad/product/domain/Product.java
package com.icoderoad.product.domain;
public record Product(String id, String name, double price) {} // ProductService.java
package com.icoderoad.product.service;
import java.util.List;
public interface ProductService {
Product getProductById(String id);
List<Product> getAllProducts();
} // SimpleProductService.java
package com.icoderoad.product.service.impl;
import com.icoderoad.product.service.ProductService;
import org.springframework.stereotype.Service;
@Service("baseProductService")
public class SimpleProductService implements ProductService {
private final ProductRepository productRepository;
public SimpleProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Product getProductById(String id) {
System.out.println("查询数据库: " + id);
return productRepository.findById(id).orElse(null);
}
@Override
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}2. Implement Logging Decorator
// LoggingProductServiceDecorator.java
package com.icoderoad.product.decorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service("loggingProductService")
public class LoggingProductServiceDecorator implements ProductService {
private final ProductService delegate;
private static final Logger log = LoggerFactory.getLogger(LoggingProductServiceDecorator.class);
public LoggingProductServiceDecorator(@Qualifier("baseProductService") ProductService delegate) {
this.delegate = delegate;
}
@Override
public Product getProductById(String id) {
log.info("开始查询商品: {}", id);
Product product = delegate.getProductById(id);
log.info("查询结束: {}", product);
return product;
}
@Override
public List<Product> getAllProducts() {
log.info("查询全部商品");
return delegate.getAllProducts();
}
}3. Implement Caching Decorator
// CachingProductServiceDecorator.java
package com.icoderoad.product.decorator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@Primary
public class CachingProductServiceDecorator implements ProductService {
private final ProductService delegate;
private final Map<String, Product> cache = new ConcurrentHashMap<>();
public CachingProductServiceDecorator(@Qualifier("loggingProductService") ProductService delegate) {
this.delegate = delegate;
}
@Override
public Product getProductById(String id) {
if (cache.containsKey(id)) {
return cache.get(id);
}
Product product = delegate.getProductById(id);
if (product != null) {
cache.put(id, product);
}
return product;
}
@Override
public List<Product> getAllProducts() {
return delegate.getAllProducts();
}
}4. Let Spring Assemble the Chain
Using @Primary on the caching decorator marks it as the entry point. @Qualifier on each constructor selects the bean to wrap, resulting in the call order: Cache → Logging → Original Service.
productService.getProductById("1");
// Execution order: Cache → Logging → Original ServiceBenefits of the Decorator Approach
Responsibilities are cleanly separated: logging, caching, and core business logic reside in independent classes.
Complies with the Open/Closed Principle; new features are added by creating additional decorators without modifying existing code.
Explicit ordering via @Qualifier makes the call sequence transparent.
Configuration Class for Complex Chains
// ProductServiceConfig.java
package com.icoderoad.product.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class ProductServiceConfig {
@Bean
public ProductService baseProductService(ProductRepository repository) {
return new SimpleProductService(repository);
}
@Bean
public ProductService loggingProductService(ProductService base) {
return new LoggingProductServiceDecorator(base);
}
@Bean
@Primary
public ProductService cachingProductService(ProductService logging) {
return new CachingProductServiceDecorator(logging);
}
}Clear ordering.
Strong controllability.
Scales better for large systems.
Common Pitfalls
Too many decorators: long chains make debugging difficult and increase the JVM call stack; keep layers typically between 3 and 5.
State pollution: only the caching decorator should hold state; core business logic must remain stateless.
Performance overhead: each layer adds a method call, so high‑frequency interfaces should be decorated cautiously.
Decorator vs. AOP
Decorator is suitable when precise control over invocation order, targeting a specific bean, or an explicit chain structure is required.
AOP is suitable for global cross‑cutting concerns such as logging or transaction management that should be woven uniformly and remain completely decoupled from business code.
AOP solves "cross‑cutting"; Decorator solves "compositional enhancement".
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.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
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.
