Skip to content

@RefreshScope Deep Dive | Auto Reload Config Without Restart

The Restaurant Menu Board Analogy

Imagine a fast casual cafe with a digital chalkboard displaying daily soup specials and prices. In an antiquated restaurant, the menu prices are painted in permanent oil paint directly onto the wall. If the price of milk changes or the soup special sells out, the manager has to shut down the entire restaurant, send all customers home, hire a painter to sand down the wall, paint the new prices, wait for the paint to dry, and reopen the doors three hours later.

In a modern cafe, the menu is displayed on an electronic digital screen connected to a central office computer. When prices change, the manager presses an update button on the keyboard. The screen refreshes instantly without closing the restaurant, without interrupting current diners, and without disturbing employees.

In Spring Boot, standard beans are like that permanent oil paint: their @Value properties are injected once at startup and remain cached in memory for the lifetime of the JVM. If a configuration property changes in Git or your external config server, the traditional response is restarting the entire microservice. @RefreshScope is that digital screen: it wraps your bean in a dynamic proxy, allowing you to reload updated configuration properties at runtime via a simple HTTP endpoint with zero server restarts and zero downtime.

This lecture covers why standard Spring singletons cache configuration, how @RefreshScope works under the hood using CGLIB proxies, the /actuator/refresh endpoint, and thread safety considerations.


The Problem: Why Standard Spring Beans Cannot Reload

By default, all beans in Spring Boot are singletons:

java
@Service
public class DiscountService {

    @Value("${discount.rate:0.10}")
    private double discountRate;

    public double calculateDiscount(double price) {
        return price * discountRate;
    }
}

When Spring Boot starts up:

  1. The container instantiates DiscountService.
  2. It resolves ${discount.rate} from application.properties (e.g. 0.10).
  3. It sets discountRate = 0.10 via reflection.
  4. The bean is placed into the singleton registry.

If you subsequently modify discount.rate=0.20 in your Git repository or Spring Cloud Config Server, nothing happens to DiscountService. The value 0.10 is already hardcoded into the instance fields of the existing singleton in heap memory. The only way to reload it traditionally is terminating the JVM process and restarting the container.

In high availability systems with continuous traffic, restarting twenty microservice instances just to update a feature flag or timeout threshold is undesirable.


How @RefreshScope Works Under the Hood

@RefreshScope is a custom bean scope provided by Spring Cloud Context (org.springframework.cloud.context.config.annotation.RefreshScope):

java
package com.example.orderservice.service;

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

@Service
@RefreshScope
public class DiscountService {

    @Value("${discount.rate:0.10}")
    private double discountRate;

    public double calculateDiscount(double price) {
        return price * discountRate;
    }
}

The Proxy Architecture

When you annotate a bean with @RefreshScope:

  1. Spring does not give callers a direct reference to the real DiscountService object.
  2. Spring creates a CGLIB dynamic proxy that stands in front of the real bean.
  3. The proxy intercepts all method calls (such as calculateDiscount()).
  4. The proxy delegates the call to a hidden underlying target object held inside a cache managed by GenericScope.
Caller ---> [ CGLIB Proxy for DiscountService ]
                        |
                 (Checks Cache)
                        |
            +-----------+-----------+
            |                       |
       [ Cache HIT ]          [ Cache MISS / Invalidate ]
            |                       |
[ Real Bean Instance ]     1. Destroys old instance
(Holds discountRate=0.10)  2. Reads fresh config from environment
                           3. Instantiates NEW Real Bean Instance
                           4. Injects fresh discountRate=0.20
                           5. Places new bean into Cache

When you trigger a refresh, Spring simply evicts the target instance from the proxy's cache. The next time any thread invokes a method on the proxy, the proxy sees a cache miss, instantiates a brand new target bean, injects the fresh properties, and executes the method. The caller has no idea the underlying instance was swapped.


Triggering Refresh via Actuator

1. Add Actuator Dependency

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

2. Expose the /actuator/refresh Endpoint

