1. Overview

Dynamically managing application configurations can be a critical requirement in many real-world scenarios. In microservices architectures, different services may require on-the-fly configuration changes due to scaling operations or varying load conditions. In other cases, applications may need to adjust their behavior based on user preferences, data from external APIs, or to comply with requirements that change dynamically.

The application.properties file is static and can’t be changed without restarting the application. However, Spring Boot provides several robust approaches to adjust configurations at runtime without downtime. Whether it’s toggling features in a live application, updating database connections for load balancing, or changing API keys for third-party integrations without redeploying the application, Spring Boot’s dynamic configuration capabilities provide the flexibility needed for these complex environments.

In this tutorial, we’ll explore several strategies for dynamically updating properties in a Spring Boot application without directly modifying the application.properties file. These methods address different needs, from non-persistent in-memory updates to persistent changes using external files.

Our examples refer to Spring Boot 3.2.4 with JDK17. We’ll also use Spring Cloud 4.1.3. Different versions of Spring Boot may require slight adjustments to the code.

2. Using Prototype-Scoped Beans

When we need to dynamically adjust the properties of a specific bean without affecting already created bean instances or altering the global application state, a simple @Service class with a @Value directly injected won’t suffice, as the properties will be static for the lifecycle of the application context.

Instead, we can create beans with modifiable properties using a @Bean method within a @Configuration class. This approach allows dynamic property changes during application execution:

@Configuration
public class CustomConfig {

    @Bean
    @Scope("prototype")
    public MyService myService(@Value("${custom.property:default}") String property) {
        return new MyService(property);
    }
}

By using @Scope(“prototype”), we ensure that a new instance of MyService is created each time myService(…) is called, allowing for different configurations at runtime. In this example, MyService is a minimal POJO:

public class MyService {
    private final String property;

    public MyService(String property) {
        this.property = property;
    }

    public String getProperty() {
        return property;
    }
}

To verify the dynamic behavior, we can use these tests:

@Autowired
private ApplicationContext context;

@Test
void whenPropertyInjected_thenServiceUsesCustomProperty() {
    MyService service = context.getBean(MyService.class);
    assertEquals("default", service.getProperty());
}

@Test
void whenPropertyChanged_thenServiceUsesUpdatedProperty() {
    System.setProperty("custom.property", "updated");
    MyService service = context.getBean(MyService.class);
    assertEquals("updated", service.getProperty());
}

This approach gives us the flexibility to change configurations at runtime without having to restart the application. The changes are temporary and only affect the beans instantiated by CustomConfig.

3. Using Environment, MutablePropertySources and @RefreshScope

Unlike the previous case, we want to update properties for already instantiated beans. To do this, we’ll use Spring Cloud’s @RefreshScope annotation along with the /actuator/refresh endpoint. This actuator refreshes all @RefreshScope beans, replacing old instances with new ones that reflect the latest configuration, allowing properties to be updated in real-time without restarting the application. Again, these changes aren’t persistent.

3.1. Basic Configuration

Let’s start by adding these dependencies to pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
    <version>4.1.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
    <version>4.1.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.2.4</version>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <scope>test</scope>
    <version>4.2.0</version>
</dependency>

The spring-cloud-starter and spring-cloud-starter-config dependencies are part of the Spring Cloud framework, while the spring-boot-starter-actuator dependency is necessary to expose the /actuator/refresh endpoint. Finally, the awaitility dependency is a testing utility to handle asynchronous operations, as we’ll see in our JUnit5 test.

Now let’s take a look at application.properties. Since in this example we’re not using Spring Cloud Config Server to centralize configurations across multiple services, but only need to update properties within a single Spring Boot application, we should disable the default behavior of trying to connect to an external configuration server:

spring.cloud.config.enabled=false

We’re still using Spring Cloud capabilities, just in a different context than a distributed client-server architecture. If we forget spring.cloud.config.enabled=false, the application will fail to start, throwing a java.lang.IllegalStateException.

Then we need to enable the Spring Boot Actuator endpoints to expose /actuator/refresh:

management.endpoint.refresh.enabled=true
management.endpoints.web.exposure.include=refresh

Additionally, if we want to log every time the actuator is invoked, let’s set this logging level:

logging.level.org.springframework.boot.actuate=DEBUG

Finally, let’s add a sample property for our tests:

my.custom.property=defaultValue

Our basic configuration is complete.

3.2. Example Bean

When we apply the @RefreshScope annotation to a bean, Spring Boot doesn’t instantiate the bean directly, as it normally would. Instead, it creates a proxy object that acts as a placeholder or delegate for the actual bean.

