Service

  • Technology and Engineering

Blog

by Jens Neubert and Alexander Metzner

Spring Boot Starters - the ingredient for connecting your infrastructure

In a micro-services world, you can easily get lost while connecting all your services with the infrastructure. Soon you may find yourself solving the same problem over and over again. While the classic code reuse pattern can help you out a lot, the Spring Boot, on the other hand, offers help that is at a new level, making the reuse of shared code much simpler.

Creating a micro-services-based system architecture seems to be a no-brainer decision these days (assuming you build some kind of web application). But soon, you’ll notice that there are a lot of cross-cutting concerns that need to be part of every service. Many of these concerns deal with the infrastructure – like logging, monitoring, and liveness probes, to name a few.

How do we manage to not re-implement these concerns every time we start a new service? Sounds like a classic code reuse thing. But isn’t there something more convenient than adding a library to every service?

In the world of Spring Boot, there are starters!

Starters are a set of convenient dependency descriptors that you can include in your application. You get a one-stop-shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors.

A one-stop-shop – providing a single dependency, and my system is loaded with additional functionality – that sounds perfect. But what if the shop isn’t selling the product you need? What if the starters available do not match your requirements?

Fortunately, Spring Boot allows creating starters on our own, which work the same as the starters provided by Spring Boot itself. So, coming back to the quote before, it’s time to add products to this one-stop shop.

Short detour: Why do we need custom starters?

This article is a summary of a project’s history we went through some time ago. It’s crucial to know how the project was set up from an infrastructure perspective to understand why we decided to create our own starters. So, let’s look at the following picture, which shows the most relevant aspects.

  • Same technology stack
    All of the applications that are part of the project share a common technology stack. That’s a hard requirement. For the main part, we had to develop Java applications based on the Spring Boot stack and a React-based UI (but that’s not relevant to this article). All the software artifacts are delivered to the customer as a Docker container and are run on an OpenShift cluster.
  • Developing artifacts in parallel
    Our services are developed by different international teams in parallel and we don’t want them to implement identical functionalities. So we want to set standards for these teams. 
  • Common infrastructure setup
    All the systems share common concerns, like how the user authenticates or how the server collects and visualizes all the logging data.
  • Operations done by a different team
    The deployment and operations tasks for the software created are being taken care of by another team working at a different location for a different company. This means that once the software was released, the packages were transferred to a different team to be deployed.

The very first Starter

As mentioned above, we deliver our software as container images to a different team to be installed in an OpenShift-container platform. Running containers in a container platform requires every application to provide health status information for the platform to know if a container is still running. (Well, strictly speaking, it’s not really a hard requirement but only a strong recommendation. But for the scope of this project, it turned into a hard requirement). Usually, this is done by implementing probes. Typically, you want to implement two probes:

Readiness probe

“A readiness probe determines if a container is ready to accept service requests. If the readiness probe fails for a container, the kubelet removes the pod from the list of available service endpoints. After a failure, the probe continues to examine the pod. If the pod becomes available, the kubelet adds the pod to the list of available service endpoints.”

Liveness probe

“A liveness probe determines if a container is still running. If the liveness probe fails due to a condition such as a deadlock, the kubelet kills the container. The pod then responds based on its restart policy. For example, a liveness probe on a pod with a restart Policy of Always or On Failure kills and restarts the container.”

A common choice to implement these kinds of tests, besides using tcp sockets or container commands, is HTTP requests.

The operations team, coming from a rather classical JEE application server background, soon created specifications regarding how these probes MUST be implemented. They wrote specs on the method (HTTP GET), the URL paths, the return codes, and what parts of an application should be considered for each probe. Unfortunately, these specs were almost impossible to implement using the Spring Boot Actuator. So we started looking at how to provide something like the Actuator that matched our specs.

So what does it take to create a starter?

Let’s get started with a custom Spring Boot starter by looking at a starter’s ingredients. The Spring framework documentation lists the following parts:

own starter spring bootLet’s spend some time looking into these parts.

Dependencies