By default, sensitive actuator endpoints are hidden. Expose refresh in application.properties:

properties
management.endpoints.web.exposure.include=health,info,refresh

3. Triggering the Reload

When you update your configuration in your config server or configuration source, issue an HTTP POST request to the target microservice:

bash
curl -X POST http://localhost:8081/actuator/refresh

Spring Boot executes the refresh and returns a JSON array listing the exact property keys that changed:

json
[
  "config.client.version",
  "discount.rate"
]

Subsequent calls to DiscountService.calculateDiscount() immediately use the updated rate with zero server downtime.


Using @RefreshScope with @ConfigurationProperties

While @Value works with @RefreshScope, the enterprise best practice is grouping properties using @ConfigurationProperties:

java
package com.example.orderservice.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "app.order")
@RefreshScope
public class OrderProperties {

    private int maxItems = 25;
    private boolean expressShippingEnabled = true;
    private String supportEmail = "support@example.com";

    // Getters and setters
    public int getMaxItems() { return maxItems; }
    public void setMaxItems(int maxItems) { this.maxItems = maxItems; }

    public boolean isExpressShippingEnabled() { return expressShippingEnabled; }
    public void setExpressShippingEnabled(boolean expressShippingEnabled) { this.expressShippingEnabled = expressShippingEnabled; }

    public String getSupportEmail() { return supportEmail; }
    public void setSupportEmail(String supportEmail) { this.supportEmail = supportEmail; }
}

In Spring Boot 3, @ConfigurationProperties beans are automatically refreshed on /actuator/refresh even without an explicit @RefreshScope annotation in many setups, but adding @RefreshScope makes the dynamic proxy behavior explicit and guaranteed.


Thread Safety and Pitfalls to Avoid

1. Never Store Stateful In Flight Data in @RefreshScope Beans

Because the target bean instance is destroyed and recreated during refresh, any internal in memory state (such as an open file handle, active socket connection, or stateful map) will be destroyed. Keep @RefreshScope beans strictly stateless.

2. Database Connection Pools Cannot Simply Be Swapped

Do not put @RefreshScope on DataSource beans unless you configure special handling. Recreating a DataSource closes active database connections while user transactions are in flight, causing catastrophic transaction rollbacks.

3. The Scaling Problem

If you have fifty running instances of order-service, calling POST /actuator/refresh on fifty separate IP addresses manually is impractical. This exact limitation is solved by Spring Cloud Bus, which broadcasts a single refresh event across all instances via a message broker.


Interview Questions & Pitfalls

Q1: How does @RefreshScope refresh a bean without restarting the JVM?

@RefreshScope creates a CGLIB dynamic proxy in front of the real bean. The proxy resolves the actual target instance from a cache. When /actuator/refresh is called, Spring clears that cache. On the next method invocation, the proxy creates a fresh bean instance, injects newly loaded configuration properties from the environment, and caches it.

Q2: What happens if a method is currently executing on a @RefreshScope bean when a refresh occurs?

The thread currently executing continues on the existing bean instance until completion. The cache invalidation affects subsequent calls. Once the in flight method exits, the old instance becomes eligible for garbage collection, and subsequent calls route to the newly instantiated bean.

Q3: Which HTTP method is required to invoke /actuator/refresh?

The /actuator/refresh endpoint requires an HTTP POST request. Sending a GET request will return a 405 Method Not Allowed error.

Q4: What is the limitation of relying solely on /actuator/refresh in a multi instance microservices deployment?

Each /actuator/refresh call only updates the single specific server instance that received the HTTP request. If a microservice is scaled to thirty instances across a cluster, each instance would need an individual HTTP call. To refresh all instances simultaneously, you must use Spring Cloud Bus.

Q5: Can @RefreshScope be used on beans that manage active network connections or thread pools?

It should be avoided. Destroying and recreating beans that manage active sockets, thread pools, or database connection pools can sever in flight network connections and cause unexpected application errors. @RefreshScope is best suited for configuration beans, feature flags, routing parameters, and stateless service components.