The @Value annotation injects the value of my.custom.property from the application.properties file into the customProperty field:

@RefreshScope
@Component
public class ExampleBean {
    @Value("${my.custom.property}")
    private String customProperty;

    public String getCustomProperty() {
        return customProperty;
    }
}

The proxy object intercepts method calls to this bean. When a refresh event is triggered by the /actuator/refresh endpoint, the proxy reinitializes the bean with the updated configuration properties.

3.3. PropertyUpdaterService

To dynamically update properties in a running Spring Boot application, we can create the PropertyUpdaterService class that programmatically adds or updates properties. Basically, it allows us to inject or modify application properties at runtime by managing a custom property source within the Spring environment.

Before we dive into the code, let’s clarify some key concepts:

  • Environment → Interface that provides access to property sources, profiles, and system environment variables
  • ConfigurableEnvironment → Subinterface of Environment that allows the application’s properties to be dynamically updated
  • MutablePropertySources → Collection of PropertySource objects held by the ConfigurableEnvironment, which provides methods to add, remove, or reorder the sources of properties, such as system properties, environment variables, or custom property sources

A UML diagram of the relationships between the various components can help us understand how dynamic property updates propagate through the application:

PropertyUpdaterService UML Diagram

Below is our PropertyUpdaterService, which uses these components to dynamically update properties:

@Service
public class PropertyUpdaterService {
    private static final String DYNAMIC_PROPERTIES_SOURCE_NAME = "dynamicProperties";

    @Autowired
    private ConfigurableEnvironment environment;

    public void updateProperty(String key, String value) {
        MutablePropertySources propertySources = environment.getPropertySources();
        if (!propertySources.contains(DYNAMIC_PROPERTIES_SOURCE_NAME)) {
            Map<String, Object> dynamicProperties = new HashMap<>();
            dynamicProperties.put(key, value);
            propertySources.addFirst(new MapPropertySource(DYNAMIC_PROPERTIES_SOURCE_NAME, dynamicProperties));
        } else {
            MapPropertySource propertySource = (MapPropertySource) propertySources.get(DYNAMIC_PROPERTIES_SOURCE_NAME);
            propertySource.getSource().put(key, value);
        }
    }
}

Let’s break it down:

  • The updateProperty(…) method checks if a custom property source named dynamicProperties exists within the MutablePropertySources collection
  • If it doesn’t, it creates a new MapPropertySource object with the given property and adds it as the first property source
  • propertySources.addFirst(…) ensures that our dynamic properties take precedence over other properties in the environment
  • If the dynamicProperties source already exists, the method updates the existing property with the new value or adds it if the key doesn’t exist

By using this service, we can programmatically update any property in our application at runtime.

3.4. Alternative Strategies for Using the PropertyUpdaterService

While exposing property update functionality directly through a controller is convenient for testing purposes, it’s generally not safe in production environments. When using a controller for testing, we should ensure that it’s adequately protected from unauthorized access.

In a production environment, there are several alternative strategies for safely and effectively using the PropertyUpdaterService:

  • Scheduled tasks → Properties may change based on time-sensitive conditions or data from external sources
  • Condition-based logic → Response to specific application events or triggers, such as load changes, user activity, or external API responses
  • Restricted access tools → Secure management tools accessible only to authorized personnel
  • Custom actuator endpoint → A custom actuator provides more control over the exposed functionality and can include additional security
  • Application event listeners → Useful in cloud environments where instances may need to adjust settings in response to infrastructure changes or other significant events within the application

Regarding the built-in /actuator/refresh endpoint, while it refreshes beans annotated with @RefreshScope, it doesn’t directly update properties. We can use the PropertyUpdaterService to programmatically add or modify properties, after which we can trigger /actuator/refresh to apply those changes throughout the application. However, this actuator alone, without the PropertyUpdaterService, can’t update or add new properties.

In summary, the approach we choose should align with the specific requirements of our application, the sensitivity of the configuration data, and our overall security posture.

3.5. Using a Controller to Test Manually

Here we demonstrate how to use a simple controller to test the functionality of the PropertyUpdaterService:

@RestController
@RequestMapping("/properties")
public class PropertyController {
    @Autowired
    private PropertyUpdaterService propertyUpdaterService;

    @Autowired
    private ExampleBean exampleBean;

    @PostMapping("/update")
    public String updateProperty(@RequestParam String key, @RequestParam String value) {
        propertyUpdaterService.updateProperty(key, value);
        return "Property updated. Remember to call the actuator /actuator/refresh";
    }

