Using Call Graphs to Break Down Monoliths into Microservices


Microservices, Monoliths, and Call Graphs

We often get called into organizations to help them plan and execute a transition from monolith-based architectures to microservices-based architectures. One of the hardest pieces, both in terms of planning and execution, is how to separate the various concerns into clean and largely independent modules.

While we have several techniques for code analysis, I want to introduce a new set of tools for everyone. I recently had a challenge to track down which pieces of code eventually touch an external service -- database, REST service, etc. This is easy enough for some simple code, or to find the actual interface points. But if you want to find which service each REST endpoint touches, for example, it can become easy to miss some one-off places. So, like nearly everything else we do, we can automate quite a lot of this!

Call Graphs

I found this interesting tool on GitHub called Java Call Graphs. Along with a dynamic trace utility, it includes a static analyzer. It reads all the classes in a JAR file, then generates a text file that describes the relationships.

For the purposes of this article, we are going to use the venerable Spring PetClinic demo application. After compiling the code, you end up with petclinic-1.5.1.jar in your target directory. Without any customizations, I am going to run the static analyzer on this JAR file. A small sample of the output:

C:org.springframework.samples.petclinic.owner.Owner java.util.Set
C:org.springframework.samples.petclinic.owner.Owner java.lang.String
C:org.springframework.samples.petclinic.owner.Owner java.util.Iterator
M:org.springframework.samples.petclinic.owner.Owner:<init>() (O)org.springframework.samples.petclinic.model.Person:<init>()
M:org.springframework.samples.petclinic.owner.Owner:getPetsInternal() (O)java.util.HashSet:<init>()
M:org.springframework.samples.petclinic.owner.Owner:getPets() (M)org.springframework.samples.petclinic.owner.Owner:getPetsInternal()
M:org.springframework.samples.petclinic.owner.Owner:getPets() (O)java.util.ArrayList:<init>(java.util.Collection)
M:org.springframework.samples.petclinic.owner.Owner:getPets() (O)<init>(java.lang.String,boolean,boolean)

The syntax of the output is on the of the project link above, but the C start is basically a class import statement. The lines starting with M are method calls. So we can see that Owner:getPets() calls Owner:getPetsInternal(), and we can see that getPetsInternal() initializes a HashSet. Due to Java's type erasure issues, it isn't possible for us to determine what the generic types are. Now, this is a lot of output. There are 2549 lines in the output, and it includes things like initializing a StringBuffer and, in the case of uber-jars, the actual framework code as well:

M:org.springframework.boot.loader.PropertiesLauncher:capitalize(java.lang.String) (O)java.lang.StringBuilder:<init>()
M:org.springframework.boot.loader.PropertiesLauncher:capitalize(java.lang.String) (M)java.lang.String:charAt(int)
M:org.springframework.boot.loader.PropertiesLauncher:capitalize(java.lang.String) (S)java.lang.Character:toUpperCase(char)

While this output may be helpful for people doing profiling, it's not very helpful for our issue. We really care about the interactions for the code that we write. Now, I don't really want to modify the jcg code, so I'm going to write a script to post-process the output. So, some things we want to do:

  • Remove all the import statements by piping output through grep -v "^C"
  • Remove classes on the LHS (left-hand side) and RHS (right-hand side).
    • grep -v ' ([IMOS])java.' to get rid of Java standard library calls on the RHS -- with more exclusions
    • grep -v 'M:org.springframework.boot.' to get rid of Spring Boot classes on the LHS.

The entire script is:



cat ${fn}.txt | sort | uniq > ${fn}-phase1.txt

cat ${fn}-phase1.txt | grep -v "^C" > ${fn}-phase2.txt
cat ${fn}-phase2.txt | grep -v 'M:org.springframework.boot.' > ${fn}-phase3.txt
cat ${fn}-phase3.txt | grep -v ' ([IMOS])java.' > ${fn}-phase4.txt
cat ${fn}-phase4.txt | grep -v ' ([IMOS])org.apache.commons.lang' > ${fn}-phase5.txt
cat ${fn}-phase5.txt | grep -v ' ([IMOS])org.slf4j' > ${fn}-phase6.txt
cat ${fn}-phase6.txt | grep -v ' ([IMOS])org.springframework.core' > ${fn}-phase7.txt
cat ${fn}-phase7.txt | grep -v ' ([IMOS])org.springframework.beans' > ${fn}-phase8.txt

