Properties-Driven Application with Spring Boot

Janani Subbiah
codeburst
Published in
6 min readJan 14, 2021

--

Building an application that is driven by properties using Spring Boot.

Photo by Arno Smit on Unsplash

This post counts as the second part of my previous post on some very commonly used annotations in the Spring framework. In this post I wanted to specifically focus on how we can build backend services using Spring/Spring Boot and drive certain sections of the application using properties, that could also be supplied from outside the codebase. When I say properties driven, at a very high level I mean the ability to change the behavior of the application by merely using properties. For example, if we were to build a backend service that would allow login functionality using third party services (say login using Service X or Service Y) and at any given time only one of them would be available and we want the ability to switch between the two options using properties from outside the application.

With a requirement like this, it becomes clear that we need a way from outside the app to dictate to the app which login to enable. We achieve this using spring’s support for properties (which can be .properties or .yaml files). These properties are placed in the src/main/resources directory and start with the word application followed by a hyphen and the name of the profile (if you would like to read more about it please refer to Spring’s documentation on profiles).

For this post I am using the following versions of tools and frameworks:

  • Spring Boot: 2.4.1
  • Kotlin: 1.4.21
  • JDK: 15
  • Gradle: 6.7.1

Without further ado, here is how we create a Spring Boot Kotlin application that can conditionally switch between services using externalized properties:

Step 1: Come up with a custom property key and value

The first thing we do is to come up with a custom property name to indicate the login service we want active. We can call it authentication.service(make sure the property name is already taken. You can refer to Spring’s list of properties to make sure). Next, we define possible values for this property. In our case, if we were to consider only Service X/Service Y, we can define the allowed values to be service-xor service-y. So if we wanted to use Service X for login, we would add the following to our application.YAML (I am using the yaml file format for this example) file:

authentication.service: service-x

And to use Service Y for login:

authentication.service: service-y

Step 2: Define an interface for the functionality

Next, we define an interface for the login functionality. This interface can be super simple and contain only one method that basically executes the login function. Note that all classes that require a login functionality will only reference the AuthenticationService and NOT the Service X or Service Y specific implementations.

interface AuthenticationService {    fun login()
}

Step 3: Add service-specific login functionality

In this step, we add a class to allow logging in using Service X and another class that allows logging in using Service Y. Note that both these classes will implement the interface we defined in Step 2.

class ServiceXAuthService: AuthenticationService {    fun login() {    
// Functionality to execute login using Service X
}
}
class ServiceYAuthService: AuthenticationService {fun login() {
// Functionality to execute login using Service Y
}
}

Step 4: Read authentication service keys into an object

In order to use APIs or SDKs from a third-party service, we are going to need to define a class to hold those properties that can then be used by the ServiceXAuthService and ServiceYAuthService to talk to services X and Y respectively.

Let us also make a couple more assumptions here: Service X requires an API key for authentication and Service Y requires an API key and API secret. So we will create a class to hold service X’s properties.

@ConstructorBinding
@ConfigurationProperties(prefix = "service-x")
data class ServiceXProperties(
val apiKey: String
)

and one to hold service Y’s properties:

@ConstructorBinding
@ConfigurationProperties(prefix = "service-y")
data class ServiceYProperties(
val apiKey: String
val apiSecret: String
)

The annotations, @ConstructorBinding and @ConfigurationProperties are the key here. @ConstructorBinding is used to indicate that the properties from the properties or YAML file will need to be read into this object using its constructor and the @ConfigurationProperties annotation indicates the prefix of the property that needs to be read from the YAML or properties file.

To ensure the properties are picked up and set on the classes as expected, add the following dependency to your dependencies section in the build.gradle.kts file (for more info refer to this doc):

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

Next, we will update the service-specific files to provide the authentication credentials:

