Codersee

Fail Spring Boot App on Missing Environment Variables

A photo is a featured image for post about failing Spring Boot app on missing environment variables and contains a Spring Boot logo in the foreground and a desk setup in the blurred background.

Hello and welcome to my blogπŸ™‚ If you’ve been working with @ConfigurationProperties and environment variables in Spring Boot, then you probably saw that by default, Spring Boot does not fail when the value is missing.

In this article, I will show you four ideas on how to solve this issue.

Please keep in mind that all of them are workarounds, and if you came up with something even better, then do not forget to share in the commentsπŸ™‚

Video Tutorial

If you prefer video content, then check out my video:

If you find this content useful, please leave a subscription  πŸ˜‰

Problem

To be on the same page, let’s take a quick look at a simple Spring Boot configuration with @ConfigurationProperties.

Firstly, let’s navigate to the application.yaml file:

example:
  some-property: ${SOME_VARIABLE}

As we can see, we defined a new property inside our config file for which the value should be set from the environment variable named SOME_VARIABLE.

Following, let’s introduce the @ConfigurationProperties class:

@ConfigurationProperties(prefix = "example")
data class ExampleConfigurationProperties(
  val someProperty: String
)

With this code, we expect that Spring Framework will read all properties defined in the configuration file and inject values into the matching properties.

Note: Spring recognizes the kebab case (some-property) and matches it with the property using camel case (someProperty)


After that, we need to explicitly inform Spring about our configuration properties class:

@Configuration
@EnableConfigurationProperties(ExampleConfigurationProperties::class)
class Config

Lastly, we must add a class, which injects the ExampleConfigurationProperties and prints out the value of someProperty:

@Component
class SomeComponent(
  private val properties: ExampleConfigurationProperties
) {

  @PostConstruct
  fun init() {
    println(properties.someProperty)
  }

}

Finally, when we run our application without the SOME_VARIABLE environment variable missing, we will see the following output:

${SOME_VARIABLE}

A bit counterintuitive, right?

It would seem pretty obvious, that if we declare someProperty as a non-nullable String and environment variable is missing, our Spring Boot app should fail to start.

Unfortunately, that’s not the case when using @ConfigurationProperties and Spring will default the String value to the name of placeholder.

Moreover, at the moment of writing, there is no built-in way we could enforce that.

Solution With Spring Validation

As a first solution (or rather workaround), let’s add the spring-boot-starter-validation to our project:

implementation("org.springframework.boot:spring-boot-starter-validation")

Nextly, let’s instruct Spring that it should default to the blank String value whenever our environment variable is missing:

example:
  some-property: ${SOME_VARIABLE:}

After that let’s modify our configuration properties class:

import jakarta.validation.constraints.NotBlank
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated

@ConfigurationProperties(prefix = "example")
@Validated
data class ExampleConfigurationProperties(
  @field:NotBlank val someProperty: String
)

As we can see, with a combination of @Validated and @NotBlank, we ask Spring Framework to check if the value is not null and contains at least one non-whitespace character.

Note: in Kotlin, we must use the @field: to annotate Java field.

If you would like to learn this, and many more then check out my Kotlin course.


Finally, when we try to run our application, we will see the following:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'example' to com.codersee.properties.ExampleConfigurationProperties failed:

    Property: example.someProperty
    Value: ""
    Origin: class path resource [application.yaml] - 2:18
    Reason: must not be blank


Action:

Update your application's configuration


Process finished with exit code 1

And maybe this solution is nott ideal, but it does its job.

And it is neat. Not only does the application not start, but also whenever the environment variable for our @ConfigurationProperties is missing, we will see a meaningful message that will speed up the debugging process.

Custom ApplicationRunner Idea

Another solution for our problem does not require any additional dependencies added to our project.

Let’s take a look at the following implementation of ApplicationRunner:

import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component

@Component
class MyApplicationRunner : ApplicationRunner {

  companion object {
    private val requiredVariables = setOf(
      "SOME_VARIABLE"
    )
  }

  override fun run(args: ApplicationArguments) {
    requiredVariables.forEach {
      System.getenv(it)
        ?: throw MissingEnvVariableException(it)
    }
  }

}

class MissingEnvVariableException(variableName: String) :
  RuntimeException("Application failed to start. Missing environment variable: $variableName")

This time, we introduce our set of required environment variables and check if they are present.

