Using API Simulation to Build Microservices Alongside a Java Monolith
At OpenCredo and SpectoLabs we’re helping a lot of organisations embrace the microservice architectural style. One problematic pattern we repeatedly see when organisations are migrating from working with a single Java-based monolith to multiple microservices is the development team stumbling with orchestrating multiple services for local development and pipeline-based automated testing.
The goal of this post is to share our practical lessons of incorporating the Hoverfly ‘API simulation’ service virtualisation tool into our local development environments and automated tests. Some readers may already be using record/replay tools like VCR and Betamax, but the big benefit of Hoverfly (which we’ll cover in a later post) is the ability to modify requests and responses in real-time by using configurable ‘middleware’ plugins that can be written in any language.
Let’s create an experimental playground with a fake Java ‘monolith’ and an example microservice. When working with Java microservices, I’m a big fan of the Spring Boot platform, and so this is what we will use…
Building a sample ‘monolith’ macroservice
I have built an example monolith/macroservice and uploaded this to a GitHub repository under my account, ‘hoverfly-blog’. If you want to follow along with this blog post, then please clone the repository and cd into the hoverfly-blog directory.
workspace $ git@github.com:daniel-bryant-uk/hoverfly-blog.git
workspace $ cd hoverfly-blog
hoverfly-blog $ ls -lsa
total 16
0 drwxr-xr-x 7 danielbryant staff 238 17 Feb 16:21 .
0 drwxr-xr-x 43 danielbryant staff 1462 16 Feb 15:02 ..
0 drwxr-xr-x 12 danielbryant staff 408 17 Feb 16:30 .git
8 -rw-r--r-- 1 danielbryant staff 96 15 Feb 14:48 .gitignore
8 -rw-r--r-- 1 danielbryant staff 3843 17 Feb 16:21 README.md
0 drwxr-xr-x 7 danielbryant staff 238 16 Feb 15:05 macroservice
0 drwxr-xr-x 10 danielbryant staff 340 17 Feb 16:21 microservice
The ‘monolithic’ application in the macroservice folder is in fact a lightweight Spring Boot service that I’m only using to prove the concepts of API simulation. However, you can use your imagination and pretend that the codebase contains a canonical data model (CDM), seven God objects, five methods of sending email, and fourteen different logging frameworks…
The application within the ‘microservice’ is another Spring Boot application, which will act as our proper microservice.
Let’s get started…
Introduce seams to the monolith
I’ve included the macroservice ComplexController code below, which shows we have a single ‘slowFragileRequest’ HTTP API endpoint. A common pattern we have used when breaking apart a large application is to create a API ‘seam’ like this within the monolith, which can be used by new microservices for making queries or issuing commands. In this example we are going to pretend that ‘slowFragileRequest’ endpoint returns data from an expensive query run within the monolith:
@Controller
public class ComplexController {
@Autowired
private ComplexObjectService complexObjectService;
@RequestMapping("/slowFragileRequest")
@ResponseBody
public List slowFragileRequest(@RequestParam(value = "origin", required = false) final String origin,
@RequestParam(value = "destination", required = false) final String destination) {
//todo - note that the request params are just for show, and are ignored in this example
return complexObjectService.getLatestComplexObjects();
}
}
Creating a microservice to integrate with the seam
We have found that although mocking and stubbing are very useful techniques, as you pull apart a monolith you will need to test against the actual application at some point. This often occurs when building microservices locally, and almost always within the integration testing phase of the build pipeline.
To compound the difficulties of spinning up the monolith, sometimes we simply haven’t had access to the source code or even the binary, and here we have had to request time (via a ticketing system) and lease an environment that contained the monolith. The ultimate challenge occurs when the monolith has throughput/throttling limits or constantly falls over.
The ComplexObjectService code shown below comes from the microservice application, and uses the Spring RestTemplate to make requests against the monolith endpoint described above.
@Service
public class ComplexObjectService {
@Autowired
private RestTemplate restTemplate;
public List getLatestComplexObjects(final String origin, final String destination) {
Map urlVariables = new HashMap<>();
urlVariables.put("origin", origin);
urlVariables.put("destination", destination);
ComplexObject[] objects =
restTemplate.getForObject("http://localhost:8080/slowFragileRequest", ComplexObject[].class, urlVariables);
// Process ComplexObjects into something else
return Arrays.asList(objects);
}
}
The problem of working with an unavailable monolith
We can now spin up both our fake monolith and supporting microservice, and make calls to the microservice, which in turn makes a call to the seam within the monolith.
For convenience we are using the Maven Spring Boot plugin to run our applications, but you would want to use something more robust for production use (i.e. an executable JAR launched with a supervisor process):
hoverfly-blog $ cd macroservice
macroservice $ mvn spring-boot:run
…
2016-02-16 15:09:05.399 INFO 97776 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-02-16 15:09:05.467 INFO 97776 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-02-16 15:09:05.471 INFO 97776 --- [ main] i.s.exmpls.macroservice.Application
I recommend opening another terminal session/window, and then launching the microservice:
hoverfly-blog $ cd microservice
microservice $ mvn spring-boot:run
….
2016-02-16 15:09:40.861 INFO 97992 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2016-02-16 15:09:40.931 INFO 97992 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http)
2016-02-16 15:09:40.936 INFO 97992 --- [ main] i.s.exmpls.microservice.Application : Started Application in 2.354 seconds (JVM running for 5.645)
Now open up one more terminal window and make a request against the microservice using curl or your favourite REST tool/browser:
hoverfly-blog $ curl localhost:8090/speedyRequest
[{"id":"1","name":"largeObject","mappings":["one","two"]}]
If you look at the logging output for both the microservice and monolith/macroservice you will see that both applications dealt with their respective requests.
Now kill the monolith (using ^C / SIGINT etc in the terminal windows in which this is running)
2016-02-16 15:09:05.471 INFO 97776 --- [ main] i.s.exmpls.macroservice.Application : Started Application in 2.173 seconds (JVM running for 4.735)
^C
macroservice $
Try calling the microservice endpoint again:
hoverfly-blog $ curl localhost:8090/speedyRequest
{"timestamp":1455635508958,"status":500,"error":"Internal Server Error","exception":"org.springframework.web.client.ResourceAccessException","message":"I/O error on GET request for \"http://localhost:8080/slowFragileRequest\": Connection refused; nested exception is java.net.ConnectException: Connection refused","path":"/speedyRequest"}
Boom! We can no longer test our microservice due to the dependency on the macroservice HTTP endpoint for downstream communication.
Let’s introduce some API simulation with Hoverfly…
Shut everything down (^C in both macroservice and microservice), and start the monolith again
macroservice $ mvn spring-boot:run
Now start the microservices, but this time run the application using the TEST profile.
microservice $ mvn spring-boot:run -Dspring.profiles.active=TEST
The primary difference with the TEST profile is that all communication via the Spring RestTemplate will now be made through a proxy - this is the Hoverfly proxy that we will be starting in a moment. (It’s worth noting that this way of programmatically forcing communication via a proxy is quite invasive, and we could use other techniques)
@Bean
@Profile("TEST")
public RestTemplate getHoverflyProxiedRestTemplate() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(hoverflyHost, hoverflyPort));
requestFactory.setProxy(proxy);
return new RestTemplate(requestFactory);
}
@Bean
@Profile("PROD")
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
Let’s now start our Hoverfly proxy. I have downloaded an appropriate Hoverfly binary from the project’s Github release page, and placed this in the microservice directory. I have also made sure the Hoverfly binary is executable ($ chmod u+x hoverfly) and excluded from git (using .gitignore) the binary and the associated ‘requests.db’ file that stores Hoverfly state.
If I list the microservice directory this is what I have. If you are following along, then please make sure your directory contains the same content:
microservice $ ls -lsa
total 21112
0 drwxr-xr-x 10 danielbryant staff 340 15 Feb 15:23 .
0 drwxr-xr-x 6 danielbryant staff 204 15 Feb 14:48 ..
0 drwxr-xr-x 12 danielbryant staff 408 16 Feb 15:15 .idea
8 -rw-r--r-- 1 danielbryant staff 44 15 Feb 15:23 application.properties
21016 -rwxr--r-- 1 danielbryant staff 10758992 12 Feb 17:08 hoverfly
16 -rw-r--r-- 1 danielbryant staff 5157 15 Feb 15:01 microservice.iml
8 -rw-r--r-- 1 danielbryant staff 1275 15 Feb 14:57 pom.xml
64 -rw------- 1 danielbryant staff 32768 16 Feb 12:07 requests.db
0 drwxr-xr-x 4 danielbryant staff 136 15 Feb 14:54 src
0 drwxr-xr-x 7 danielbryant staff 238 16 Feb 15:09 target
We can now start Hoverfly in ‘capture’ mode where it will record all requests and responses made via the application as a proxy
microservice $ ./hoverfly -capture
{"databaseName":"requests.db","level":"info","msg":"Initiating database","time":"2016-02-16T15:20:03Z"}
{"Destination":".","Mode":"capture","ProxyPort":"8500","level":"info","msg":"Proxy prepared...","time":"2016-02-16T15:20:03Z"}
{"AdminPort":"8888","level":"info","msg":"Admin interface is starting...","time":"2016-02-16T15:20:03Z"}
[negroni] listening on :8888
Now all of our communication between microservices and macroservice is being recorded! Lets test our microservice again:
hoverfly-blog $ curl localhost:8090/speedyRequest
[{"id":"1","name":"largeObject","mappings":["one","two"]}]
All good. Now let’s stop Hoverfly and start it again in the default ‘virtualize’ playback mode
microservice $ ./hoverfly
Let’s kill the monolith/macroservice again.
^C
2016-02-16 15:23:04.313 INFO 98315 --- [ Thread-2] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@787c751a: startup date [Tue Feb 16 15:13:51 GMT 2016]; root of context hierarchy
2016-02-16 15:23:04.316 INFO 98315 --- [ Thread-2] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 09:14 min
Now, make the microservice call again:
hoverfly-blog $ curl localhost:8090/speedyRequest
[{"id":"1","name":"largeObject","mappings":["one","two"]}]
Everything still works - even without the monolith running! If we look at the logging output on Hoverfly we can see that it served the request targeted at the monolith, rather than the (non-running) monolith replying itself:
{"bodyLength":58,"destination":"localhost:8080","key":"7e5f6ff30f2adcf5d402e7bbbaf632db","level":"info","method":"GET","middleware":"","mode":"virtualize","msg":"Response found, returning","path":"/slowFragileRequest","rawQuery":"","status":200,"time":"2016-02-16T15:25:32Z"}
Incorporating Hoverfly into the testing workflow
The above example demonstrates how requests against an unavailable or unreliable monolithic application (or indeed any internal or third-party service) can be recorded and replayed when working locally with a dependent application. Now let’s describe a simple process to incorporate the use of Hoverfly into automated testing via JUnit.
Creating a Hoverfly JUnit Rule
We’ll cover the process of automatically capturing requests/response with Hoverfly in a future blog post, but for the moment we will assume that we have captured all of the traffic we need for the API simulation through manual testing.
We could create a Maven plugin to automatically execute the Hoverfly binary during Failsafe integration testing, but for the moment let’s keep this simple by starting the binary via a Hoverfly JUnit Rule that extends the ExternalResource abstract class.
public class HoverflyRule extends ExternalResource {
private static final String HOVERFLY_LOCATION = "/hoverfly-blog/microservice/";
private static Process hoverflyProcess;
@Override
protected void before() throws Throwable {
ProcessBuilder builder = new ProcessBuilder()
.inheritIO()
.command(HOVERFLY_LOCATION + "hoverfly");
hoverflyProcess = builder.start();
}
@Override
protected void after() {
hoverflyProcess.destroy();
}
}
Please note the that the use of a static HOVERFLY_LOCATION variable is only for convenience in this example, and we would ideally inject this value in at runtime.
Once we have created this rule we can create simple tests using the example below as a template:
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles(profiles = "TEST")
@SpringApplicationConfiguration(Application.class)
@WebIntegrationTest
public class ShinyMicroIntegrationTest {
public static final String SPEEDY_REQUEST_URI = "http://localhost:8090/speedyRequest";
@ClassRule
public static TestRule hoverflyRule = new HoverflyRule();
private RestTemplate restTemplate = new RestTemplate();
@Test
public void test() {
ComplexObject[] complexObjects =
restTemplate.getForObject(SPEEDY_REQUEST_URI, ComplexObject[].class);
assertTrue(complexObjects.length == 1);
}
}
If you aren’t familiar with Spring Boot, then the annotations at the top of the Class cause the application to be fully started with an in-memory application server (@WebIntegrationTest,taking the Spring configuration from the Application.class and running with the TEST profile (@ActiveProfiles(profiles = “TEST”))) via the Spring JUnit runner.
The full microservice test suite can be run via Maven:
microservice $ mvn clean verify
...
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] --- maven-jar-plugin:2.5:jar (default-jar) @ microservice ---
[INFO] Building jar: /Users/danielbryant/Documents/dev/daniel-bryant-uk/hoverfly-blog/microservice/target/microservice-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.3.2.RELEASE:repackage (default) @ microservice ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.073 s
[INFO] Finished at: 2016-02-17T18:42:39+00:00
[INFO] Final Memory: 28M/326M
[INFO] ------------------------------------------------------------------------
In conclusion
When decomposing a monolithic Java application it is often good practice to introduce ‘seams’ as API endpoints that newly created microservices can call. The challenge, however, is running the monolith locally, or getting access to a test environment with the monolith running. API simulation, using a tool like Hoverfly, can help here. The above examples explain the basic concepts of using Hoverfly with two Java applications, but the real fun starts when we start introducing Hoverfly ‘middleware’, which can be used to simulate a variety of failure scenarios, or even create an entirely synthetic service response.
References
“Testing Strategies in a Microservice Architecture” by Toby Clemson
“Working Effectively with Legacy Code” by Michael C. Feathers
“Building software against the meetup API whilst 20,000ft above the North Sea” by Mark Coleman
“API mocking for development and E2E testing – Part 1: from zero to hero” by Karolis Rusenas