Create your own
Lesson illustration

Dynamic Configuration with Spring Cloud Bus

Hello! Welcome back to our module on Observability & Monitoring.

In our last lesson, we took a big step towards production-readiness by setting up a Spring Cloud Config Server. We successfully externalized our application properties into a centralized Git repository. However, we ended on a cliffhanger: to see any configuration changes, our client applications needed a full restart. In a cloud-native environment with potentially hundreds of service instances, this is not a viable strategy.

Today, we solve that exact problem. Our learning outcome is to implement dynamic configuration refresh using Spring Cloud Bus to update properties without a service restart. We will link our microservices through a message broker, enabling a change to be broadcast and applied across the entire system instantly. This is a critical pattern for achieving zero-downtime configuration updates and a concept you'll likely be asked about in senior-level interviews.

1. The Scalability Problem with Manual Refresh

In the previous lesson's resources, you might have noticed the /actuator/refresh endpoint. This endpoint is powerful; when called, it forces a single application instance to discard its cached configuration and pull the latest values from the Config Server.

However, consider a service that is scaled out to 20 instances for high availability. If a configuration value changes, you would need to make a POST request to /actuator/refresh on all 20 instances. This is tedious, error-prone, and doesn't scale.

This is the exact problem Spring Cloud Bus is designed to solve.

Spring Cloud Bus — Dynamically manage and refresh ...

To better understand this challenge, let's start with this article by Anupriya Kumawat, which clearly frames the problem.

Please read the section titled 'Need of Spring Cloud Bus'. It perfectly describes the scenario of having to call the refresh endpoint on multiple service instances and introduces the Bus as the solution.

2. Spring Cloud Bus: A Distributed Actuator

Spring Cloud Bus works by connecting all microservice instances to a shared message broker, such as Kafka or RabbitMQ. When a refresh event is triggered on any single instance, instead of just refreshing itself, it publishes an event to the message broker. All other instances listening on the bus receive this event and trigger their own refresh process.

It acts like a distributed notification system for your microservices. The architecture looks like this:

Spring Cloud Bus with Kafka for Dynamic Microservices Configuration Refresh
This diagram shows the complete flow for dynamic configuration refresh. A change is made in a Git repository (1), which can trigger the Config Server to publish a refresh event to a Kafka topic (2). Microservice clients (A, B, C) are subscribed to this topic. When they receive the event (3), they all independently contact the Config Server to pull the latest configuration (4), all without requiring a restart.

This mechanism is not just for configuration; the bus can be used for any kind of state change propagation or broadcasting commands between services.

3. How Does a Bean Magically Update Itself?

Before we jump into the implementation, let's address a key question that's crucial for interviews: how does a Spring bean, which is a singleton by default, suddenly get new property values while the application is running?

The answer lies in the @RefreshScope annotation and runtime proxies.

When you annotate a bean with @RefreshScope, Spring doesn't inject the actual bean instance into other components. Instead, it injects a proxy. This proxy wraps the real bean. Here’s how it works:

  1. When a method is called on the proxied bean, the proxy intercepts the call.
  2. It looks up the current, real bean instance from a special cache (the refresh scope cache).
  3. It then delegates the method call to that real instance.
  4. When a bus refresh event occurs, Spring destroys the old bean instance in the cache and clears it.
  5. The next time a method is called on the proxy, it finds the cache empty, creates a new instance of the bean (injecting the newly loaded properties), places it in the cache, and delegates the call.

The components using your bean never know this happened; they hold a reference to the same stable proxy throughout. This is a powerful application of the Proxy design pattern.

Spring Boot Context Refresh in Cloud

This article by Alexander Obregon provides an excellent deep dive into the mechanics of @RefreshScope.

Please read the section 'How @RefreshScope Works Internally'. Focus on understanding the role of proxies and why they are essential for this mechanism to work without breaking dependencies in your application.

This underlying mechanism is triggered by an EnvironmentChangeEvent, which is what Spring Cloud Bus ultimately causes to be fired inside each listening application.

4. Implementation with Spring Cloud Bus and Kafka

Now, let's add this capability to the client and server applications we built in the previous lesson. We'll use Kafka as our message broker, as it's a common choice in modern microservice architectures.

Prerequisite: You'll need Kafka and Zookeeper running. If you have Docker, the quickest way is to use a docker-compose.yml file like the one provided by Confluent here. You can save it and run docker-compose up -d.

Step 1: Add Dependencies

You need to add the Spring Cloud Bus dependency with the Kafka binder to all your client microservices that should participate in the dynamic refresh.

In your client application's pom.xml:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>

Step 2: Configure the Client

