Spring Modulith & Sonargraph – Better Together

We created Sonargraph with the vision in mind, that it would allow architects to formally specify an enforceable architectural model. Another goal was to provide exceptional dependency visualization capabilities, so that issues could be easily detected not only in a formal way, but also by just looking at a dependency graph. Sonargraph’s architecture DSL (domain specific language) solved the first problem, while our exploration view solved the second one in a very unique and scalable way. The DSL is quite powerful and easy to learn. For an introduction you could read “How to Organize your Code” on this very site.

But obviously we were not the only ones thinking about a way to formally define architectural rules. Spring Modulith turned out to be a very powerful and successful solution to define domain driven architectures for Spring-Boot applications. Spring Modulith follows a pretty simple hands-off approach that allows the checking of architectural boundaries with a minimum configuration approach.

In the screenshot above you see the directory structure of the Spring-Restbucks demo project. Restbucks.java is the Spring-Boot main class. All folders under there are considered to be Spring Modulith modules, also known as “application modules”. So in the example above we have 6 modules. Modulith is very generous with the dependency rules. The only thing not allowed are dependency cycles between modules. It is possible to define allowed dependencies between modules, but usually not necessary. You can do this by using the @ApplicationModule package annotation. By default the interface of a module are the public classes in its root package. If you declare a module to be open, all public types, even the ones in sub-packages, are part of the interface.

You can also defined named interfaces, if the default interface rules are not sufficient for your purposes. Tis is done by using Spring Modulith’s @NamedInterface annotation. If a type is added to a named interface, it will not be part of the default interface anymore.

When you are not using allowed dependencies, a module can access the interface of any other module as long as there are no cyclic dependencies between modules. Once you name allowed dependencies, only explicitly allowed modules and so called “shared” modules can be accessed. Again, all of those details can be specified using the @ApplicationModule package annotation.

Modules can also have nested modules inside. If there is more than one nested module, the access rules between them are defined in the same way as for the top level modules. Nested modules are normally hidden inside of their parent module, but a module can specify an allowed dependency to a nested module of an other module. When it comes to allowed outgoing dependencies from nested modules, they can access the same modules as their parent module.

Considering dependencies between nested modules and the parent module, Spring Modulith has no explicit rules, except that dependencies are not allowed to form a cycle between the parent module and the nested modules. So either a nested module has access to the parent or the parent has access to the nested modules.

All of these rules can easily be translated into Sonargraph’s architecture DSL. But before we can generate the DSL we need to analyze the dependencies between modules. Everything is relatively easy as long as there are no cyclic dependencies. As soon as cycles occur we need to compute a minimal breakup set for the cycles so that we can put the modules in a meaningful order. But that computation is complicated by the fact, that the algorithm could accidentally remove allowed dependencies between modules, so we have to tell the algorithm which dependencies have to be kept. When everything is done right, we can generate the right architecture specification, where all the removed edges are real architecture violations.

In the original Restbucks example there are no cyclic dependencies. In order to test our code generator we added two cyclic edges, going from “order” to “dashboard” and from “core” to “engine”. Also we added more dependencies from “order” to “dashboard” than there are dependencies from “dashboard” to “order”. We also defined an allowed dependency from “order” to “dashboard”. If you just compute a minimal breakup set for the cycle between “order” and “dashboard” the algorithm would remove the dependency going from “dashboard” to “order”, because in our example it has a lower weight (fewer actual code dependencies). But since we have defined an allowed dependency from “dashboard” to “order” the algorithm will keep this dependency and cut the other one.

The screenshot above shows the generated architecture in the Sonargraph exploration view. The arcs are directed and go counterclockwise. Green arcs are conforming to the architecture, while red arcs depict real architecture violations. The generated model correctly identified the two dependencies we introduced to form cyclic dependencies as architecture violations.

Clicking on one of the red arcs will show the violating dependency in the “Parser Dependencies Out” view. Another double click leads directly to the offending line in the code:

