Easy places to start in refactoring Java to Microservices
Kyle Brown, IBM Fellow, CTO for Cloud Architectures, IBM Cloud Garage
Despite what many in Silicon Valley like to believe, not every application is a green-field. When you work within the constraints of a big enterprise like a bank, an insurance company or an airline, you have to live in a world where there are way more ideas floating around about how things could be done than funding and time to actually accomplish those things. The reality is that enterprises have a lot of existing Java code, and a lot of Java developers, and limited time and money with which to start new projects. It’s simply not economically feasible to throw away all of that Java code and start afresh with all new runtimes and programming languages. Instead, it’s best if we can find the good parts and reuse those in the right framework. That’s why refactoring existing Java applications into Microservices is often the best, and most prudent approach to how to both keep your existing systems running and at the same time move it to a more sustainable and productive development model.
What is refactoring to Microservices?
So what do I mean by refactoring? Refactoring is a well-established term in the software engineering community that is defined as “introducing a behavior-preserving code transformation.” That boils down to keeping your external API’s the same while changing the way in which your code operates or is packaged. Refactoring to Microservices is, therefore, building Microservices into your application without necessarily changing what the application does — you don’t add new functionality to your application, but instead change how its packaged, how the code is structured, and perhaps how the API is expressed. Refactoring to Microservices is not something that’s right for every application, and you can’t always do it successfully, but it’s certainly something that’s worth considering in cases where you can’t throw everything away.
Some simple starting points.
However, as I’ve seen it discussed, refactoring is usually interpreted in a slightly different way — that is, rewriting everything as brand-new cloud-native code. That is a long and often costly endeavor. The amount of risk involved in this, and also the amount of funding and time required for something like this to succeed, that often discourages teams from pursuing it, even if the teams involved know that the benefits would pay off in the long term. So we need to find ways to introduce refactoring without throwing everything away — we have to build skills and confidence in the process while still showing business value. Luckily, there are some simple starting points that provide short-term benefits for relatively little risk and cost. By building skills on a project starting at one of these starting points, you can begin to gain benefits incrementally, and then take on a more complex and involved refactoring effort by building from those skills.
We have seen there are three categories of starting points you can look at; how your application is packaged (and built), how your application code functions, and how your application interacts with back-end data sources.
Starting Point 1: Repackaging your application
The first place to begin is by revisiting your Java application packaging structure and adapting some new packaging practices before you even start to change your code. Throughout the 2000s and 2010s, we all started building ever-larger EAR files to contain our Java applications. We would then deploy those EARs across every application server in our application server farms. The problem is this tied each piece of code in that application to the same deployment schedules, and the same physical servers. If you changed anything you’d have to retest everything, and that made big changes too expensive to consider.
However, with lightweight container technologies and lightweight Java servers like Open Liberty, the economics have changed. Given that, it’s time to start reconsidering the packaging strategy we’ve taken in the past. There are three principles you need to then start applying:
· Split up your EARs: Instead of packaging all of your related WARs in one EAR, you split them up into their independent WARs. Note that this may involve some minor changes to code or more likely to static content if you need to separate application context roots.
· Apply “One Service Per Container”: Next, deploy each WAR in its own Liberty or Tomcat server — preferably in its own container. You can then learn how to scale multiple containers independently, which is a critical skill to gain for Microservices operations.
· Build, deploy and Manage independently: Once you have split your WAR files, you can then begin to manage each WAR independently through its own automated DevOps pipeline. This is a step toward gaining the advantages of continuous delivery and an important way to build skills in managing multiple pipelines.
You can see the effect of applying these three principles in this diagram:
Starting Point 2: Low-cost refactoring
Now that you’re deployment strategy has gotten down to the level of independent WAR’s you can start looking for opportunities to refactor the WAR’s to even more fine-grained levels. Again, we’ve seen three particular cases in which you can find easy opportunities for refactoring your code to get down to a way to package Microservices independently.
Case 1: Existing REST or JMS services — This is by far the easiest case for refactoring. It may be that you have existing services that are already compatible with a Microservices architecture, or that could be made compatible. An example of this kind of application can be seen in this diagram:
Start by untangling each REST or simple JMS service from the rest of the EAR and then deploy each service as its own WAR. At this level, duplication of supporting JAR files is fine — this is still mostly a question of packaging. What this is really teaching you is how to set up multiple DevOps Pipelines to replace the single pipeline (or no pipeline!) for your monolith. That’s a crucial skill that your team will need to master in order to succeed with Microservices. You see an example of this in the diagram below:
This approach has the beneficial side effect of reducing the set of things that have to be tested all at once on a release — so the scope of your release is decreased, which should contribute to your team’s ability to release changes faster.
Case 2: Existing SOAP or EJB services — If you have existing EJB or SOA services, they were probably built following a functional approach (such as the Service Façade pattern). In this case, a functionally based services design can usually be refactored into an asset-based services design that is compatible with REST. This is because in many cases, the functions in the Service façade were originally written as CRUD operations on a single object. In the case where that is true, the mapping to a RESTful interface is straightforward — just re-implement the EJB session bean interface or JAX-WS interface as a JAX-WS interface — you may need to convert object representations to JSON in order to do this, but that’s usually not very difficult, especially where you were already using JAX-B for serializations.
In cases where it’s not a simple set of CRUD operation on a single entity (for instance in a case like an account transfer) then you can apply any of a number of different approaches for constructing RESTful services (such as building simple functional services like /accounts/transfer) that implement variants of the Command pattern. In any case, make sure that you don’t separate things into very fine-grained services at this point — it’s OK if you end up with several REST interfaces in the same microservice, so long as they are related by a common business purpose — that’s the easiest way to resolve some of the more complex issues that would arise with 2PC transactions if you were to split the services out into more finely-grained services.
Case 3: Simple Servlet/JSP interfaces. Many Java programs (particularly older ones) are really just simple Servlet/JSP front-ends to database tables. They may not have what is referred to as a Domain Object Layer at all, especially if they follow design patterns like the Active Record pattern. In this case, creating a domain layer that you can then represent as a RESTful service is a good first step. Identifying your domain objects by applying Domain Driven Design will help you build the pieces of your missing domain services layer. Once you’ve built that (and packaged each new service in its own WAR) then you can either refactor your existing Servlet/JSP app to use the new service or you can build a whole new interface, perhaps with JavaScript, HTML5 and CSS, or maybe as a native mobile application. This is, of the three, the highest-cost and most complex refactoring, but it can at least preserve much of your existing database code and logic at the start. You can tackle the problem undoing coupling at the database at a later date.
Starting Point 3: Low-risk Database refactoring
Repackaging is a great way to start moving toward smaller services, and low-cost refactoring is another way to start breaking apart the monolith. However, after you’ve mastered those, you’ll want to turn your attention to what may be your hardest problem of all in adopting Microservices — refactoring the data structures that your applications are built on. In general, that’s a difficult question that we’ll examine more deeply in another article. But there are a couple of options you can examine in the simplest cases, just to get going:
1. Look for Isolated Islands of Data: Begin by looking at the database tables that your code uses. If the tables used are either independent of all other tables or come in a small, isolated “island” of a few tables joined by relationships — then you can just split those out from the rest of your data design. It may be simplest and easiest to just break those tables out into a new schema in your existing database to start with — that starts the decoupling process and is relatively low-risk.
However, once you have done that, you can consider the best long-term option for your service. For instance, you may decide to stay with SQL, but perhaps consider a move from a heavy-weight Enterprise Database like Oracle to a smaller, self-contained database like MySQL. Or you might consider a NoSQL database to replace your SQL database. Which option you choose depends on the kinds of queries you actually perform on your data. If most of the queries you do are simple queries on primary keys, then a key-value database or a Document Database may serve you very well. On the other hand, if you really do have complex joins that vary widely (e.g. your queries are unpredictable) then staying with SQL may be your best option.
2. Look for opportunities for Table denormalization: If you have more than a few relationships to other tables, you may be able to refactor (or in DBA terms, “denormalize”) your tables. Even bringing this discussion up may be a cause of friction with your DBAs. However, if you take a step back, your team as a whole should think about why data was normalized to begin with. Often, the reason for highly normalized schemas was to reduce duplication, which was to save space, because disk space was expensive. However, that’s simply not true anymore. Instead, query time is now the thing you want to optimize and denormalization is a straightforward way to achieve that. Again, it’s a low-cost, low-risk step in the right direction, even if it just moves the ball forward a few yards.
Wrapping it Up
That should give you at least a taste of what Refactoring to Microservices is about. In the next article, we’ll dive deeper into looking at how the structure of your data can affect how you may want to introduce microservices into your application. The good news is that refactoring your code is not as hard as you may think and in many cases, it’s actually pretty simple. If you work your way through your code looking for these (relatively) simple cases, you may find that the more complex code sections are actually few and far between.
An earlier version of this article had been previously published in IBM DeveloperWorks