Chapter 10 – Get Your Hands Dirty on Clean Architecture

Chapter 10

Enforcing Architecture Boundaries

We have talked a lot about architecture in the previous chapters and it feels good to have a target architecture to guide us in our decisions on how to craft code and where to put it.

In every above-playsize software project, however, architecture tends to erode over time. Boundaries between layers weaken, code becomes harder to test, and we generally need more and more time to implement new features.

In this chapter, we will discuss some measures that we can take to enforce the boundaries within our architecture and thus fight architecture erosion.

Boundaries and Dependencies

Before we talk about different ways of enforcing architecture boundaries, let's discuss where the boundaries lie within our architecture and what "enforcing a boundary" actually means:

Figure 10.1: Enforcing architecture boundaries means enforcing that dependencies point in the right direction. Dashed arrows mark dependencies that are not allowed according to our architecture

The preceding figure shows how the elements of our hexagonal architecture might be distributed across four layers resembling the generic clean architecture approach introduced in Chapter 2, Inverting Dependencies.

The innermost layer contains domain entities. The application layer may access those domain entities to implement use cases within application services. Adapters access those services through incoming ports or are being accessed by those services through outgoing ports. Finally, the configuration layer contains factories that create adapter and service objects and provides them to a dependency injection mechanism.

In the preceding figure, our architecture boundaries become pretty clear. There is a boundary between each layer and its next inward and outward neighbors. According to the dependency rule, dependencies that cross such layer boundaries must always point inward.

This chapter is about ways to enforce the dependency rule. We want to make sure that there are no illegal dependencies that point in the wrong direction (dashed arrows in the figure).

Visibility Modifiers

Let's start with the most basic tool that Java provides us for enforcing boundaries: visibility modifiers.

Visibility modifiers have been a topic in almost every entry-level job interview I have conducted in the last couple of years. I would ask the interviewee what visibility modifiers Java provides and what their differences are.

Most of the interviewees only list the public, protected, and private modifiers. Almost none know the package-private (or "default") modifier. This is always a welcome opportunity for me to ask some questions about why such a visibility modifier would make sense in order to find out whether the interviewee could abstract the answer from their previous knowledge.

So, why is the package-private modifier such an important modifier? Because it allows us to use Java packages to group classes into cohesive "modules." Classes within such a module can access each other but cannot be accessed from outside of the package. We can then choose to make specific classes public to act as entry points to the module. This reduces the risk of accidentally violating the dependency rule by introducing a dependency that points in the wrong direction.

Let's have another look at the package structure discussed in Chapter 3, Organizing Code, with visibility modifiers in mind:

Figure 10.2: Package structure with visibility modifiers.

We can make the classes in the persistence package package-private (marked with "o" in the preceding tree) because they don't need to be accessed by the outside world. The persistence adapter is accessed through the output ports it implements. For the same reason, we can make the SendMoneyService class package-private. Dependency injection mechanisms usually use reflection to instantiate classes, so they will still be able to instantiate those classes even if they are package-private.

With Spring, this approach only works if we use the classpath scanning approach discussed in Chapter 9, Assembling the Application, however, since the other approaches require us to create instances of those objects ourselves, which requires public access.

The rest of the classes in the example have to be public (marked with "+") by definition of the architecture: the domain package needs to be accessible by the other layers and the application layer needs to be accessible by the web and persistence adapters.

The package-private modifier is awesome for small modules with no more than a handful of classes. Once a package reaches a certain number of classes, however, it grows confusing to have so many classes in the same package. In this case, I like to create sub-packages to make the code easier to find (and, I admit, to satisfy my need for aesthetics). This is where the package-private modifier fails to deliver since Java treats sub-packages as different packages and we cannot access a package-private member of a sub-package. So, members in sub-packages must be public, exposing them to the outside world and thus making our architecture vulnerable to illegal dependencies.

Post-Compile Checks

As soon as we use the public modifier on a class, the compiler will let any other class use it, even if the direction of the dependency points in the wrong direction according to our architecture. Since the compiler won't help us out in these cases, we have to find other means to check that the dependency rule isn't violated.

One way is to introduce post-compile checks – that is, checks that are conducted at runtime when the code has already been compiled. Such runtime checks are best run during automated tests within a continuous integration build.