When it came to the relation between parent and child modules we had to solve a little problem caused by differences between the Spring Modulith and Sonargraph DSL specifications. The DSL also allows nested artifacts, but forbids dependencies from nested artifacts to parent artifacts. To solve this problem the code generator would analyze the dependencies between nested artifacts and parent artifacts. Only if there were more dependencies from the nested artifact to the parent artifacts we would add an additional nested artifact named “Shared” at the bottom of the list of nested artifacts. This artifact would include everything from the parent artifact and would be declared “public” so that all sibling artifacts defined above it could use it.

Here is the DSL code generated for the Restbucks example:

// Generated from target/classes/META-INF/spring-modulith/application-modules.json
//
// 2025-07-23T13:01:11.286819-04:00
//
// Regenerate if:
// - number of modules or module structure changes
// - changes in named interfaces
// - change of module dependency structure

relaxed artifact Engine
{
    include "server/de/odrotbohm/restbucks/engine/**"

    interface default
    {
        include "server/de/odrotbohm/restbucks/engine/*"
    }
}

relaxed artifact Payment
{
    include "server/de/odrotbohm/restbucks/payment/**"

    hidden relaxed artifact Nested
    {
        include "server/de/odrotbohm/restbucks/payment/nested/**"

        interface default
        {
            include "server/de/odrotbohm/restbucks/payment/nested/*"
        }
    }

    public artifact Shared
    {
        include "**"

        interface default
        {
            include "server/de/odrotbohm/restbucks/payment/*"

            exclude "server/de/odrotbohm/restbucks/payment/PaymentInitializer"
        }

        interface other
        {
            include "server/de/odrotbohm/restbucks/payment/PaymentInitializer"
        }
    }

    interface other
    {
        export Shared.other
    }

    interface default
    {
        export Shared
    }
}

artifact Dashboard
{
    include "server/de/odrotbohm/restbucks/dashboard/**"

    interface default
    {
        include "server/de/odrotbohm/restbucks/dashboard/*"
    }

    connect to Order
}

relaxed artifact Order
{
    include "server/de/odrotbohm/restbucks/order/**"

    interface default
    {
        include "server/de/odrotbohm/restbucks/order/*"
    }
}

relaxed artifact Drinks
{
    include "server/de/odrotbohm/restbucks/drinks/**"

    interface special
    {
        include "server/de/odrotbohm/restbucks/drinks/DrinksModelProcessor"
    }

    interface default
    {
        include "server/de/odrotbohm/restbucks/drinks/*"

        exclude "server/de/odrotbohm/restbucks/drinks/DrinksModelProcessor"
    }
}

public artifact Core
{
    include "server/de/odrotbohm/restbucks/core/**"

    interface default
    {
        include "server/de/odrotbohm/restbucks/core/*"
    }
}

Have a look at the “Payment” artifact where we have a dependency going from the nested module to payment. For that case we had to generate the “Shared” artifact as described above. Artifacts marked as “relaxed” can access all artifacts defined beneath them. “public” artifacts can be accessed by all sibling artifacts defined above them. We use “relaxed” for all modules that do not define allowed dependencies and “public” for all shared artifacts.

You can imagine that using Sonargraph and Spring Modulith together can be quite useful, especially if you want to migrate a legacy Spring Boot application to Spring Modulith. In any case, having the dependency visualization capabilities of Sonargraph combined with a powerful architecture definition, is always a productivity booster. Btw, for the integration to work you need to use Spring Modulith 1.4.2 or higher. The 1.4.2 release is scheduled for July 25th 2025.

And as a special icing on the cake, you can even create an UML Component Diagram out of Sonargraph’s DSL:

Thank for for reading this article to the very end. Special thanks to Oliver Drotbohm, the man behind Spring Modulith, who was a tremendous help in creating this integration. Let me know what you think about our newest feature in the comment section below.

Leave a Reply

Your email address will not be published. Required fields are marked *