Case Study: Refactoring a Monolith Into A Cloud-Native Application (Part 2)

October 1, 2015 Jared Gordon

sfeatured-clouds-monolithsThis is the second installment of a series of articles, this post covers the migration of an existing monolithic application into a federation of related microservices. Part 1 covered the context for the case study, explained our methodology, and implemented the changes needed to get the app running on Pivotal Cloud Foundry. Please check out that blog post for helpful background information. For the impatient, feel free to just dive right in at this point—we’ll get into the details of the component identification, API design, refactoring, and more.

Where We Left Off

Based on what we accomplished in Part 1, we have a running application, but one that is fundamentally unchanged from where we started beyond minimal work to refactor and migrate the app to run on Pivotal Cloud Foundry. The baseline we will be starting from for this post is the endpoint of the previous blog.

Justification

Each round of changes needs to be justified in terms of business value, and we are stating these up front.

The SpringTrader application is meant to mimic a financial trading environment by allowing users to purchase and sell securities. User holdings are then subjected to randomized changes, as if by market forces. However, many of SpringTrader’s users have experience with real-time trading systems, and they expect a more realistic representation. The application would be more credible and have greater impact if we were able to make use of real-time market data.

Goals:

With real-time scenarios in mind, our key goals for this round of changes are:

  • Replace the existing simulated Quote data with actual, real-time market data
  • Minimize changes to the existing code base
  • Make sure we have a working system when our refactor has been completed

Our Approach

To access market data, we built a new QuoteService using the Yahoo Finance APIs, which provide near-real-time market data (15 minute delayed) via REST APIs.

Thinking about possible implementation alternatives, our design goals were informed by the following considerations:

  • We might try to simply exchange the existing service code embedded within the monolith, but…
  • We are building this new service based upon a third-party API, and we need to insulate our monolith against any potential, external API changes over which we have no control.
  • We would like our new service to provide a generic API for potential use beyond our current SpringTrader requirements.
  • The technology stack we would like to use for the new service is different enough from the existing SpringTrader stack that attempting to shoehorn it back into the monolith might be difficult and add risk.

The New Quote Service

Based on these considerations, we created a new externalized Quote Service, which can be found here. For details concerning how it was implemented, please refer to its readme.

There is a significant benefit in extracting the Quote Service from the monolith—it frees us to choose whichever technology stack makes sense to us based on our requirements, without regard to potential ripple effects within the existing code. Thus we are able to take advantage of JDK 8, Spring Framework 4, Spring Boot, and Spring Cloud in the new service. As a reminder, the monolith is currently pegged to JDK7 and Spring 3.

Making Use of our New Quote Service

Before we go into detail, here is the high-level process we used to refactor the monolith so it can interact with the new service:

  1. Locate the code that represents the functionality that we will be refactoring.
  2. Identify a service interface that will be the “go-to” for rest of the monolith.
  3. Use the proxy pattern and modify existing code so that it exclusively funnels through this service interface
  4. Create an implementation of the interface for the monolith to talk to our new microservice, and provide code to bridge any “impedance mismatch” between the new and old service semantics.
  5. Point the monolith to the new service via the new implementation, then test and verify.

The Solution Details

To see what was done in this round of changes, please refer to the diff between the part1 and part2 branches, representing the Part 1 and Part 2 solution end-states. As in the previous post, the readme for the branch goes into greater technical detail than we can cover here in the blog.

Now, let’s discuss the steps listed above in a little more detail.

1. Locate the Code

Establishing the boundaries of our new service is key. SpringTrader is built around the notion of Spring controllers focused on defined business functionality: we have a good starting point for our API. We will proceed outward from the existing Quote class and its related services and repositories, then modify as needed.

2. Identify the Service Interface

We have an existing QuoteService interface. If we create a new implementation of this existing interface it should all just work cleanly within the existing monolith, right? Well, in theory.

3. Use the Proxy Pattern

The proxy pattern is a well-known approach that we will be leveraging to pry our service out of the monolith. There are many good descriptions of this pattern on line—for more information refer to this helpful summary. For our purposes, we will use this pattern to swap out the existing, underlying service implementation and replace it with a new implementation that calls our new service.

But, there is some “leakage” that we need to identify and remediate. We need SpringTrader to talk to our service via its service interface exclusively, but there are many places in the code where some components are making end-runs around the interface and speaking to the JPA repository directly. The result is that the underlying database semantics are exposed (via embedded SQL).

To see what was necessary to corral the existing code behind the interface, refer to this commit. Then this commit was needed to further consolidate service functionality under the updated interface. And then this one too.

In a perfect world, design theory and practice would alway be in alignment, but in the case of this application (and probably most existing legacy applications) we ran into places where things have gone a bit entropic. We managed this by:

  • Focusing on a single functional area to fix (no “big bang” approach)
  • Changing only what was needed to accomplish our current goals
  • Relying on our test cases to catch regression issues
  • Working incrementally, and making sure we have a working application at all times

4. Create an Implementation of the Interface

As we worked through our changes we took the opportunity to modify the service interface and rationalize it into a coherent API. These refinements can often take several iterations.

With the service functionality safely contained behind the service interface we can now create a new QuoteServiceImpl class to speak to our externalized service. This class is backed by a new QuoteRepository implemented using Feign from the Netflix OSS project.

5. Point the Monolith to the New Service

The proxy pattern allows us to simply exchange one implementation for another, and the final series of commits in the part2 branch finalize this change. Spring Cloud Connectors are being used to hook SpringTrader to the new service via a User Provided Service (UPS) created in the deployment script on line 10. For further reference, Spring-Cities is another application that makes use of this approach.

However, while this UPS helps accomplish our immediate goals for Part 2, it will be replaced in Part 3 of the blog, for reasons that will soon become apparent.

Where We Stand at the End of Part 2

There were some pretty extensive modifications to extract our first microservice. Arguably, the majority of these were needed to address existing anti-patterns. But we prevailed and were able to hooked up our monolith to a shiny new microservice.

Summary:

  • We identified, implemented, and updated the QuoteService interface
  • Using the proxy pattern, we performed some remedial refactoring of the monolith to enforce the use of our new interface.
  • We created a new real-time QuoteService implementation and pointed the monolith to it.

Great! However, we need to acknowledge that we’ve added new technical debt to the project.

Specifically, we have replaced in-process functionality with remote service calls. What happens if that new service goes down or is otherwise unavailable? This is a major consideration and will be the focus of Part 3 of our blog.

Our technical debt currently stands as:

  • Need to upgrade from JDK 7 to JDK 8 (from Part 1)
  • Need to refactor dependency management and revisit library versions (from Part 1)
  • Need to provide resiliency now that our application is distributed

Stay tuned for Part 3, and we will address this last bullet point.

Thanks to the following folks who provided valuable contributions to this blog series—Michael Cote, Cornelia Davis, Brian Dussault, Joshua Kruck, Scott Frederick, and Duncan Winn.

Learning More:

Find more Cloud Foundry blog articles

About the Author

Biography

Previous
What Is a Value Proposition and Why Do You Need One? (Part 1)
What Is a Value Proposition and Why Do You Need One? (Part 1)

This is the first in a six-post series that outlines a step-by-step process to define your value propositio...

Next
How to Define Your Value Proposition
How to Define Your Value Proposition

In 2014, Pivotal Labs hosted over 50 sessions of Product Office Hours, an hour of free advice to help entre...