Back to blog
Dec 27, 2024
8 min read

Strategy Pattern with Spring: A Practical Guide

Comprehensive Guide to Strategy Pattern Implementation with Spring Framework

Spring-based example for implementing Strategy, Factory, and Builder patterns for flexible product creation.

Table of Contents

Architecture Overview

Design Patterns Used

  • Strategy Pattern: Enables different product creation algorithms based on product type
  • Factory Pattern: Manages strategy creation and caching
  • Builder Pattern: Flexible product details construction with type-specific attributes

Core Components

ProductType.java

Enum defining supported product types. Centralized definition ensures type safety across the system.

public enum ProductType {
ELECTRONICS,
FURNITURE,
CLOTHING
}

ProductDetails.java

Core domain model using Builder pattern. Provides flexible specification management for different product types.

@Builder
public class ProductDetails {
private final String productId;
private final BigDecimal price;
private final String category;
private final String name;
private final Map<String, String> specifications;
public static class ProductDetailsBuilder {
private Map<String, String> specifications = new HashMap<>();
public ProductDetailsBuilder addSpecification(String key, String value) {
this.specifications.put(key, value);
return this;
}
}
}

Processors

QualityProcessor.java

Quality validation interface. Decouples validation logic from product creation.

public interface QualityProcessor {
void validate(ProductDetails details);
}

DefaultQualityProcessor.java

Default implementation of quality validation. Can be extended for specific product types.

@Service
public class DefaultQualityProcessor implements QualityProcessor {
@Override
public void validate(ProductDetails details) {
// Implementation of quality validation
}
}

Strategies

ProductStrategy.java

Base strategy interface defining product creation contract. Each product type implements this interface.

@Service
public interface ProductStrategy {
void create(ProductDetails details);
ProductType getType();
}

ElectronicsStrategy.java

Electronics-specific creation logic. Handles validation and creation for electronic products.

@Service("electronics")
public class ElectronicsStrategy implements ProductStrategy {
@Autowired
private QualityProcessor processor;
@Override
public void create(ProductDetails details) {
processor.validate(details);
// Electronics specific creation logic
}
@Override
public ProductType getType() {
return ProductType.ELECTRONICS;
}
}

FurnitureStrategy.java

Furniture-specific creation logic. Handles validation and creation for furniture products.

FurnitureStrategy.java
@Service("furniture")
public class FurnitureStrategy implements ProductStrategy {
@Autowired
private QualityProcessor processor;
@Override
public void create(ProductDetails details) {
processor.validate(details);
// Furniture specific creation logic
}
@Override
public ProductType getType() {
return ProductType.FURNITURE;
}
}

Factory & Service

ProductStrategyFactory.java

Factory using Spring DI for strategy management. Caches strategies for better performance.

ProductStrategyFactory.java
@Component
public class ProductStrategyFactory {
private final Map<ProductType, ProductStrategy> strategyCache = new EnumMap<>(ProductType.class);
@Autowired
public ProductStrategyFactory(List<ProductStrategy> strategies) {
strategies.forEach(strategy ->
strategyCache.put(strategy.getType(), strategy));
}
public ProductStrategy createStrategy(ProductType type) {
ProductStrategy strategy = strategyCache.get(type);
if (strategy == null) {
throw new IllegalArgumentException("Unknown product type: " + type);
}
return strategy;
}
}

ProductService.java

Central service orchestrating product creation. Provides both direct and builder-based creation methods.

ProductService.java
@Service
public class ProductService {
private final ProductStrategyFactory strategyFactory;
@Autowired
public ProductService(ProductStrategyFactory strategyFactory) {
this.strategyFactory = strategyFactory;
}
public void createProduct(ProductType type, ProductDetails details) {
ProductStrategy strategy = strategyFactory.createStrategy(type);
strategy.create(details);
}
public void createProduct(ProductType type, Consumer<ProductDetails.ProductDetailsBuilder> builderConsumer) {
ProductDetails.ProductDetailsBuilder builder = ProductDetails.builder();
builderConsumer.accept(builder);
createProduct(type, builder.build());
}
}

API Layer

ProductController.java

