A Modular Architecture with Spring Boot

For my current project, we built on the Hexagon Architecture beautifully described by my colleague previously to develop a more Spring-centric approach to application modularity.

In that article, the application concerned maintained all Spring beans within a single module. Now, I have been accused of having drunk the Spring Kool-Aid in its entirety, but for me this approach was never going to cut it. Spring’s great value is as an integration framework: the benefits I derive from it aren’t merely those it offers as a dependency injection container and MVC framework, but also stem from the rock-solid, familiar libraries it provides me with to implement all layers of my application.

In this article I describe how, using Spring Boot, we came up with a mechanism for supporting the full canon of Spring voodoo throughout a modular application, whilst as far as possible preventing Spring configuration from bleeding across module boundaries.

The Application Context

In Spring, when we register a component by any means, we are registering it with an application context. In a Spring Boot application started with SpringApplication.run() and using MVC auto-configuration, we will have a single application context, and we don’t usually have to worry about its existence very much. Typically, we just register beans via Spring Boot auto-configuration, component scanning or @Bean annotations, and they all just get added to the bean soup, ready for auto-wiring.

Oh Hell No

This is convenient, but is of course a nightmare for application modularity. With all of your modules slurping the same soup, any attempt to limit the collaborators a program unit can access will be purely an exercise in self-discipline.

What I would like is to be able to use multiple application contexts in my application, one per-module, and prevent beans registered in one context being accessible in another, unless I explicitly export that API. This ensures that modules are loosely coupled, and that should I ever have the need to partition my modular monolithic application into distinct microservices, I won’t have a hard time untangling them.

Hierachical Contexts

Key to our solution is the feature that application contexts can be hierarchical. You may remember this from pre-Boot days, when a Spring MVC DispatcherServlet was typically backed by a child of the root context, with the root registered by a ContextLoaderListener or some such. Child contexts can resolve beans from their parent – but never vice versa, and sibling contexts are isolated from one another.

I want to be able to export an inter-module API by registering a bean with a root application context in addition to the module’s own context. That bean will then be auto-wirable in any module in the application, whereas all other beans will essentially be private to their module.

Modules

Building Our Contexts

SpringApplication.run() doesn’t have enough power to create this setup of hierarchical contexts, but Spring Boot provides a SpringApplicationBuilder that can do it for us. Here’s a first attempt:

@SpringBootApplication
public class Application {

  public static void main(String[] args) {

    new SpringApplicationBuilder()
      .sources(Application.class).web(false)
      .child(DiaryConfig.class).web(false)
      .sibling(WebConfig.class).web(true)
      .run(args);
  }
}

Here we’ve defined a root context based on the Application class, and two child contexts hanging off it, driven by the @Configuration classes DiaryConfig and WebConfig. These configurations, and any associated beans, live in their own application modules.

We can now define whatever beans we like in our two config classes, and these can never interfere with each other. So far so good – but what happens when we do actually need a collaborator from another module?

First, we define an API module. The interfaces contained within are the only means by which our “web” module can collaborate with our “diary” module.

public interface DiaryService {
  String[] getDiaryEvents(int days);
}

What’s missing is the means by which we will acquire a reference to our DiaryService – whose implementation will live in the diary module – in our web module’s controller.

@Controller
class DiaryController {

  private DiaryService service;

  DiaryController(DiaryService service) { // <-- but from where?
    this.service = service;
  }

  @GetMapping
  String[] getWeek() {
    return service.getDiaryEvents(7);
  }
}

Exporting an API

In a @Configuration class, I want to be able to define beans as @Exported, so they are available to other modules. @Exported is just a trivial marker annotation we’ve created:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Exported {}

The guts of our bean export mechanism is a BeanPostProcessor. For each initialised bean, we check to see if there is a parent context further up the hierarchy from where we are currently. If there is, and the bean we’re currently looking at was registered by a method marked with @Exported, then we re-register this bean as a singleton in the parent.

class ExportedBeanPostProcessor implements BeanPostProcessor {

  private ConfigurableListableBeanFactory beanFactory;

  ExportedBeanPostProcessor(
      ConfigurableListableBeanFactory beanFactory) {

    this.beanFactory = beanFactory;
  }

  @Override
  public Object postProcessBeforeInitialization(Object bean,
      String beanName) throws BeansException {

    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean,
      String beanName) throws BeansException {

    ConfigurableListableBeanFactory parentBeanFactory =
        getParentBeanFactory();

    if (parentBeanFactory == null) {
      return bean;
    }

    BeanDefinition bd;
    try {
      bd = beanFactory.getBeanDefinition(beanName);
    }
    catch (NoSuchBeanDefinitionException exception) {
      return bean;
    }

    if (bd.getSource() instanceof StandardMethodMetadata) {
      StandardMethodMetadata metadata =
          (StandardMethodMetadata) bd.getSource();

      if (metadata.isAnnotated(Exported.class.getName())) {
        parentBeanFactory.registerSingleton(beanName, bean);
      }
    }

    return bean;
  }

  private ConfigurableListableBeanFactory getParentBeanFactory() {
    BeanFactory parent = beanFactory.getParentBeanFactory();

    return (parent instanceof ConfigurableListableBeanFactory)
      ? (ConfigurableListableBeanFactory) parent : null;
  }
}