Dependencies are references to other pieces of software that a starter requires to compile, run the tests, and finally run as part of a Spring Boot application. Dependencies in starters work exactly the same way they do in regular applications. You list the dependencies, including their versions and the scope in the starter’s build file (i.e., the build.gradle or pom.xml file).

You should spend a little more time thinking about a starter’s dependencies though. Since a starter is typically used in multiple applications (otherwise, it might become a little “over-engineered,” creating a starter for just one application), each dependency your starter declares will become a transitive dependency for every application where you use the starter. This can quickly bloat your applications when multiple starters deploy a set of dependencies. In general, it’s good advice to keep the list of dependencies short – and this becomes even more important when creating custom starters.

For our starter, things are straightforward, and we only need a dependency for Spring Boot and the web layer, which we declare as the scope provided. Using the provided scope makes sure that the dependency is not added to the application’s dependencies but must be specified by the application itself.

Auto-configuration

This is where the magic happens! Auto-configuration is what turns a plain library with Spring Boot related code into a Spring Boot starter. Basically, auto-configuration is a way of registering one or more @Configuration classes to be picked up during application startup. This gives your starter the chance to do whatever you want to do when the application starts.

To be recognized by the starting Spring Boot application, you have to add a file called spring.factories in your project’s META-INFdirectory and include a reference to your auto-configuration class in that file. The Spring documentation has all the details.

For our starter, we define a single configuration class that extends the package scan path to pick up Spring components from the starter and define other @Beandefinitions.

Useful application defaults

Most starters will require some kind of configuration to work. Providing useful defaults greatly improves the convenience for users of your starter.

Our starter uses several values from the specification as defaults. This means that applications can override these to use the starter in different setups, but overriding nothing allows the starter to be used in its designed context.

Conditionals

Conditionals are a very powerful feature provided by the core of the Spring framework. Conditionals allow you to apply a certain kind of configuration (such as a bean definition) conditionally. In a typical Spring Boot application, this is rarely needed. However, most developers may find themself using this feature to toggle definitions when executing automated tests (i.e., use a mock instead of a regular bean).

When creating a custom starter, conditionals become extremely powerful. They allow you to write a single starter to run in a wide range of applications. Remember the slogan from the intro: “one-stop-shop”? Conditionals are a crucial part of turning a useful starter into a real one-size-fits-all starter.

Spring provides conditionals that allow you to toggle the configuration based on configuration properties (present or not), defined bean (present or not – although it is great to provide some kind of default bean and let the application override it), whether the application contains a web layer and much more. See the javadoc package for org.springframework.boot.autoconfigure.condition to see what Spring provides.

Our starter uses @ConditionalOnWebApplication to decide whether to contribute a web controller (handling the default endpoints). It also uses @ConditionalOnMissingBean to contribute a default bean implementation based on some conventions; the application is free to provide a custom implementation, in which case, the default bean definition is simply skipped.

Metadata for configuration

You might want to assist the user in configuring your starter, so you have two options to do so:

  • Define the additional information in META-INF/spring-configuration-metadata.json
  • Include the dependency for the spring-boot-configuration-processor in your build configuration, and add Javadoc to your @ConfigurationProperties annotated classes

For further details, look into the document explaining the configuration of metadata.

Is this all I need to create a custom Spring Boot starter?

Yes, that is all that is necessary.

But that doesn’t mean you have to stop here. Spring provides many great features you can use to create a starter that is even more convenient to use. To give you an example, we look into two elements used in our starter that provide a great experience to the starter’s users.

The first one is called custom qualifier annotations. Custom qualifier annotations are a great way of creating a shortcut used on bean instances. Spring also provides the ability to collect these bean instances and inject them into another bean or bean factory method. For our starter, we would like the applications to contribute parts of the readiness probe. To do so, we created a couple of interfaces and classes that are part of the starter and define the API of a single part, which we call a check. A check is a class implementing a given interface that must be registered to a CheckExecutor.

To make this process more convenient, we declared an annotation Check like the following:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier
public @interface Check {
}