    @GetMapping("/customProperty")
    public String getCustomProperty() {
        return exampleBean.getCustomProperty();
    }
}

Performing a manual test with curl will allow us to verify that our implementation is correct:

$ curl "http://localhost:8080/properties/customProperty"
defaultValue

$ curl -X POST "http://localhost:8080/properties/update?key=my.custom.property&value=baeldungValue"
Property updated. Remember to call the actuator /actuator/refresh

$ curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json"
[]

$ curl "http://localhost:8080/properties/customProperty"
baeldungValue

It works as expected. However, if it doesn’t work on the first try, and if our application is very complex, we should try the last command again to give Spring Cloud time to update the beans.

3.6. JUnit5 Test

Automating the test is certainly helpful, but it’s not trivial. Since the properties update operation is asynchronous and there’s no API to know when it’s finished, we need to use a timeout to avoid blocking JUnit5. It’s asynchronous because the call to /actuator/refresh returns immediately and doesn’t wait until all beans are actually recreated.

An await statement saves us from using complex logic to test the refresh of the beans we are interested in. It allows us to avoid less elegant designs such as polling.

Finally, to use RestTemplate, we need to request the start of the web environment as specified in the @SpringBootTest(…) annotation:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PropertyUpdaterServiceUnitTest {
    @Autowired
    private PropertyUpdaterService propertyUpdaterService;

    @Autowired
    private ExampleBean exampleBean;

    @LocalServerPort
    private int port;

    @Test
    @Timeout(5)
    public void whenUpdatingProperty_thenPropertyIsUpdatedAndRefreshed() throws InterruptedException {
        // Injects a new property into the test context
        propertyUpdaterService.updateProperty("my.custom.property", "newValue");

        // Trigger the refresh by calling the actuator endpoint
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity = new HttpEntity<>(null, headers);
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.postForEntity("http://localhost:" + port + "/actuator/refresh", entity, String.class);

        // Awaitility to wait until the property is updated
        await().atMost(5, TimeUnit.SECONDS).until(() -> "newValue".equals(exampleBean.getCustomProperty()));
    }
}

Of course, we need to customize the test with all the properties and beans we are interested in.

4. Using External Configuration Files

In some scenarios, it’s necessary to manage configuration updates outside of the application deployment package to ensure persistent changes to properties. This also allows us to distribute the changes to multiple applications.

In this case, we’ll use the same previous Spring Cloud setup to enable @RefreshScope and /actuator/refresh support, as well as the same example controller and bean.

Our goal is to test dynamic changes on ExampleBean using the external file external-config.properties. Let’s save it with this content:

my.custom.property=externalValue

We can tell Spring Boot the location of external-config.properties using the –spring.config.additional-location parameter, as shown in this Eclipse screenshot. Let’s remember to replace the example /path/to/ with the actual path:

Eclipse run configuration external properties file

Let’s verify that Spring Boot loads this external file correctly and that its properties override those in application.properties:

$ curl "http://localhost:8080/properties/customProperty"
externalValue

It works as planned because externalValue in external-config.properties replaced defaultValue in application.properties. Now let’s try to change the value of this property by editing our external-config.properties file:

my.custom.property=external-Baeldung-Value

As usual, we need to call the actuator:

$ curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json"
["my.custom.property"]

Finally, the result is as expected, and this time it’s persistent:

$ curl "http://localhost:8080/properties/customProperty"
external-Baeldung-Value

One advantage of this approach is that we can easily automate the actuator call each time we modify the external-config.properties file. To do this, we can use the cross-platform fswatch tool on Linux and macOS, just remember to replace /path/to/ with the actual path:

$ fswatch -o /path/to/external-config.properties | while read f; do
    curl -X POST http://localhost:8080/actuator/refresh -H "Content-Type: application/json";
done

Windows users may find an alternative PowerShell-based solution more convenient, but we won’t get into that.

5. Conclusion

In this article, we explored various methods for dynamically updating properties in a Spring Boot application without directly modifying the application.properties file.

We first discussed using custom configurations within beans, using the @Configuration, @Bean, and @Scope(“prototype”) annotations to allow runtime changes to bean properties without restarting the application. This method ensures flexibility and isolates changes to specific instances of beans.

We then examined Spring Cloud’s @RefreshScope and the /actuator/refresh endpoint for real-time updates to already instantiated beans and discussed the use of external configuration files for persistent property management. These approaches provide powerful options for dynamic and centralized configuration management, enhancing the maintainability and adaptability of our Spring Boot applications.

As always, the full source code is available over on GitHub.