In your client's src/main/resources/application.properties, add the properties to enable the bus and connect to Kafka.

# Previous configuration from last lesson
spring.application.name=my-client-app
spring.config.import=optional:configserver:http://localhost:8888

# --- Spring Cloud Bus Configuration ---
# Point to your Kafka broker
spring.cloud.stream.kafka.binder.brokers=localhost:9092
# Point to your Zookeeper (some older binders might need this)
spring.cloud.stream.kafka.binder.zkNodes=localhost:2181

# Expose bus endpoints via actuator
management.endpoints.web.exposure.include=bus-refresh

Note: The zkNodes property is often not required with modern versions of the Kafka binder but is included here for completeness.

Step 3: Annotate Your Bean

Ensure the bean that uses the externalized properties is annotated with @RefreshScope. Let's refactor our controller from the last lesson slightly to follow best practices, moving the logic to a service component.

MessageService.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Service;

@Service
@RefreshScope
public class MessageService {

    @Value("${greeting.message:Hello default}")
    private String message;

    public String getMessage() {
        return this.message;
    }
}

MessageRestController.java (Updated)

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
class MessageRestController {

    private final MessageService messageService;
    
    public MessageRestController(MessageService messageService) {
        this.messageService = messageService;
    }

    @RequestMapping("/message")
    String getMessage() {
        return this.messageService.getMessage();
    }
}

With @RefreshScope on MessageService, a refresh event will now create a new instance of this service, while the MessageRestController remains untouched.

5. Verifying the Dynamic Refresh

Let's test the complete flow.

  1. Ensure your Kafka broker and Config Server are running.
  2. Start two instances of your client application on different ports.
    # Terminal 1
    mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081
    
    # Terminal 2
    mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8082
    
  3. Check the initial message on both instances:
    • curl http://localhost:8081/message -> Hello from the config held in Git!
    • curl http://localhost:8082/message -> Hello from the config held in Git!
  4. Go to your Git configuration repository, change the greeting.message property to something new (e.g., Hello from the refreshed config!), and commit the change.
  5. Now, trigger the broadcast by sending a POST request to the /actuator/bus-refresh endpoint on just one of the instances.
    curl -X POST http://localhost:8081/actuator/bus-refresh
    
  6. Wait a few seconds, and then check the message on both instances again.
    • curl http://localhost:8081/message -> Hello from the refreshed config!
    • curl http://localhost:8082/message -> Hello from the refreshed config!

Success! Both instances picked up the change from a single trigger, with no restarts required.

Test your understanding!

A teammate reports that they've added @RefreshScope to a @Component, but the configuration properties injected via @Value are not updating after a /bus-refresh event. What is a likely reason that the MessageService in our example was refreshable, but their component is not?

Show answer

The most likely reason is that their non-refreshing component is being injected into a standard singleton bean (like a @RestController) which is not itself marked with @RefreshScope.

The proxy magic of @RefreshScope only works if the bean is accessed through its proxy. If another singleton bean takes a direct reference to the initial instance of the @RefreshScope bean at startup (which can happen under certain dependency injection scenarios, especially constructor injection in older Spring versions or field injection), it might hold onto that old instance.

By ensuring our MessageRestController injects the MessageService via constructor injection, Spring correctly injects the proxy, not the underlying instance, ensuring that every call to messageService.getMessage() is correctly delegated to the latest version of the bean. Another possible issue is if the @Configuration class that creates the bean is not being processed correctly or if the bean is marked as final.

Conclusion

You have now implemented a robust, scalable mechanism for managing configuration in a distributed system. By leveraging Spring Cloud Bus, you can ensure that all service instances react to configuration changes dynamically and consistently. Understanding both the implementation (spring-cloud-starter-bus-kafka) and the theory (@RefreshScope proxies) prepares you well for deep technical discussions in an interview.

Key Takeaways:

  • Manually refreshing configuration with /actuator/refresh does not scale.
  • Spring Cloud Bus connects services via a message broker (like Kafka) to broadcast state changes.
  • The /actuator/bus-refresh endpoint triggers an event that is published to all listening services.
  • @RefreshScope enables beans to be destroyed and re-created at runtime without breaking the application by using a proxy-based mechanism.
  • This pattern allows for zero-downtime updates to configuration properties across a fleet of microservices.

Next Up

Now that our services are running and their configurations can be managed dynamically, the next challenge is understanding what they are doing. When a user's request fails, how do we trace its path through multiple microservices to find the source of the error? In the next lesson, we will begin to answer this by implementing structured logging with correlation IDs for end-to-end request troubleshooting.

Can't find a good explanation? Sign up and we'll make it for you

Sign up