The Auto-Configuration Menace

There is a problem with the setup so far: the Spring Boot auto-configuration via @SpringBootApplication will register all applicable beans with the root context. This means that all beans registered due to auto-configuration will be available to all contexts, which is not what we want. Worse, by default, the EmbeddedServletContainerAutoConfiguration in particular only considers container factories registered in the web module context itself, not in its parents, and we’ll need that to provide a servlet container for our application.

But it’s not practical to enable auto-configuration in the module configuration classes either. This is because the applicable beans will be determined from the runtime classpath, which all the modules share when running together. The effect of this is that every bean registered by auto-configuration will be registered in every context. This is clearly not what we want either!

Disabling Auto-Configuration

Our approach was simply to disable auto-configuration by omitting @SpringBootApplication and @EnableAutoConfiguration annotations on the configuration classes completely. But we don’t have to forego all the Spring Boot application bootstrapping convenience – we can just bring in the features we want by importing them explicitly:

@Configuration
@ComponentScan
@Import({
  DispatcherServletAutoConfiguration.class,
  EmbeddedServletContainerAutoConfiguration.class,
  HttpMessageConvertersAutoConfiguration.class,
  ServerPropertiesAutoConfiguration.class,
  WebMvcAutoConfiguration.class
})
public class WebConfig {
}

Granted, this is significantly more verbose than just using @EnableAutoConfiguration, particularly where you are using Spring MVC as here you are likely to be using a lot of auto-configuration. We haven’t found this particularly onerous though – you’re likely to change this only rarely, and the configuration classes are easily discoverable via Appendix C in the Spring Boot docs. Running an application with --debug to log the auto-configuration can help with divining this a lot too.

DevTools

DevTools poses a particular problem for our approach to auto-configuration. As it won’t be available on the classpath in production, only during development, we have to include it only if it is available. A static @Import isn’t sufficient to configure this; we need to use an import selector:

public class DevToolsImportSelector
    extends AutoConfigurationImportSelector {

  private static final String AUTO_CONFIGURATION_CLASS =
      "org.springframework.boot.devtools.autoconfigure." +
          "LocalDevToolsAutoConfiguration";

  @Override
  public String[] selectImports(AnnotationMetadata annotationMetadata) {

    try {
      Class.forName(AUTO_CONFIGURATION_CLASS);
    }
    catch (ClassNotFoundException exception) {
      return new String[0];
    }

    return new String[] {AUTO_CONFIGURATION_CLASS};
  }
}

with our final Application class configured as follows:

@SpringBootConfiguration
@Import(DevToolsImportSelector.class)
public class Application {

  public static void main(String[] args) {
    new SpringApplicationBuilder()... ;
  }
}

Exporting our Service

Now all that is left is to implement our DiaryService and mark it as @Exported in our module configuration class.

@Configuration
@Import({
  CacheAutoConfiguration.class,
  ExportedConfiguration.class
})
@EnableCaching
public class DiaryConfig {

  @Exported
  @Bean
  public DiaryService diary() {
    return new CraigDiaryService();
  }
}
public class CraigDiaryService implements DiaryService {

  @Override
  @Cacheable("diary-events")
  public String[] getDiaryEvents(int days) {
    checkArgument(days == 7, "Unsupported period");

    return new String[] { "met this girl",
        "took her for a drink",
        "making love", "making love",
        "making love", "making love",
        "chilled" };
  }
}

The DiaryController will now be able to resolve our DiaryService implementation successfully, but the CacheManager instance Spring Boot creates for us here will be private to the diary module. As we build on our CraigDiaryService to be a less trivial and ludicrously contrived implementation, we will be able to utilise component scanning to bring in collaborators that are also private to the module. Our work here is done!

But Why?

You may be wondering what exactly is the point of all this. I certainly have been asking myself this a fair bit.

There are numerous benefits that I have found or anticipate with this approach. A modular architecture is a good thing in that it encourages cohesive program units, which should be simpler for developers to understand. Modularisation of a codebase enables parallelisation in its build process. And it should be comparatively easy for modules to be spun out into stand-alone services if their boundaries are clear. On a very large project, modularisation might be essential to provide structure to the code base, and prevent developers from stepping on each other’s toes.

If your modular project is making heavy use of Spring, you may find the approach here works well. An arguable point is whether it is in fact a good thing to riddle your codebase with patterns and library invocations associated with one particular framework, or if it would be better to try to keep such code in one place. I’m sure it’s pretty clear which side I take in this debate. In my defence, I submit that so far in my career I have never removed Spring from a codebase…

But do you need modularisation at all? On a mid-size project, I’ve found my highest velocity to be when working on code in a single, monolithic artifact, with “module” boundaries demarcated implicitly purely by package names and other namespaces. My lowest has been on projects employing a service-oriented or other distributed architecture. This approach sits somewhere in the middle. Whether it’s of benefit would depend on a multitude of factors regarding the project concerned, but, clearly, there is no silver bullet for managing complexity.

Source code for this article is on GitHub.

comments powered by Disqus