Dynamic Strategy-Based Processing in Spring Boot: A Tutorial with Java-Based Configuration
In this tutorial, we will build a dynamic strategy-based processing framework in Spring Boot using Java-based configuration instead of YAML. This approach ensures better type safety, IDE support, and flexibility while adhering to modern Java practices and design principles.
Overview
Goals of the Framework
-
Dynamic Behavior:
- Process requests dynamically based on subcategories and operations defined in Java configuration.
-
Extensibility:
- Add new subcategories or operations by modifying the configuration class with minimal changes to core logic.
-
Best Practices:
- Use modern Java features like streams,
Optional
, and record types. - Promote type safety and avoid parsing configurations from external files.
- Use modern Java features like streams,
-
Testability:
- Ensure the system is highly testable with unit and integration tests.
Architecture Diagram
Java-Based Configuration
1. Subcategory Configuration
Define subcategories and their operations in a centralized Java configuration class.
SubcategoryConfig.java
package com.example.demo.config;
import com.example.demo.service.Operation;import com.example.demo.strategy.SubcategoryStrategy;import com.example.demo.strategy.impl.AdminStrategy;import com.example.demo.strategy.impl.DeveloperStrategy;import com.example.demo.strategy.impl.ManagerStrategy;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
import java.util.List;import java.util.Map;
@Configurationpublic class SubcategoryConfig {
@Bean public Map<String, SubcategoryStrategy<?, ?>> strategies( ManagerStrategy managerStrategy, AdminStrategy adminStrategy, DeveloperStrategy developerStrategy ) { return Map.of( "Manager", managerStrategy, "Admin", adminStrategy, "Developer", developerStrategy ); }
@Bean public Map<String, List<Operation<?, ?>>> operations(OperationFactory factory) { return Map.of( "Manager", List.of( factory.getOperation("ValidateEmployeeRequest"), factory.getOperation("PrepareEmployeeDetails"), factory.getOperation("FetchEmployeeDetails") ), "Admin", List.of( factory.getOperation("ValidateAdminRequest"), factory.getOperation("FetchAdminDetails") ), "Developer", List.of( factory.getOperation("ValidateDeveloperRequest"), factory.getOperation("FetchDeveloperDetails"), factory.getOperation("PostProcessDeveloperDetails") ) ); }}
2. Strategy Interface and Implementation
2.1. Strategy Interface
Define a generic strategy interface.
package com.example.demo.strategy;
import com.example.demo.model.OperationContext;
public interface SubcategoryStrategy<R, T> { T process(R request);}
2.2. Manager Strategy
package com.example.demo.strategy.impl;
import com.example.demo.model.EmployeeRequest;import com.example.demo.model.OperationContext;import com.example.demo.service.Operation;import com.example.demo.strategy.SubcategoryStrategy;import org.springframework.stereotype.Component;
import java.util.List;
@Componentpublic class ManagerStrategy implements SubcategoryStrategy<EmployeeRequest, String> {
private final List<Operation<EmployeeRequest, String>> operations;
public ManagerStrategy(List<Operation<?, ?>> allOperations) { this.operations = (List<Operation<EmployeeRequest, String>>) allOperations; }
@Override public String process(EmployeeRequest request) { OperationContext<EmployeeRequest, String> context = new OperationContext<>(request);
operations.forEach(operation -> { operation.execute(context); if ("FAILURE".equals(context.getStatus())) { throw new IllegalStateException("Processing failed: " + context.getIntermediateData()); } });
return context.getIntermediateData(); }}
Repeat similar patterns for AdminStrategy
and DeveloperStrategy
.
3. Operation Factory
Dynamically instantiate operations based on their type.
package com.example.demo.factory;
import com.example.demo.service.Operation;import org.springframework.stereotype.Component;
@Componentpublic class OperationFactory {
public Operation<?, ?> getOperation(String operationType) { try { String className = "com.example.demo.service.impl." + operationType; return (Operation<?, ?>) Class.forName(className).getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalArgumentException("Invalid operation type: " + operationType, e); } }}
4. SubcategoryStrategyFactory
package com.example.demo.strategy;
import org.springframework.stereotype.Component;
import java.util.Map;
@Componentpublic class SubcategoryStrategyFactory {
private final Map<String, SubcategoryStrategy<?, ?>> strategies;
public SubcategoryStrategyFactory(Map<String, SubcategoryStrategy<?, ?>> strategies) { this.strategies = strategies; }
@SuppressWarnings("unchecked") public <R, T> SubcategoryStrategy<R, T> getStrategy(String subcategory) { SubcategoryStrategy<?, ?> strategy = strategies.get(subcategory); if (strategy == null) { throw new IllegalArgumentException("No strategy found for subcategory: " + subcategory); } return (SubcategoryStrategy<R, T>) strategy; }}
5. Controller
package com.example.demo.controller;
import com.example.demo.model.EmployeeRequest;import com.example.demo.strategy.SubcategoryStrategyFactory;import org.springframework.web.bind.annotation.*;
@RestController@RequestMapping("/api/use/employee")public class EmployeeController {
private final SubcategoryStrategyFactory strategyFactory;
public EmployeeController(SubcategoryStrategyFactory strategyFactory) { this.strategyFactory = strategyFactory; }
@PostMapping public String handleEmployeeRequest(@RequestBody EmployeeRequest request) { return strategyFactory .<EmployeeRequest, String>getStrategy(request.getSubcategory()) .process(request); }}
Testing the Framework
Unit Test Example for ManagerStrategy
package com.example.demo.strategy.impl;
import com.example.demo.model.EmployeeRequest;import com.example.demo.service.Operation;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;
import java.util.List;
import static org.mockito.Mockito.*;
class ManagerStrategyTest {
private ManagerStrategy managerStrategy; private Operation<EmployeeRequest, String> mockOperation;
@BeforeEach void setUp() { mockOperation = mock(Operation.class); managerStrategy = new ManagerStrategy(List.of(mockOperation)); }
@Test void testProcessSuccess() { EmployeeRequest request = new EmployeeRequest("Manager", "123", "John Doe"); managerStrategy.process(request); verify(mockOperation, times(1)).execute(any()); }}
Integration Test Example
package com.example.demo.controller;
import com.example.demo.model.EmployeeRequest;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTestclass EmployeeControllerIntegrationTest {
@Autowired private MockMvc mockMvc;
@Test void testManagerEndpoint() throws Exception { mockMvc.perform(post("/api/use/employee") .contentType(MediaType.APPLICATION_JSON) .content(""" { "subcategory": "Manager", "employeeId": "123", "employeeName": "John Doe" } """)) .andExpect(status().isOk()); }}
Key Benefits of Java-Based Configuration
-
Type Safety:
- Compile-time validation ensures no misconfigured subcategories or operations.
-
IDE Support:
- Autocomplete, navigation, and refactoring support.
-
Flexibility:
- Easily extend the configuration with Java logic (e.g., conditionally add strategies).
This tutorial provides a type-safe, modern Java approach to dynamic strategy-based processing in Spring Boot, complete with tests for high reliability. Let me know if you’d like further refinements!