Formalising code architecture using ArchUnit

I’ve been thinking about all the implicit rules in our code base recently, we ‘know’ that our endpoints should not talk directly to the data access layer and services should not call the web layer. We also have conventions around naming things, methods that return collections should be named findXYZ etc.

These rules are often captured in a style guide and diligent developers will read them and incorporate them into their code. In Waltz we wanted to see if there was a way to enforce these rules in a more concrete fashion. We happened upon ArchUnit, an excellent library for describing these concerns and asserting compliance at build time via unit tests. From their website:

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cycl ic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure. ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework

Getting Started

The tutorial is great at getting you up and running quickly, simply add a maven dependency and start writing tests. We started with some easy rules such as: “All *Dao classes should be annotated with the Repository annotation”. This can be described in a simple test:

private JavaClasses myClasses = new ClassFileImporter()
    .importPackages("com.khartec");

@Test
public void daosNeedRepositoryAnnotation() {
    ArchRule rule = classes().that()
            .areNotInterfaces()
            .and()
            .haveNameMatching(".*Dao")
            .should()
            .beAnnotatedWith(Repository.class);
    rule.check(myClasses);
}

This quick success spurred us on to look at what else we could do with this library. We rapidly created tests to:

  • Ensure our services and endpoints are annotated with Service
  • Check that marker interfaces and base classes are used consistently
  • No calls made to java.util.logging (we use logback)
  • No generic exceptions are thrown (no throw new Exception() or throw new RuntimeException() )

The last two are provided by the library and simply need calling:

@Test
public void noGenericExceptions() {
    GeneralCodingRules.NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS
            .check(myClasses);
}

break

Enforcing a Layered Architecture

ArchUnit also has support for defining layers in our architecture. We simply define layers as matching package patterns. We defined layers for our endpoints, data-extractors, services and data-access-objects. Using these defined layers we can write statements such as ‘api-endpoints cannot access the data-access-objects directly, however the extractor-endpoints can’. The code ended up looking like:

@Test
public void ensureLayersAreRespected() {
  layeredArchitecture()
    ...
    .layer("ApiEndpoints")
      .definedBy("..endpoints.api..")
    .layer("ExtractorEndpoints")
      .definedBy("..endpoints.extracts..")
    .layer("Data")
      .definedBy("..data..")
    ...
    .whereLayer("Data")
      .mayOnlyBeAccessedByLayers("Services", "ExtractorEndpoints")
    .check(myClasses);
}

A lot of these layers can be achieved by setting up maven sub-project dependencies, however it’s good to have additional checks and we currently don’t have our extractors separated from our api-endpoints — this lets us put in an architectural rule to allow us to treat them as a distinct layer within a sub-project.

Naming Conventions

Another convention within our code-base that we wished to enforce was our naming convention within the data-access-objects. In particular we wanted to ensure that findXYZ() methods returned either a collection or map. This one took a little longer to figure out and a careful reading of the documentation. We eventually came up with a rule which looks like:

ArchCondition<JavaClass> findMethodsMustReturnCollectionsOrMaps =
      new ArchCondition<JavaClass>("..desc..") {
  Set<Class<?>> validTypes = SetUtilities
     .asSet(Collection.class, Map.class);

  public void check(JavaClass item, ConditionEvents events) {
    item
     .getMethods()
     .stream()
     .filter(m -> m.getName().startsWith("find"))
     .filter(m -> m.getModifiers().contains(JavaModifier.PUBLIC))
     .forEach(m -> {
       JavaClass rt = m.getReturnType();
       if (! any(validTypes, vrt ->   rt.isAssignableTo(vrt))) {
         String message = String.format(
            "Method %s.%s does not return a collection or map. It returns: %s",
            item.getName(),
            m.getName(),
            returnType.getName());
        events.add(SimpleConditionEvent.violated(item, message));
      }
    });
  }
}

private JavaClasses myClassesAndJavaUtilClasses = 
   new ClassFileImporter()
       .importPackages("com.khartec", "java.util");

@Test
public void methodsPrefixedFindShouldReturnCollections() {
    ArchRule rule = classes().that()
            .areNotInterfaces()
            .and()
            .haveNameMatching(".*Dao")
            .should(haveFindMethodsThatReturnCollectionsOrMaps);

    rule.check(myClassesAndJavaUtilClasses);
}

One thing that tripped us up was our original definition of myClasses which was simply an including classes within com.khartec , when we wished to check the return types against java.util.Collection and java.util.Map the isAssignableTo test was always failing. We fixed this by ensuring the set of classes included java.util`

Conclusion

We are really pleased with how this task turned out and highly recommend that you investigate how ArchUnit could help on your projects. Many thanks to the team at TNG for making such a great library available via opensource.