REST controller handling product creation requests. Processes type-specific attributes dynamically.

ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/create")
public ResponseEntity<String> createProduct(@RequestBody JsonNode request) {
try {
ProductType type = ProductType.valueOf(request.get("productType").asText().toUpperCase());
ProductDetails.ProductDetailsBuilder builder = ProductDetails.builder()
.productId(request.get("productId").asText())
.name(request.get("name").asText())
.price(new BigDecimal(request.get("price").asText()));
switch (type) {
case ELECTRONICS:
addElectronicsSpecs(builder, request);
break;
case FURNITURE:
addFurnitureSpecs(builder, request);
break;
case CLOTHING:
addClothingSpecs(builder, request);
break;
default:
throw new IllegalArgumentException("Unsupported product type");
}
productService.createProduct(type, builder.build());
return ResponseEntity.ok("Product created successfully");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("Invalid product type");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid request format");
}
}
private void addElectronicsSpecs(ProductDetails.ProductDetailsBuilder builder, JsonNode request) {
builder.addSpecification("processor", request.get("processor").asText())
.addSpecification("ram", request.get("ram").asText())
.addSpecification("storage", request.get("storage").asText())
.category("Electronics");
}
private void addFurnitureSpecs(ProductDetails.ProductDetailsBuilder builder, JsonNode request) {
builder.addSpecification("material", request.get("material").asText())
.addSpecification("dimensions", request.get("dimensions").asText())
.addSpecification("weight", request.get("weight").asText())
.category("Furniture");
}
private void addClothingSpecs(ProductDetails.ProductDetailsBuilder builder, JsonNode request) {
builder.addSpecification("size", request.get("size").asText())
.addSpecification("color", request.get("color").asText())
.addSpecification("material", request.get("material").asText())
.category("Clothing");
}
}

Usage Examples

API Request Examples

// Electronics Product
{
"productType": "ELECTRONICS",
"productId": "ELEC123",
"name": "Super Phone X",
"price": "599.99",
"processor": "Snapdragon 8 Gen 2",
"ram": "8GB",
"storage": "256GB"
}
// Furniture Product
{
"productType": "FURNITURE",
"productId": "FUR456",
"name": "Ergonomic Chair",
"price": "299.99",
"material": "Leather",
"dimensions": "60x70x120cm",
"weight": "15kg"
}
// Clothing Product
{
"productType": "CLOTHING",
"productId": "CLT789",
"name": "Winter Jacket",
"price": "89.99",
"size": "L",
"color": "Navy Blue",
"material": "Wool"
}

System Interaction Diagram

QualityProcessorProductStrategyProductStrategyFactoryProductServiceProductControllerClientQualityProcessorProductStrategyProductStrategyFactoryProductServiceProductControllerClientParse JSON & Extract TypePOST /api/products/createBuild ProductDetailscreateProduct(type, details)createStrategy(type)Return cached strategycreate(details)validate(details)Validation resultExecute type-specific logicCreation completeSuccess200 OK Response

sequenceDiagram
participant Client
participant ProductController
participant ProductService
participant ProductStrategyFactory
participant ProductStrategy
participant QualityProcessor
Client->>ProductController: POST /api/products/create
Note over ProductController: Parse JSON & Extract Type
ProductController->>ProductController: Build ProductDetails
ProductController->>ProductService: createProduct(type, details)
ProductService->>ProductStrategyFactory: createStrategy(type)
ProductStrategyFactory-->>ProductService: Return cached strategy
ProductService->>ProductStrategy: create(details)
ProductStrategy->>QualityProcessor: validate(details)
QualityProcessor-->>ProductStrategy: Validation result
ProductStrategy->>ProductStrategy: Execute type-specific logic
ProductStrategy-->>ProductService: Creation complete
ProductService-->>ProductController: Success
ProductController-->>Client: 200 OK Response

Exception Flow Diagram

ProductStrategyFactoryProductServiceProductControllerClientProductStrategyFactoryProductServiceProductControllerClientPOST with invalid typecreateProduct(invalidType)createStrategy(invalidType)Throw IllegalArgumentExceptionException propagation400 Bad Request

sequenceDiagram
participant Client
participant ProductController
participant ProductService
participant ProductStrategyFactory
Client->>ProductController: POST with invalid type
ProductController->>ProductService: createProduct(invalidType)
ProductService->>ProductStrategyFactory: createStrategy(invalidType)
ProductStrategyFactory-->>ProductService: Throw IllegalArgumentException
ProductService-->>ProductController: Exception propagation
ProductController-->>Client: 400 Bad Request

Dependencies

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>

Test Guide

