Implementation of the Decorator Pattern to Accommodate Mocking in a Spring Boot Application

Alexander Stadler
Cloud Workers
Published in
7 min readFeb 5, 2024

Recently, a client had a pressing requirement. For testing purposes our application should return mock responses for a specific request when the application is in test environment, but not in production environment. While the initial impression was that this was a simple task, creating a clean and scalable solution proved to be more challenging than anticipated. Which solution adheres to the open-close principle to provide a clean (mock-) architecture? Let’s discover this in the following article.
— written with Steffen Jäckel as co-author

Imagine planning a ski weekend with two ski slopes at your disposal, conveniently named DEV and PROD. On the DEV slope, you’re eager to experiment by equipping your skis with the latest technology — sensors for tracking speed, digital displays for navigation, or whatever else you can imagine. Contrastingly, on the PROD slope, your focus shifts to reliability and safety. You seek the comfort and predictability of conventional, tried-and-tested skis. In an ideal world, you wouldn’t want the hassle of switching between two different sets of skis each time you change slopes. What if you could seamlessly and swiftly modify the features of your skis to suit the slope you’re on?

This image was created with the assistance of DALL·E 2

Just as you’d prefer to modify your skis on the go, software objects in certain environments benefit from being dynamically ‘decorated’ with additional responsibilities or behaviors. This is where the decorator pattern comes in to extend the functionality of objects at runtime, without altering their structure in the underlying class system. It’s about adding those ‘high-tech’ features to your basic ‘ski’ object when needed, ensuring that when the circumstances change, your software can adapt just as fluidly as you would on the ski slopes.

This image was created with the assistance of DALL·E 2

In the following sections, we’ll delve into a similar problem and how we were elegantly able to solve it using the decorator pattern.

Requirement

Our development team was recently tasked with an urgent and challenging requirement: to test the memory limitations of a specific request by scaling it to a predetermined magnitude. The objective was to expand the response by scaling it up to an arbitrary number of items. The standard response, was to be scaled our Spring Boot application was evaluated for feasibility to meet this requirement.

Initial Approach

Our initial approach was simple. Create a MockService class and redirect the request from the RealService. The MockService class implemented a conditional check to determine the environment and whether the request, referred to as MockRequest, was the target for mocking. In the test environment, if the MockRequest was received, the request was forwarded to the RealService, the response was retrieved, and the data was scaled to an arbitrary number of items. Conversely, in the production environment or if the received request was not the MockRequest, the request was forwarded to the RealService without modification and the response was returned immediately.

Diagram of the initial Solution

Problem

The limitations of the initial solution were identified during the evaluation process. Firstly, the approach was not scalable, as it would not accommodate future requirements for mocking from other customers. Secondly, the code of the MockService would be present in production, even if mocking was inactive, leading to a suboptimal solution. To address these issues, alternative solutions were proposed through a collaborative brainstorming session.

The next proposed solution was to implement inheritance, where the MockService would inherit from the RealService. The environment would determine which bean (MockService or RealService) was injected into the Controller. This could for example be done using @Conditional or @Profile. However, this approach was deemed inappropriate due to the principle of favoring composition over inheritance, and it did not resolve the scalability issue. Further brainstorming led to the identification of a more suitable solution.

Diagram of the solution with inheritance

Solution: The Decorator Pattern

The new solution involved the implementation of the decorator pattern, a structural design pattern that enables the addition of behavior to an object without affecting other objects of the same class. The decorator pattern extends the functionality of an object by wrapping it with a decorator object that includes additional logic.

In our scenario, the behavior of the RealService was to be augmented by applying mock-decorators to it. The implementation of the decorator pattern involved the creation of a Service interface, followed by an abstract class named MockDecorator. Both the abstract class and the RealService needed to implement the Service interface, with the abstract class also having a reference to the RealService. Concrete decorators were then created, extending the abstract class and overriding its method(s).

Diagram of the final solution using the decorator pattern

