Modularizing a Spring Boot Application

Table Of Contents

Every software project comes to a point where the code should be broken up into modules. These may be modules within a single code base or modules that each live in their own code base. This article explains some Spring Boot features that help to split up your Spring Boot application into several modules.

Example Code

This article is accompanied by a working code example on GitHub.

What’s a Module in Spring Boot?

A module in the sense of this article is a set of Spring components loaded into the application context.

A module can be a business module, providing some business services to the application or a technical module that provides cross-cutting concerns to several other modules or to the whole of the application.

The modules discussed in this article are part of the same monolithic codebase. To better enforce module boundaries, we could split up that monolithic codebase into multiple build modules with Maven or Gradle, if we so wish.

Options for Creating Modules

The base for a Spring Module is a @Configuration-annotated class along the lines of Spring’s Java configuration feature.

There are several ways to define what beans should be loaded by such a configuration class.

@ComponentScan

The easiest way to create a module is using the @ComponentScan annotation on a configuration class:

@Configuration
@ComponentScan(basePackages = "io.reflectoring.booking")
public class BookingModuleConfiguration {
}

If this configuration class is picked up by one of the importing mechanisms (explained later), it will look through all classes in the package io.reflectoring.booking and load an instance of each class that is annotated with one of Spring’s stereotype annotations into the application context.

This way is fine as long as you always want to load all classes of a package and its sub-packages into the application context. If you need more control on what to load, read on.

@Bean Definitions

Spring’s Java configuration feature also brings the @Bean annotation for creating beans that are loaded into the application context:

@Configuration
public class BookingModuleConfiguration {

  @Bean
  public BookingService bookingService(){
    return new BookingService();
  }
  
  // potentially more @Bean definitions ...

}

When this configuration class is imported, a BookingService instance will be created and inserted into the application context.

Using this way to create a module gives a clearer picture of what beans are actually loaded, since you have a single place to look at - in contrast to using @ComponentScan where you have to look at the stereotype annotations of all classes in the package to see what’s going on.

@ConditionalOn... Annotations

If you need even more fine-grained control over which components should be loaded into the application context, you can make use of Spring Boot’s @ConditionalOn... annotations:

@Configuration
@ConditionalOnProperty(name = "io.reflectoring.security.enabled", 
    havingValue = "true", matchIfMissing = true)
public class SecurityModuleConfiguration {
  // @Bean definitions ...
}

Setting the property io.reflectoring.security.enabled to false will now disable this module completely.

There are other @ConditionalOn... annotations you can use to define conditions for loading a module. These include a condition depending on the version of the JVM and the existence of a certain class in the classpath or a certain bean in the application context.

If you ever asked yourself how Spring Boot magically loads exactly the beans your application needs into the application context, this is how. Spring Boot itself makes heavy use of the @ConditionalOn... annotations.

Options for Importing Modules

Having created a module, we need to import it into the application.

@Import

The most straight-forward way is to use the @Import annotation:

@SpringBootApplication
@Import(BookingModuleConfiguration.class)
public class ModularApplication {
  // ...
}

This will import the BookingModuleConfiguration class and all beans that come with it - no matter whether they are declared by @ComponentScan or @Bean annotations.

@Enable... Annotations

Spring Boot brings a set of annotations that each import a certain module by themselves. An example is @EnableScheduling, which imports all Beans necessary for the scheduling sub system and its @Scheduled annotation to work.

We can make use of this ourselves, by defining our own @EnableBookingModule annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(BookingModuleConfiguration.class)
@Configuration
public @interface EnableBookingModule {
}

The annotation is used like this:

@SpringBootApplication
@EnableBookingModule
public class ModularApplication {
 // ...
}

The @EnableBookingModule annotation is actually just a wrapper around an @Import annotation that imports our BookingModuleConfiguration as before. However, if we have a module consisting of more than one configuration, this is a convenient and expressive way to aggregate these configurations into a single module.

Auto-Configuration

If we want to load a module automatically instead of hard-wiring the import into the source code, we can make use of Spring Boot’s auto-configuration feature.

To enable a module for auto configuration, put the file META-INF/spring.factories into the classpath:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  io.reflectoring.security.SecurityModuleConfiguration

This would import the SecurityModuleConfiguration class all its beans into the application context.

An auto-configuration is especially handy if we’re building a cross-cutting concern to be used in many Spring Boot applications. In this case, we can even build a separate starter module around the configuration.

Configuring a Module

With the @ConfigurationProperties annotation, Spring Boot provides first-class support for binding external configuration parameters to a Spring bean in a type-safe manner.

When to use which Import Strategy?

This article presented the major options for creating and importing modules in a Spring Boot application. But when should we use which of those options?

Use @Import for Business Modules

For modules that contain business logic - like the BookingModuleConfiguration from the code snippets above - a static import with the @Import annotation should suffice in most cases. It usually does not make sense to not load a business module, so we do not need any control about the conditions under which it is loaded.

Note that even if a module is always loaded, it still has a right to exist as a module, since it being a module enables it to live in its own package or even its own JAR file.

Use Auto-Configuration for Technical Modules

Technical modules, on the other hand - like the SecurityModuleConfiguration from above - usually provide some cross-cutting concerns like logging, exception handling, authorization or monitoring features which the application can very well live without.

Especially during development, these features may not be desired at all, so we want to have a way to disable them.

Also, we do not want to import each technical module statically with @Import, since they should not really have any impact on our code.

So, the best option for importing technical modules is the auto-configuration feature. The modules are loaded silently in the background and we can influence them outside of the code with properties.

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Understanding Null Safety in Kotlin

One of the standout features that sets Kotlin apart is its robust approach to null safety. Null safety is a critical aspect of programming languages, aiming to eliminate the notorious null pointer exceptions that often plague developers.

Read more

Merge Sort in Kotlin

Sorting is a fundamental operation that plays a crucial role in various applications. Among the many sorting algorithms, merge sort stands out for its efficiency and simplicity.

Read more

Extension Functions in Kotlin

One of Kotlin’s standout features is extension functions, a mechanism that empowers developers to enhance existing classes without modifying their source code.

Read more