A tool that supports this kind of check for Java is ArchUnit (https://github.com/TNG/ArchUnit). Among other things, ArchUnit provides an API to check whether dependencies point in the expected direction. If it finds a violation, it will throw an exception. It's best run from within a test based on a unit testing framework such as JUnit, making the test fail in the event of a dependency violation.

With ArchUnit, we can now check the dependencies between our layers, assuming that each layer has its own package, as defined in the package structure discussed in the previous section. For example, we can check that there is no dependency from the domain layer to the outward-lying application layer:

class DependencyRuleTests {

  @Test

  void domainLayerDoesNotDependOnApplicationLayer() {

    noClasses()

        .that()

        .resideInAPackage("buckpal.domain..")

        .should()

        .dependOnClassesThat()

        .resideInAnyPackage("buckpal.application..")

        .check(new ClassFileImporter()

            .importPackages("buckpal.."));

  }

}

With a little work, we can even create a kind of domain-specific language (DSL) on top of the ArchUnit API that allows us to specify all relevant packages within our hexagonal architecture and then automatically checks whether all the dependencies between those packages point in the right direction:

class DependencyRuleTests {

  @Test

  void validateRegistrationContextArchitecture() {

    HexagonalArchitecture.boundedContext("account")

        .withDomainLayer("domain")

        .withAdaptersLayer("adapter")

          .incoming("web")

          .outgoing("persistence")

          .and()

        .withApplicationLayer("application")

          .services("service")

          .incomingPorts("port.in")

          .outgoingPorts("port.out")

          .and()

        .withConfiguration("configuration")

        .check(new ClassFileImporter()

            .importPackages("buckpal.."));

  }

}

In the preceding code example, we first specify the parent package of our bounded context (which might also be a complete application if it spans only a single bounded context). We then go on to specify the sub-packages for the domain, adapter, application, and configuration layers. The final call to check() will then execute a set of checks, verifying that the package dependencies are valid according to the dependency rule. The code for this hexagonal architecture DSL is available in the HexagonalArchitecture class of the example project at https://github.com/thombergs/buckpal.

While post-compile checks can be a great help in fighting illegal dependencies, they are not fail-safe. If we misspell the package name buckpal in the preceding code example, for instance, the test will find no classes and thus no dependency violations. A single typo or, more importantly, a single refactoring renaming a package can make the whole test useless. We might fix this by adding a check that fails if no classes are found, but it's still vulnerable to refactorings. Post-compile checks always have to be maintained parallel to the codebase.

Build Artifacts

Until now, our only tool for demarcating architecture boundaries within our codebase has been packaged. All of our code has been part of the same monolithic build artifact.

A build artifact is the result of a (hopefully automated) build process. Currently, the most popular build tools in the Java world are Maven and Gradle. So, until now, imagine we had a single Maven or Gradle build script and we could call Maven or Gradle to compile, test, and package the code of our application into a single JAR file.

One main feature of build tools is dependency resolution. To transform a certain codebase into a build artifact, a build tool first checks whether all the artifacts that the code base depends on are available. If not, it tries to load them from an artifact repository. If this fails, the build will fail with an error, before even trying to compile the code.

We can leverage this to enforce the dependencies (and thus, enforce the boundaries) between the modules and layers of our architecture. For each such module or layer, we create a separate build module with its own codebase and its own build artifact (JAR file) as a result. In the build script of each module, we specify only those dependencies to other modules that are allowed according to our architecture. Developers can no longer inadvertently create illegal dependencies because the classes are not even available on the classpath and they would run into compile errors:

Figure 10.3: Different ways of dividing our architecture into multiple build artifacts to prohibit illegal dependencies

The preceding figure shows an incomplete set of options to divide our architecture into separate build artifacts.

Starting on the left, we see a basic three-module build with a separate build artifact for the configuration, adapter, and application layers. The configuration module may access the adapters module, which in turn may access the application module. The configuration module may also access the application module due to the implicit, transitive dependency between them.

Note that the adapter's module contains the web adapter as well as the persistence adapter. This means that the build tool will not prohibit dependencies between those adapters. While dependencies between those adapters are not strictly forbidden by the dependency rule (since both adapters are within the same outer layer), in most cases it's sensible to keep adapters isolated from each other.

After all, we usually don't want changes in the persistence layer to leak into the web layer and vice versa (remember the Single Responsibility Principle).

The same holds true for other types of adapters, such as adapters connecting our application to a certain third-party API. We don't want details of that API leaking into other adapters by adding accidental dependencies between adapters.

Thus, we may split the single adapters module into multiple build modules, one for each adapter, as shown in the second column of Figure 10.2.

Next, we could decide to split up the application module further. It currently contains the incoming and outgoing ports to our application, the services that implement or use those ports, and the domain entities that should contain much of our domain logic.

If we decide that our domain entities are not to be used as transfer objects within our ports (that is, we want to disallow the "No Mapping" strategy from Chapter 8, Mapping between Boundaries), we can apply the Dependency Inversion Principle and pull out a separate API module that contains only the port interfaces (the third column in Figure 10.2).

The adapter modules and the application module may access the API module, but not the other way around. The API module does not have access to the domain entities and cannot use them within the port interfaces. Also, the adapters no longer have direct access to the entities and services, so they must go through the ports.

We can even go a step further and split the API module in two, one part containing only the incoming ports and the other part only containing the outgoing ports (the fourth column in Figure 10.1). This way, we can make very clear whether a certain adapter is an incoming adapter or an outgoing adapter by declaring a dependency only to the input or the outgoing ports.

Also, we could split the application module even further, creating a module containing only the services and another containing only the domain entities. This ensures that the entities don't access the services and it would allow other applications (with different use cases and thus different services) to use the same domain entities by simply declaring a dependency on the domain build artifact.

Figure 10.2 illustrates that there are a lot of different ways to divide an application into build modules, and there are of course more than just the four ways depicted in the figure. The gist is that the finer we cut our modules, the stronger we can control dependencies between them. The finer we cut, however, the more mapping we have to do between those modules, enforcing one of the mapping strategies introduced in Chapter 8, Mapping between Boundaries.

Besides that, demarcating architecture boundaries with build modules has a number of advantages over using simple packages as boundaries.

First, build tools absolutely hate circular dependencies. Circular dependencies are bad because a change in one module within the circle would potentially mean a change in all other modules within the circle, which is a violation of the single responsibility principle. Build tools don't allow circular dependencies because they would run into an endless loop while trying to resolve them. Thus, we can be sure that there are no circular dependencies between our build modules.

The Java compiler, on the other hand, doesn't care at all whether there is a circular dependency between two or more packages.

Second, build modules allow isolated code changes within certain modules without having to take the other modules into consideration. Imagine we have to do a major refactoring in the application layer that causes temporary compile errors in a certain adapter. If the adapters and application layer are within the same build module, most IDEs will insist that all compile errors in the adapters must be fixed before we can run the tests in the application layer, even though the tests don't need the adapters to compile. If the application layer is in its own build module, however, the IDE won't care about the adapters at that time, and we could run the application layer tests at will. The same goes for running a build process with Maven or Gradle: if both layers were in the same build module, the build would fail due to compile errors in either layer.

So, multiple build modules allow isolated changes in each module. We could even choose to put each module into its own code repository, allowing different teams to maintain different modules.

Finally, with each inter-module dependency explicitly declared in a build script, adding a new dependency becomes a conscious act instead of an accident. A developer who needs access to a certain class they currently cannot access will hopefully give some thought to the question of whether the dependency is really reasonable before adding it to the build script.

These advantages come with the added cost of having to maintain a build script, though, so the architecture should be somewhat stable before splitting it into different build modules.

How Does This Help Me Build Maintainable Software?

Software architecture is basically all about managing dependencies between architecture elements. If the dependencies become a big ball of mud, the architecture becomes a big ball of mud.

So, to preserve the architecture over time, we need to continually make sure that dependencies point in the right direction.

When producing new code or refactoring existing code, we should keep the package structure in mind and use package-private visibility when possible to avoid dependencies on classes that should not be accessed from outside the package.

If we need to enforce architecture boundaries within a single build module, and the package-private modifier doesn't work because the package structure won't allow it, we can make use of post-compile tools such as ArchUnit.

And anytime we feel that the architecture is stable enough, we should extract architecture elements into their own build modules because this gives explicit control over the dependencies.

All three approaches can be combined to enforce architecture boundaries and thus keep the code base maintainable over time.