Spring Boot Custom Components

Let's build domain-oriented custom components with Spring Boot starters, auto-configuration and configuration properties. Updated to Spring Boot 3.0!


Spring Boot philosophy is to develop applications by composing independent, modular and highly configurable components. Spring Boot provides smart and simple mechanisms and conventions for building such components, that could be easily applied in custom projects.

In this post, we will explore those ideas, and show how to create domain-oriented custom components the Spring Boot way.

What Is Spring Boot

Spring Boot takes an opinionated view of the Spring platform and third-party libraries. This means, all necessary dependencies are included and pre-configured, and there is none or very little additional configuration most Spring Boot applications need to add.

Dependencies are consistent with each other and tested in integration.

For web applications, embedded servers (Tomcat, Jetty, Undertow) are included. Even the traditional deployment into an application container is possible, Spring Boot is self-contained as default, everything is included in the final JAR and no additional infrastructure is needed (except JDK, obviously).

In addition, Spring Boot comes with a lot of production ready features, such as loggers, metrics, auditing, tracing, and monitoring.

A simple application skeleton can be easily prototyped by Spring Initializr.

Build Systems

Spring Boot offers Gradle and Maven plugins for packaging and running Spring Boot applications. It is also possible to create and publish Docker images via the plugins.

Gradle

Spring Boot comes with org.springframework.boot and io.spring.dependency-management plugins:

plugins {
  id 'org.springframework.boot' version '3.0.4'
  id 'io.spring.dependency-management' version '1.1.0'
}

Alternatively, only the dependency management could be used in isolation:

plugins {
  id 'io.spring.dependency-management' version '1.1.0'
}

dependencyManagement {
  dependencies {
    dependency 'org.springframework:spring-core:6.0.6'
  }
}

Maven

Typically, your Maven POM file inherits from the spring-boot-starter-parent project:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.0.4</version>
</parent>

The parent POM brings the Spring Boot plugin, dependency management and other sensible defaults. All default settings can be overwritten in your POM.

Alternatively, only dependency management can be used without the parent POM by using an import scoped dependency:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.0.4</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Features Overview

Spring Boot introduces several features new to the Spring ecosystem.

Spring Boot Application

The SpringApplication class provides a convenient way to bootstrap a Spring application that is started from a main() method.

When the annotation @SpringBootApplication is included on the main class, the application is bootstrapped, auto-configuration enabled and Spring components scan is set to the current package as root.

It is recommended to locate the application class in a root package above other classes. So the package defines the search base for the components scan.

org.example.myshop
+- MyShopApplication
+- catalog
+- delivery
+- order
...

Starters

Starters are a set of convenient dependency descriptors that you can include in your application.

Starters bring all necessary dependencies and configurations into the application:

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
}

All starters have the core starter spring-boot-starter a direct dependency, which includes auto-configuration support, logging and YAML.

Starters are part of Spring Boot philosophy, a convenient way to include components into your application.

Auto-Configuration

Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added.

When auto-configuration is enabled, components found on the classpath are automatically loaded and configured.

Most standard Spring auto-configurations are highly configurable via properties.

Application Properties

Spring Boot automatically finds and loads application[-profile].(properties|yaml|yml) from the classpath and other default locations.

Properties from the loaded files are added into Spring environment.

Application properties form a configuration structure of a Spring Boot application with default values, which are meant to be overwritten in runtime from the environment or other external sources.

Configuration properties are grouped into trees by the context, typically prefixed by the component name and feature:

# application.yml

spring:
  datasource:
    url: jdbc:postgresql://db:5432/test
    username: admin
    password: secret

Custom Components

Features of Spring Boot can serve as a conventional template for custom components. To do things right, a deeper knowledge of Spring Boot concepts and mechanisms is needed.

In the following section, we take a look at techniques for creating a modular application with independent domain-driven components. The components can be assembled into a single monolithic application or separate applications as microservices afterwards.

For each component a Spring Boot starter with auto-configuration will be created.

Assembling of components into an application is achieved simply by adding the component onto the classpath. Practically, by putting the component starter into the application dependencies.

Spring Boot components are logically defined by auto-configurations, starters are an additional mechanism to bring components into the application conveniently.

Code Structure

We will use Java packages for structuring code by the domain and Maven modules or Gradle sub-projects for technical cuts.

Every component is a self-contained business capability service, exposing multiple artifacts for API and implementation, formed by physical modules.

A typical component source code structure looks like:

delivery/	-- comp. root
+- domain/	-- domain API
+- events/	-- events API
+- jdbc/	-- JDBC impl
+- rest/	-- Restful API
+- spring-boot-starter/ -- starter
+- pom.xml
+- build.gradle
\- settings.gradle

Alternatively, auto-configurations can live in a separate module, which the starter includes as its dependency.

In a single-application scenario a separate module for a Spring Boot application is created on the root level:

myshop/
+- application/
+- catalog/
+- delivery/
+- order/
...

In a microservices scenario, each component has its own application module:

myshop/
+- catalog/
|  +- application/
|  +- domain/
|  +- jdbc/
|  +- rest/
|  \- spring-boot-starter/
+- delivery/
|  +- application/
|  +- domain/
|  +- events/
|  +- jdbc/
|  +- rest/
|  \- spring-boot-starter/
+- order/
|  +- application/
|  +- domain/
|  +- events/
|  +- jdbc/
|  +- rest/
|  \- spring-boot-starter/
...

