Manage the monolith with JDepend

The code spaghetti problem

When an application code base is growing it may become complicated to maintain, cause understanding the whole thing requires more and more cognitive effort and the code entropy tends to grow exponentially with time.

The classical approach to address this problem is to make the application more modular : the whole application is split in several part that have clear boundary and dependencies between each other : let’s call it a module. It makes much easier to figure out the impact of adding or modifying a functionality. Also each module can be understood without having the the whole system in mind. As a consequence, teams can be split to concentrate on a subset of the system.

Modularity can be achieved at different level :

Process as module :  The application is split in several smaller applications, each running in its own process and communicating each other through network. That is the micro-services approach. It offers highest isolation but may be considered as extreme in regards of technology shift and productivity overhead it implies (see controverse).

Project as module : The application still run in a single process but the code is split in several projects, each standing for a library with well defined interface. IDE can enforce dependency rules between sub-projects, preventing developers to break dependency scheme. The cons is that it makes build significantly more complicated and longer to run if we need to build the whole thing altogether. Developer experience also get worse cause of additional elements to manage and keep in sync within the IDE.

Java package as module : This is the natural mechanism to structure code within a Java project. Java classes belong to Java package which stands for a module. Unfortunately there is no native way to express package dependency rules in java and therefore no native natural way to enforce it.  It’s where JDepend comes in play…

What is JDepend ?

JDepend is a simple jar library that analyses a Java code base (class files) to determine some metrics about package dependencies and design of an application. It can be used as a black box tool that produce metrics but also as a library for reasoning about package dependencies.

What to do with JDepend ?

The idea is to use JDepends for checking that package dependency scheme is respected. A good strategy is to run the check within the unit test suite.

A simple but powerful check is to detect any package cycle and fail test suite if any one is present. It’s not obvious to keep zero package cycle in a growing application. If you manage to, you can be reasonably confident that complexity and modularity of the application is under control.

The following test fails when a cycle is detected. The failure message mentions where the cycle occurs. Note that we need to mention the base package of our application in order JDepend do not analyse more code than needed.

public class PackageDependencyTest {

    @Test
    public void testDependencies() throws IOException {
        final String packagePrefix = "my.basepackage";
        final File classDir = new File("target/classes");
        final String cycle = PackageAnalyser.of(classDir, packagePrefix).cycle();
        Assert.assertTrue(cycle, cycle == null);
    }

    public static class PackageAnalyser {

        private final Collection<JavaPackage> packages;

        private PackageAnalyser(Collection<JavaPackage> packages) {
            this.packages = packages;
        }

        @SuppressWarnings("unchecked")
        public static PackageAnalyser of(File classDir, final String packagePrefix) {
            final PackageFilter filter = new PackageFilter() {

                @Override
                public boolean accept(String name) {
                    return name.startsWith(packagePrefix);
                }

            };
            final JDepend depend = new JDepend(filter);
            try {
                depend.addDirectory(classDir.getPath());
            } catch (final IOException e) {
                throw new RuntimeException(e);
            }
            final Collection<JavaPackage> packages = depend.analyze();
            return new PackageAnalyser(packages);
        }

        public String cycle() {
            for (final JavaPackage javaPackage : packages) {
                if (javaPackage.containsCycle()) {
                    return "package " + javaPackage.getName() + " involved in cycles : " + cycleAsString(javaPackage);
                }
            }
            return null;
        }

        private static String cycleAsString(JavaPackage javaPackage) {
            final List<JavaPackage> objects = new LinkedList<JavaPackage>();
            javaPackage.collectCycle(objects);
            final StringBuilder builder = new StringBuilder();
            for (final JavaPackage javaPackage2 : objects) {
                builder.append(javaPackage2.getName()).append(" -> ");
            }
            if (builder.length() > 4) {
                builder.delete(builder.length() - 4, builder.length());
            }
            return builder.toString();
        }

    }

}

You also can do finer checks as expressing explicitly allowed dependencies. JDepend site explains how to.

Conclusion

You don’t have to switch to micro-services architecture or even to split your code base in several projects to manage a growing code base. If two modules are released together and are deployed in the same process, you would probably better code them in the same project, just don’t forget to use an automated tool to control that package design is well respected inside.