The implementation of the decorator pattern offered several benefits. Firstly, it did not necessitate any modifications to the RealService or the controller, in accordance with the open-close principle which stipulates that software entities should be open for extension but closed for modification. Secondly, the mock-decorators could be named in a manner that was specific to each use case, such as MockADecorator or MockAForClientADecorator for Client A’s logic. Finally, the addition of another mock logic to the RealService could be achieved through the simple wrapping of another decorator around the service.

The final step was to specify which mock-decorators should be used by Spring Boot in each environment. This was achieved through the utilization of the @ConditionalOnProperty annotation in Spring, which allowed for the creation of the appropriate bean for each environment. For the test environment, a flag was set to true to indicate that the RealService was to be wrapped in the mock-decorators, whereas in the production environment, the flag was set to false to indicate that only the RealService was to be used.

The following is a simple code example how to implement that setup in your project.

  1. Create the configuration that decorates your service depending on a property that can be set for each stage.
@Configuration
public class MockServiceConfig {

@Autowired
private AutowireCapableBeanFactory autowireCapableBeanFactory;

// Create a Bean with mock decorators e.g. in test environment
@Bean
@ConditionalOnProperty(prefix = "mock", name = "enabled", havingValue = "true")
public ServiceInterface realServiceWithMockDecorator() {
ServiceInterface realService = autowireCapableBeanFactory.createBean(RealService.class);

return new MockServiceDecoratorA(new MockServiceDecoratorB(realService));
}

// Create a Bean without mock decorators e.g. in production environment
@Bean
@ConditionalOnProperty(prefix = "mock", name = "enabled", havingValue = "false")
public ServiceInterface realServiceWithoutDecorator() {
return autowireCapableBeanFactory.createBean(RealService.class);
}
}

In your application.properties file set the variable to true or false depending on stage

mock.enabled=true

2. Create the RestController that uses the ServiceInterface.

@RestController
public class Controller {

private final ServiceInterface service;

@Autowired
public Controller(ServiceInterface service) {
this.service = service;
}

@GetMapping("/")
public String receiveRequest(@RequestParam(name = "request") String request) {
return service.returnResponseObject(request);
}
}

3. Create the ServiceInterface.

public interface ServiceInterface {
String returnResponseObject(String request);
}

4. Create the RealService.

public class RealService implements ServiceInterface{
@Override
public String returnResponseObject(String request) {
return "Real Response";
}
}

5. Create the abstract class that implements the ServiceInterface. Any method that should not be decorated can simply be implemented here by returning the service-call.

public abstract class MockServiceDecorator implements ServiceInterface {
public final ServiceInterface realService;

public MockServiceDecorator(ServiceInterface realService) {
this.realService = realService;
}
}

6. Implement the decorator by extending the abstract class. In this decorator you can implement your logic for decorating specific (or all) requests. In this example all requests that match “mockA” will be decorated. Other requests will be returned without modification.

public class MockServiceDecoratorA extends MockServiceDecorator {
public MockServiceDecoratorA (ServiceInterface realService) {
super(realService);
}

@Override
public String returnResponseObject(String request) {
// If request matches given value return the real response + mock; else return only the real response
if (request.equals("mockA")) {
String response = realService.returnResponseObject(request);
return response + " MockA";
}
return realService.returnResponseObject(request);
}
}

7. Implement as many more decorators as needed.

public class MockServiceDecoratorB extends MockServiceDecorator {
public MockServiceDecoratorB (ServiceInterface realService) {
super(realService);
}

@Override
public String returnResponseObject(String request) {
// If request matches given value return the real response + mock; else return only the real response
if (request.equals("mockB")) {
String response = realService.returnResponseObject(request);
return response + " MockB";
}
return realService.returnResponseObject(request);
}
}

Conclusion

And there we have it. Circling back to our skiing analogy, the picture becomes clear: much like seamlessly adding high-tech enhancements to your skis on the DEV slope, our implementation allows for the effortless integration of additional features during the development phase. Conversely, on the PROD slope, you can confidently ski with the assurance of safety and stability, free from the complexities of the developmental add-ons. This approach eliminates the need for cumbersome switches — or in other words, not having to change skis.

--

--