mv ${fn}-phase8.txt ${fn}-final.txt
rm -rf ${fn}-phase*.txt

This results in a significantly smaller 97-line file, a solid 96% reduction. This results in mostly interesting lines like so: (M) (M) (M) (I)


Now that we have the interesting calls all in one place, we need to do something with this text. In order to visualize what we have, we need to do a bit more processing on the data. We are going to use the common GraphViz tools to generate pictures from text.


You've probably used GraphViz before without knowing it. It's one of the go-to libraries for visualizing graphs, and is supported in a surprising number of other tools. Notably, OmniGraffle can open a GraphViz text file, and then you can use the power of OmniGraffle to make it pretty. The default rendering in GraphViz can be a bit weird, but we can make a few tweaks to make it better. I don't want to give a whole tutorial on using GraphViz, but a few explanations are in order.

First, to create a node, you simply declare it:

digraph GraphName {
  node1 -> node2

This creates node1 and node2, and an edge from node1 to node2. It's incredibly simple, and this is what it looks like:

 Figure 1

Figure 1

The other thing to note is that if the node name has complex characters, we can enclose the whole thing in double quotes. Therefore, this DOT file will render the same image as above:

digraph GraphName {
  "node1" -> "node2"

Visualizing our Output

Now, to get a picture of our call graph, we can take the output above, wrap each LHS and RHS in quotes, and stick a -> between them. And then wrap the whole document in a DOT template. It results in a file that looks like this:

digraph out {
  node [shape="box"];
"M:org.springframework.samples.petclinic.PetClinicApplication:main(java.lang.String[])" -> "(S)org.springframework.boot.SpringApplication:run(java.lang.Object,java.lang.String[])"
"M:org.springframework.samples.petclinic.model.NamedEntity:<init>()" -> "(O)org.springframework.samples.petclinic.model.BaseEntity:<init>()"

And if we use the tools to render the graph, we get this big graph (figure 2). It's nothing spectacular, but we have a visualization!

 Figure 2

Figure 2

Improving Visualizations

 Figure 3

Figure 3

Now that we have basic visualizations, it's time to try making it a bit more helpful. First, let's get rid of a bunch of the extra syntax in the file, like the M: preceding the method calls, and the (O)-type preceding the method invocations. In addition, since everything here is the PetClinic application, we're going to shorten the org.springframework.samples.petclinic down to just petclinic. This will help the nodes from being too wide. Also, most of the arguments aren't useful to us either. So we're going to convert it into just method references. This has a small risk of causing issues if there is a lot of overloaded methods, but for the sake of this demonstration, we're going to just ignore that issue. The result of this is that the file is the same number of lines, but the rendered image is significantly easier to read
(figure 3)

 Figure 4

Figure 4

Much better, right? But we can go deeper! The dot command is one of several layout options for GraphViz. There are several others, but the one we're going to look at is called fdp. According to the docs, it "draws undirected graphs using a 'spring' model. It relies on a force-directed approach in the spirit of Fruchterman and Reingold (cf. Software-Practice & Experience 21(11), 1991, pp. 1129-1164)." Ok then! Using this layout instead of dot gives us this pretty picture (figure 4).

This reveals quite a few interesting things. There are patches of unreachable code. I confirmed this manually -- the code is only used by the unit tests. Based on how the force-direction is applied, we can already see some unique clusters. Big clusters are ripe candidates for breaking off into microservices, as well as a sign of well-developed software: high cohesion, but low coupling. If we just add some coloring into this based on the package names, we will get this picture (figure 5), which now shows us some patterns. One obvious thing to break apart is that purple cluster of Vets, which is what you would expect from this application. The other thing that jumps out is the coral-colored nodes have too much going on -- they are managing both People and Pets.

 Figure 5

Figure 5


These kinds of tools can be difficult because there is no right answer. This is where your experience, intuition, and observations all come together to help you tease out a migration strategy. Obviously, for this app, we'd want to create a service split for Vets and People/Pets. It's likely, depending on your application, that you'd also want to segment People and Pets. Given their current levels of cohesion, we would first separate them, together as a unit, from the main application, then split them again when we have more time and have reached a new stable position.

No tool will allow us to not think -- but making these tools work for you is a step in the "work smarter, not harder" line of thinking. Use all the tools at your disposal, and learn to really make them your own.



© 2007-2017. All rights reserved. Dev9 Inc.