Besides all the standard Java annotations, the interesting one here is @Qualifier, imported from Spring. This annotation is used by Spring to collect all beans that use this annotation. The following is a sample from our @Configuration class:

@Bean
@ConditionalOnMissingBean
public CheckRegistry checkRegistry(@Check final Collection<Check> checks) {
  // ...
}

Spring automatically creates the beans and passes them into our bean factory method, which will add the beans to the registry. Using this setup wrapped into an auto-configuration class, the application may use code like the following to create and register a single check for the readiness probe:

@Bean
@Check
public Check urlCheck() {
  return Checks.url("https://...");
}

That’s all the application needs to do! This is what we mean by “one-stop-shop”.

Another feature we use is Spring’s support for aspect-oriented programming. This is another way to improve the application programmer’s experience greatly. Spring uses the AspectJ project to create aspects. The code involved in getting this working is a little more complex, so we do not show it up here. You can find a great tutorial on how to create your own aspects out there. But we’d like to give you an idea of how we use the aspects to create a nice developer experience. Suppose you have an operation that fails from time to time (maybe due to a timeout). A certain number of failures within a specific time are acceptable for the service to remain ready, but the service should be marked as not ready once the limit has been overridden. It’s easy to imagine creating a Check with a counter-variable and some timer code to reset the counter. But that’s a lot of repeated work to do. Here is a much simpler approach using a custom aspect:

@Service
public class SomeService {
  @MaxFailuresCheck(maxFailures = 10, duration = "1m")
  public void someMethodThatMightFail () throws TimedOutException {
    // ...
  }
}

This code sample shows an implementation of a service with a single method that can fail by throwing a TimedOutException. The annotation marks the method to be part of Check, which causes the application to become unready whenever more than ten timeouts are being hit within one minute.

As before, this shows how easy it is for a developer to use the functionality provided by the starter.

Sounds like the starter really saved you a lot of work

It does, and it will save a lot of time and work in the future with every new application we kick off.

But we also had our lessons to learn.

First, we created a single Spring Boot starter to collect all the infrastructure support we made. Soon we realized that the starter became too big to handle. We found ourselves adding tons of configuration switches to allow using only a single part of the starter. We had to set up many conditionals to get this to work. Following the single responsibility principle, we split the starter into multiple ones that support one kind of infrastructure need, i.e.,

  • metrics
  • logging
  • health checks
  • login

Another lesson learned was that starters should only use the most common dependencies. As an example, we use Java 1.8 and Java 11 for the applications. All of our starters have to use Java 1.8 to use them in all the applications. The same goes for Kotlin: Although we use Kotlin to create some applications, all of our starters remain to use Java as we still have applications being written in Java (and it doesn’t feel very nice adding tons of Kotlin dependencies to a plain Java app).

Summary

To summarize, what advice can we give a starter developer?

  • The value of a starter increases with every application you build on the same stack that shares a common infrastructure with the others.
  • They help you standardize the way your applications interact with common infrastructure.
  • Use the starter to connect your applications to infrastructure, do not use them to create shared business logic.
  • Check if a starter already exists that fulfills your needs before you create one.
  • Keep them small, easy to configure, and do not use other libraries too extensively.

We think the last piece of advice is more important than you might first think. Developers tend to implement their own things if they believe some sort of library, framework, or even Spring Boot starter is too complicated and that they can solve the problem “better” by writing their own code.

Lastly, we hope you enjoyed reading our starter story and are motivated to create one.

Related content

Blog

  • Data and AI
  • Technology and Engineering

Solution Specialist experience: low-coding and AI in IT

In this blog, our Solution Specialist Semi discusses the benefits of today’s game-changers – low-code development and AI – and what they offer for both our customers and our developers.

Blog

#d human cell with code in it
  • Data and AI
  • Technology and Engineering

Blog

Stairs in a triangle

The Iron Triangle as Forcing Function

The Iron Triangle of project management asserts that Scope, Cost, and Time represent dimensions fundamentally and irrevocably bound together such […]

Get in touch

Let us offer you a new perspective.