Back to blog
Dec 27, 2024
5 min read

Dynamic Strategy-Based Processing in Spring Boot - Java Based

build a dynamic strategy-based processing framework in Spring Boot using Java-based configuration

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

  1. Dynamic Behavior:

    • Process requests dynamically based on subcategories and operations defined in Java configuration.
  2. Extensibility:

    • Add new subcategories or operations by modifying the configuration class with minimal changes to core logic.
  3. Best Practices:

    • Use modern Java features like streams, Optional, and record types.
    • Promote type safety and avoid parsing configurations from external files.
  4. Testability:

    • Ensure the system is highly testable with unit and integration tests.

Architecture Diagram

POST Request

Subcategory

Selects

Executes

Sequential Processing

Client

Controller

StrategyFactory

SubcategoryStrategy

Operations

Response


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;
@Configuration
public 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;
@Component
public 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;
@Component
public 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;
@Component
public 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;
@SpringBootTest
class 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

  1. Type Safety:

    • Compile-time validation ensures no misconfigured subcategories or operations.
  2. IDE Support:

    • Autocomplete, navigation, and refactoring support.
  3. 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!