Whenever it’s missing, we throw the MissingEnvVariableException and prevent the app from starting:

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:765) ~[spring-boot-3.1.3.jar:3.1.3]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:752) ~[spring-boot-3.1.3.jar:3.1.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:319) ~[spring-boot-3.1.3.jar:3.1.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.1.3.jar:3.1.3]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.1.3.jar:3.1.3]
	at com.codersee.properties.PropertiesApplicationKt.main(PropertiesApplication.kt:13) ~[main/:na]
Caused by: com.codersee.properties.MissingEnvVariableException: Application failed to start. Missing environment variable: SOME_VARIABLE
	at com.codersee.properties.MyApplicationRunner.run(MyApplicationRunner.kt:19) ~[main/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762) ~[spring-boot-3.1.3.jar:3.1.3]
	... 5 common frames omitted


Process finished with exit code 1

This solution gives us a bit more control and allows us to customize the behavior better.

Would we like to log an error, or maybe send an alert request? No problem. With a few lines of code, we can easily achieve that.

Nevertheless, with this approach, we introduce a new, separate place in the code we need to remember every time we want to add a new environment variable. And this might be easily forgotten in bigger projects.

Fix With @Conditional

As the next step, let’s take a look at the approach with @Conditional annotation, which slightly reduces the chance of forgetting about additional checks.

Firstly, let’s add a new class called RequiredEnvVariablesCondition:

class RequiredEnvVariablesCondition : Condition {

  private val logger: Logger = LogManager.getLogger(this::class.java)

  companion object {
    private val requiredVariables = setOf(
      "SOME_VARIABLE"
    )
  }

  override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
    val isEnvVariableMissing = requiredVariables
      .any { envVariableName ->
        val isMissing = System.getenv(envVariableName) == null

        if (isMissing)
          logger.error("Variable $envVariableName is missing!")

        isMissing
      }

    return !isEnvVariableMissing
  }

}

As we can see, this logic is pretty similar to what we’ve done previously.

Every time the environment variable is missing, we log an error message and return false from the overridden matches function.

So with that done, we can annotate our config class with @Conditional:

import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(ExampleConfigurationProperties::class)
@Conditional(RequiredEnvVariablesCondition::class)
class Config

This time, we will get the following output if we try to run the app without passing the environment variable:

2023-09-14T07:20:58.813+02:00 ERROR 16188 --- [           main] c.c.p.RequiredEnvVariablesCondition      : Variable SOME_VARIABLE is missing!

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.codersee.properties.SomeComponent required a bean of type 'com.codersee.properties.ExampleConfigurationProperties' that could not be found.


Action:

Consider defining a bean of type 'com.codersee.properties.ExampleConfigurationProperties' in your configuration.


Process finished with exit code 1

(Bonus) Use @Value Instead

There’s no doubt that @ConfigurationProperties helps us write cleaner code and better organize configuration in our codebase. Moreover, we can effortlessly add new values to the project with this approach.

However, in some cases, we should consider using @Value annotation instead:

package com.codersee.properties

import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component

@Component
class SomeComponent(
  @Value("\${example.some-property}") private val someProperty: String
) {

  @PostConstruct
  fun init() {
    println(someProperty)
  }
}

This time, we can completely get rid of @Configuration and @ConfigurationProperties classes.

Instead of injecting a whole configuration object, we focus on the desired config value.

And whenever it’s missing, we get the following output out of the box:

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-09-14T07:16:43.532+02:00 ERROR 18412 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'someComponent' defined in file: Unexpected exception during bean creation
...
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'SOME_VARIABLE' in value "${SOME_VARIABLE}"
...
Process finished with exit code 1

As we can see, our Spring Boot app won’t start if the environment variable is missing and we will get a meaningful message.

But again, we should use @Value wisely and most of the time @ConfigurationProperties can be a better approach.

Summary

And that’s all for this article on how to fail a Spring Boot app on missing environment variables.

As I mentioned in the beginning- these are just a few ideas that came to my mind. If you figure out some other way, then please share them in the comments section below.

As always, you can find the source code in this GitHub repository.

Thank you for reading, and see you next time! πŸ™‚

Share this:

Hi there! πŸ‘‹

Hi there! πŸ‘‹

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Related content

Newsletter

Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy

Prefer videos?

Check out my YouTube channel:

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.