class ServiceXAuthService(
private val authProperties: ServiceXProperties
): AuthenticationService {
fun login() {
// Functionality to execute login using Service X
}
}
class ServiceYAuthService(
private val authProperties: ServiceYProperties
): AuthenticationService {
fun login() {
// Functionality to execute login using Service Y
}
}

Step 5: Providing Property values

Now that we have all the classes in place, we provide values for these properties in the application.yaml (or application.properties file. I am going to use yaml files as examples here).

If we wanted to use Service X’s login service:

authentication.service: service-xservice-x:
apiKey: X-Key
apiSecret: X-Secret

Instead, if we wanted to use Service Y as the login service provider the application.yaml file will look similar to:

authentication.service: service-yservice-y.apiKey: Y-Key

Step 6: Configuration magic

This is the crux of this post. After all the code for the functionality has been added, we now write a configuration class that will decide which bean (ServiceXAuthService or ServiceYAuthService) will be available to all classes that require the use of the login functionality. Spring’s set of Conditional annotations make it possible for us to read the property authentication.service and create the ServiceXAuthService if the value is service-x or the ServiceYAuthService if the value is service-y.

// For Service X Auth Service@Configuration
@EnableConfigurationProperties(ServiceXProperties::class)
@ConditionalOnProperty(
prefix = "authentication",
name = ["service"],
havingValue = "service-x"
)
class ServiceXAuthConfiguration(
private val serviceXProperties: ServiceXProperties
) {
@Bean
fun xAuthService(): AuthenticationService = ServiceXAuthService(serviceXProperties)
}

And in order to wire in Service Y’s login functionality

// For Service Y Auth Service@Configuration
@EnableConfigurationProperties(ServiceYProperties::class)
@ConditionalOnProperty(
prefix = "authentication",
name = ["service"],
havingValue = "service-y"
)
class ServiceYAuthConfiguration(
private val serviceYProperties: ServiceYProperties
) {
@Bean
fun yAuthService(): AuthenticationService = ServiceYAuthService(serviceYProperties)
}

The @EnableConfigurationProperties in conjunction with @Configuration allows creating a bean of the type specified by @EnableConfigurationProperties (in this case, it would be either the ServiceXProperties or the ServiceYProperties).

It is also possible for users to skip the configuration class and directly put these annotations along with an @Component annotation on the ServiceXAuthService and ServiceYAuthService classes. This is a matter of personal preference. I like to use a configuration class for a few reasons:

  1. It is very obvious which classes are exposed as beans in the application, and this information is held in a central location.
  2. If there are additional beans/classes that are perhaps required for specific conditions, for example if we need another class, that would serve as a utility class called, ServiceXAuthLoginHelper and we only need this when we have authentication.service set to service-x, we could create two configuration classes one for Service X and another for Service Y and the configuration class specifically for Service X login can contain the code to create and expose both these classes as beans and the conditional annotation can be applied to the entire class.
  3. Thanks to Spring’s out-of-the-box support for testing configuration classes, it is very easy to unit test configuration classes with conditional logic to make sure things work as expected. This blog post explains very clearly how we can write tests for Configuration classes.

There are multiple variants of the ConditionalX annotation provided by Spring that could be based on beans, classes, etc. Refer to the Annotation Types Summary of this documentation for more information.

Step 7: Making the property an environment variable

By now you are probably wondering how we can switch between preferred login services without a code change. After all, we need to change the value authentication.service for all the conditional magic to work. Well, this is probably the second most important step of this post. We make the value of authentication.service an environment variable, hence making it injectable from outside the application!

authentication.service: ${authentication.service.name}

Now we can switch between services X and Y logins by providing either service-x or service-y as the value for the authentication.service.name environment variable and provide the properties each of those services may need and start the app up to see the correct service created and exposed for dependency injection. A bean of the other service will not be created!

To read more about externalized configuration with Spring Boot please refer to their doc on this topic.

Conclusion

I hope you found this blog post insightful on how to achieve properties driven conditional application behavior using Spring Boot!

--

--