For both scenarios, the separation of concerns is the structure driver. Every module has its own unique role in the final application.

Starters bring Spring Boot auto-configuration, applications bring Spring Boot application main classes, but all other modules has no Spring Boot concerns or dependencies.

For example, even if the REST module is built upon Spring Web, only the corresponding Spring framework dependency should be included:

// rest/build.gradle

implementation 'org.springframework:spring-web'

The Spring Boot Web starter will be included into the starter module only:

// spring-boot-starter/build.gradle

implementation 'org.springframework.boot:spring-boot-starter-web'

Java Packages

As we build domain-oriented components, packages should be domain-driven as well.

All the modules of a component should share a root package. The modules could be further structured by the domain feature or a technical aspect. Same features should be included in an identical package.

This strategy enables information hiding of implementation classes using Java package accessibility. Consider an example, where and mean public and package-protected, respectively:

delivery/
+- domain/
|  \- src/main/java/
|     \- org.example.myshop.delivery
|        \- ○DeliveryService.java
+- events/
|  \- src/main/java/
|     \- org.example.myshop.delivery
|        \- ○DeliveryDispatched.java
+- jdbc/
|  \- src/main/java/
|     \- org.example.myshop.delivery.jdbc
|        \- ●DeliveryServiceJdbc.java
+- rest/
|  \- src/main/java/
|     \- org.example.myshop.delivery.rest
|        \- ●DeliveryRestController.java
\- spring-boot-starter/
   \- src/main/java/
      \- org.example.myshop.delivery
         \- jdbc
            \- ●DeliveryJdbcConfig.java
         \- rest
            \- ●DeliveryRestConfig.java

Classes DeliveryServiceJdbc and DeliveryRestController are in the same package as DeliveryJdbcConfig and DeliveryRestConfig, respectively. This makes them accessible to the configuration classes, which is the only one place they have to be accessible from, and hidden for the rest of the world.

This kind of protection based on the basic language features is a great asset to the overall modularity, preventing the temptation to access implementation details of a foreign component, and violate its sovereignty so.

Custom Spring Boot Starter

A typical Spring Boot starter contains auto-configuration code and declared dependencies, and it’s extensible via configuration properties in a dedicated namespace (prefix).

By convention, the name of a starter starts with the component name followed by -spring-boot-starter suffix:

org.example.myshop:delivery-spring-boot-starter

Auto-Configurations

The entry point to a Spring Boot starter is the META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file. Spring Boot checks for the presence of the file within your published JAR. The file should list component auto-configuration classes:

org.example.myshop.delivery.DeliveryConfiguration
...

Auto-configurations must be loaded that way only. Make sure that they are defined in a specific package space and that they are never the target of component scanning.

Under the hood, auto-configuration is implemented with standard configuration classes annotated with @AutoConfiguration which supports ordering via attributes such as after, before and similar. Multiple configuration classes could be composed via the @Import annotation:

@AutoConfiguration
@Import(JdbcDeliveryConfiguration.class)
class DeliveryConfiguration {
  ...
}

The auto-configuration creates and registers all necessary Spring beans for the particular component.

A component starter is the only place configurations should exist. Other modules serve different purposes.

Dependencies

A Spring Boot starter contains all dependencies required by the component as whole.

If, for example, a component has a module with Spring Web restful controllers, the starter should contain the corresponding Spring Boot Starter for web:

implementation 'org.springframework.boot:spring-boot-starter-web'

The minimal dependency every Spring Boot starter must include is Spring Boot core starter:

implementation 'org.springframework.boot:spring-boot-starter'

The component starters are then added as dependencies into the application module:

implementation 'org.example.myshop:catalog-spring-boot-starter'
implementation 'org.example.myshop:delivery-spring-boot-starter'
implementation 'org.example.myshop:order-spring-boot-starter'
...

Configuration Properties

Spring Boot provides an alternative method of working with properties that lets strongly typed beans govern and validate the configuration of your application.

@ConfigurationProperties(prefix = "myshop.delivery")
@Setter
@Getter
class DeliveryProperties {

  private String cargoName;
  private String dateFormat;
}

Configuration properties are meant to be a convenient way for initializing auto-configuration:

@EnableConfigurationProperties(DeliveryProperties.class)
@AuthConfiguration
class DeliveryConfiguration {

  @Bean
  DeliveryService deliveryService(
      DeliveryProperties properties
  ) {
    return new DeliveryServiceImpl(
      properties.getCargoName(),
      properties.getDateFormat()
    );
  }
}

The Spring Boot application defines the configuration structure with default values:

# application.yml

myshop:
  delivery:
    cargo-name: PPL
    date-format: yyyy-mm-dd
  order:
    prefix-id: OrderID

Defaults can be overwritten in runtime, for example via environment variables:

MYSHOP_DELIVERY_CARGO_NAME=DHL

Conclusion

Spring Boot is a great tool for developing modular monolithic and microservices applications.

Auto-configurations provide a convenient mechanism for creating independent components in isolation.

Spring Boot starters contain configurations and dependencies for components and define configuration structure via configuration properties with dedicated namespaces.

A Spring Boot application assemblies the components and provides cross-cutting concerns in addition.

Example

An example code of a rich modular Spring Boot application can be found on my GitHub.

Links