Dependency injection pattern for cleaner business logic in your Java Spring application

Viktor Reinok
5 min readFeb 3, 2023

Here you will learn more about dependency injection and use of it in Spring Boot.

  1. Hide business logic behind object-oriented concepts
  2. Use dependency injection to retrieve relevant business context
  • Simple injection
  • Dependency injection with for different domains
  • AOP approach
  • Spring bean scope Prototype
  • How will it perform with new Java lightweight threads

3. Evaluate the performance implication of different DI approaches

2.1 Let's dive into the topic with a simple example.
There are differences in traffic regulations in different countries although the majority is the same. Why not express only the differences and hide common stuff? Let’s use OOP for that.

@Test
public void restCallWithHeaderXCountryWithValueCH() throws Exception {
mockMvc.perform(get("/")
.header("X-Country", "CH"))
.andExpect(status().isOk())
.andExpect(content().string("Speed limit is 50 km/h in city and 120 km/h on highway"));
}

@Test
public void restCallWithHeaderXCountryWithValueDE() throws Exception {
mockMvc.perform(get("/")
.header("X-Country", "DE"))
.andExpect(status().isOk())
.andExpect(content().string("Speed limit is 50 km/h in city and 200 km/h on highway"));
}

Implementation:

@RestController
public class TestController {

private final BeanFactory beanFactory;

public TestController(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}

private SpeedLimitService getSpeedLimitService() {
final Country country = CountryHolder.getCountry();
return beanFactory.getBean(SpeedLimitService.class.getSimpleName() + country.name(), SpeedLimitService.class);
}

@RequestMapping
public String index() {
return "Speed limit is " + getSpeedLimitService().getCitySpeedLimit() + " km/h in city and " + getSpeedLimitService()
.getHighwaySpeedLimit() + " km/h on highway";
}

}
@Component
public class CountryFilter implements Filter {

@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest req = (HttpServletRequest) request;
final String xCountryHeader = req.getHeader("X-Country");

// check if xCountryHeader is black and throw exception
if (ObjectUtils.isEmpty(xCountryHeader)) {
throw new CountryNotDefinedException("Header X-Country is missing");
}

Country country;
try {
country = Country.fromString(xCountryHeader);
} catch (IllegalArgumentException e) {
throw new CountryNotSupportedException("Country not supported. " + xCountryHeader);
}

CountryHolder.setCountry(country);

chain.doFilter(request, response);
}
}

Quite some boilerplate, as we had to pull down the logic that decides which bean to select from the Spring context.

2.1 In order further reduce boilerplate let's extract a factory pattern.

@Component
public class SpeedLimitServiceFactory {
private final SpeedLimitServiceCH speedLimitServiceCH;
private final SpeedLimitServiceDE speedLimitServiceDE;

public SpeedLimitServiceFactory(
SpeedLimitServiceCH speedLimitServiceCH,
SpeedLimitServiceDE speedLimitServiceDE) {
this.speedLimitServiceCH = speedLimitServiceCH;
this.speedLimitServiceDE = speedLimitServiceDE;
}

public SpeedLimitService getSpeedLimitService() {
Country country = CountryHolder.getCountry();
if (country == null) {
throw new IllegalStateException("Country not set in CountryHolder");
} else if (country == Country.CH) {
return speedLimitServiceCH;
} else if (country == Country.DE) {
return speedLimitServiceDE;
} else {
throw new IllegalArgumentException("Unsupported country: " + country);
}
}
}
@RestController
@RequestMapping("/scenario1")
public class Scenario1Controller {

final SpeedLimitServiceFactory speedLimitServiceFactory;

public Scenario1Controller(SpeedLimitServiceFactory speedLimitServiceFactory) {
this.speedLimitServiceFactory = speedLimitServiceFactory;
}

@RequestMapping
public String index() {
SpeedLimitService speedLimitService = speedLimitServiceFactory.getSpeedLimitService();

return "Speed limit is " + speedLimitService.getCitySpeedLimit() + " km/h in city and " + speedLimitService
.getHighwaySpeedLimit() + " km/h on highway";
}

}

2.3 AOP

In order to avoid factory patterns and to further reduce some boilerplate we could use naming conventions to choose the desired bean. We were not able to squeeze that behaviour out of the default DI provided by Spring so, we created our own annotation and an AspectJ aspect.

@RestController
@RequestMapping("/scenario3")
public class Scenario3Controller {

@AutowiredCustom
public SpeedLimitService speedLimitService;

@RequestMapping
public String index() {
return "Speed limit is " + speedLimitService.getCitySpeedLimit() + " km/h in city and " + speedLimitService
.getHighwaySpeedLimit() + " km/h on highway";
}

}
@Aspect
@Component
public aspect AutowiredCustomFieldAspect implements ApplicationContextAware {

private static ApplicationContext applicationContext;

pointcut annotatedField(): get(@performance.annotation.AutowiredCustom * *);

before(Object object): annotatedField() && target(object) {
try {
String fieldName = thisJoinPoint.getSignature().getName();
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);

String className = field.getType().getSimpleName() + CountryHolder.getCountry().name();

Object bean = applicationContext.getAutowireCapableBeanFactory().getBean(className);

field.set(object, bean);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

}

2.4 Spring bean scope Prototype

So, the idea was to use Prototype scope and Conditional to drive contextual behaviours.

@Service
@Scope(value = org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST)
@Conditional(CountryCH.class)
public class SpeedLimitServiceProtoCH implements SpeedLimitServiceProto {

@Override
public int getHighwaySpeedLimit() {
return 120;
}
}

Unfortunately, this idea flopped as the Conditional was not assigned during the bean creation for each request. It would have been interesting to check how it performs with virtual threads.

3. Is there overhead?

Let's run a load test across those 3 designs. 10000 requests while ramping up parallelism. Also, we’ll do two sets of experiments: With and without Project Loom goodies.

3.1 Without virtual threads

3.2 With virtual threads

Conclusion: Performance & overhead

The difference between different approaches is almost not noticeable. What I found interesting is that there is absolutely no overhead while using less boilerplate AOP approach. In fact, it came out first. The deviation is greater when virtual threads are turned on.

Although, as we might have not expected virtual threads were slower than the vanilla threading model. Write me a comment to rerun my tests as I was watching YouTube videos during the last experiment in the evening. :)

And ofc. feel free to rerun the experiment as all the prerequisites are provided. #RRR

Repository

You are able to find all the experiments mentioned above in the following repository. FYI! Gatling performance tests are included as well. https://github.com/vikreinok/spring-boot-performance

Update!!! A long-awaited update.

And indeed! Virtual threads were much much faster!

Without virtual thread
With virtual thread

--

--