Unit Tests

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
ProductDetailsBuilderTest.java
@Test
public class ProductDetailsBuilderTest {
@Test
void shouldBuildValidProductDetails() {
ProductDetails details = ProductDetails.builder()
.productId("TEST123")
.price(new BigDecimal("99.99"))
.name("Test Product")
.category("Test")
.addSpecification("key1", "value1")
.build();
assertNotNull(details);
assertEquals("TEST123", details.getProductId());
assertEquals(new BigDecimal("99.99"), details.getPrice());
assertEquals("value1", details.getSpecifications().get("key1"));
}
}
DefaultQualityProcessorTest.java
@ExtendWith(MockitoExtension.class)
public class DefaultQualityProcessorTest {
@InjectMocks
private DefaultQualityProcessor processor;
@Test
void shouldValidateValidProduct() {
ProductDetails details = ProductDetails.builder()
.productId("TEST123")
.price(new BigDecimal("99.99"))
.build();
assertDoesNotThrow(() -> processor.validate(details));
}
}
ElectronicsStrategyTest.java
@ExtendWith(MockitoExtension.class)
public class ElectronicsStrategyTest {
@Mock
private QualityProcessor qualityProcessor;
@InjectMocks
private ElectronicsStrategy strategy;
@Test
void shouldCreateElectronicsProduct() {
ProductDetails details = ProductDetails.builder()
.productId("ELEC123")
.price(new BigDecimal("599.99"))
.addSpecification("processor", "Intel i7")
.build();
doNothing().when(qualityProcessor).validate(any(ProductDetails.class));
assertDoesNotThrow(() -> strategy.create(details));
verify(qualityProcessor, times(1)).validate(details);
}
@Test
void shouldReturnCorrectType() {
assertEquals(ProductType.ELECTRONICS, strategy.getType());
}
}
ProductStrategyFactoryTest.java
@ExtendWith(MockitoExtension.class)
public class ProductStrategyFactoryTest {
@Mock
private List<ProductStrategy> strategies;
@Mock
private ElectronicsStrategy electronicsStrategy;
@Test
void shouldCreateValidStrategy() {
when(electronicsStrategy.getType()).thenReturn(ProductType.ELECTRONICS);
List<ProductStrategy> strategyList = Collections.singletonList(electronicsStrategy);
ProductStrategyFactory factory = new ProductStrategyFactory(strategyList);
ProductStrategy strategy = factory.createStrategy(ProductType.ELECTRONICS);
assertNotNull(strategy);
assertEquals(ProductType.ELECTRONICS, strategy.getType());
}
@Test
void shouldThrowExceptionForInvalidType() {
ProductStrategyFactory factory = new ProductStrategyFactory(Collections.emptyList());
assertThrows(IllegalArgumentException.class,
() -> factory.createStrategy(ProductType.ELECTRONICS));
}
}
ProductServiceTest.java
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductStrategyFactory strategyFactory;
@Mock
private ProductStrategy productStrategy;
@InjectMocks
private ProductService productService;
@Test
void shouldCreateProductSuccessfully() {
ProductDetails details = ProductDetails.builder()
.productId("TEST123")
.build();
when(strategyFactory.createStrategy(ProductType.ELECTRONICS))
.thenReturn(productStrategy);
assertDoesNotThrow(() ->
productService.createProduct(ProductType.ELECTRONICS, details));
verify(productStrategy, times(1)).create(details);
}
}

Integration Tests

ProductControllerTest.java
@ExtendWith(MockitoExtension.class)
public class ProductControllerTest {
@Mock
private ProductService productService;
@InjectMocks
private ProductController controller;
@Test
void shouldCreateProductFromValidRequest() throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode request = mapper.readTree("""
{
"productType": "ELECTRONICS",
"productId": "ELEC123",
"name": "Test Product",
"price": "599.99",
"processor": "Intel i7",
"ram": "16GB",
"storage": "1TB"
}
""");
ResponseEntity<String> response = controller.createProduct(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(productService, times(1)).createProduct(any(), any(ProductDetails.class));
}
@Test
void shouldReturnBadRequestForInvalidType() throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode request = mapper.readTree("""
{
"productType": "INVALID",
"productId": "TEST123"
}
""");
ResponseEntity<String> response = controller.createProduct(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
}

Conclusion

The Product Creation System demonstrates effective use of multiple design patterns to create a flexible, maintainable architecture:

  1. Flexibility: The Strategy pattern allows easy addition of new product types without modifying existing code
  2. Performance: Factory pattern with caching optimizes strategy instantiation
  3. Maintainability: Clear separation of concerns between validation, creation, and API layers
  4. Extensibility: Type-specific attributes handled through specification map rather than fixed fields
  5. Error Handling: Comprehensive exception management at controller level

To extend the system:

  • Add new product type to ProductType enum
  • Create corresponding strategy implementation
  • Add type-specific validation in QualityProcessor
  • Update controller with type-specific attribute handling

The system’s modular design makes it an ideal foundation for building complex product management solutions.```