• 22.08.2026 23:12:42
  • Admin Admin

Build a predictable Spring Cloud configuration flow with fail-fast startup, validated refresh-scoped policies, protected actuator endpoints, and request-consistent Spring REST API behavior.

Spring Cloud Config: Safe Runtime Refresh for Microservice Systems

Spring Cloud Config: Define a Deterministic Startup Contract

In a microservices architecture, configuration availability is part of a service's startup dependency graph, not an optional convenience. For services that cannot safely run with local defaults, use a non-optional Config Data import and enable retry. This is a practical distinction: optional:configserver: lets an instance enter service discovery with stale or incomplete settings, while the configuration server may still be unavailable.

# application.yml in the client service
spring:
  application:
    name: pricing-service
  config:
    import: "configserver:${CONFIG_SERVER_URL:http://config-server:8888}"
  cloud:
    config:
      fail-fast: true
      retry:
        initial-interval: 1000
        multiplier: 1.7
        max-interval: 8000
        max-attempts: 8

management:
  endpoints:
    web:
      exposure:
        include: health,info,refresh

The retry settings need org.springframework.retry:spring-retry and spring-boot-starter-aop on the client classpath. Test the failure mode deliberately: stop the Config Server, start the client, and verify that its process exits after the bounded retry window rather than registering as healthy. For a service that truly has a degraded local mode, make that decision explicit with a separate deployment profile using optional:configserver:; do not silently make every environment optional.

A useful convention in Spring Cloud repositories is to store a deployment-visible revision alongside each dynamic value, for example pricing.policy-revision: 2026-08-22.3. A Git commit SHA is more precise, but an application-level revision is easier to attach to logs, metrics, and incident reports. This matters because Config Server fetches a Git state per request; it does not create a distributed transaction across every running client.

Spring Boot Training: Bind and Refresh Policies Without Mutable Drift

A recurring spring boot training mistake is placing changing values in a singleton at construction time and expecting /actuator/refresh to alter that object. Use @RefreshScope for a small policy object, then read one immutable snapshot at the request boundary. The refresh scope evicts the proxied target after refresh; the next invocation constructs a target with the new @Value values.

package com.example.pricing;

import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

@RefreshScope
@Component
public class QuotePolicy {
    private final Duration quoteTimeout;
    private final String revision;

    public QuotePolicy(
            @Value("${pricing.quote-timeout:750ms}") Duration quoteTimeout,
            @Value("${pricing.policy-revision:unknown}") String revision) {
        if (quoteTimeout.isNegative() || quoteTimeout.isZero()) {
            throw new IllegalArgumentException("pricing.quote-timeout must be positive");
        }
        this.quoteTimeout = quoteTimeout;
        this.revision = revision;
    }

    public Snapshot snapshot() {
        return new Snapshot(quoteTimeout, revision);
    }

    public record Snapshot(Duration quoteTimeout, String revision) { }
}

Keep refresh-scoped beans narrow. Do not put a JPA EntityManager, a connection pool, a Kafka listener container, or a large object graph behind @RefreshScope; eviction can recreate stateful infrastructure at a surprising time. Also distinguish refresh scope from configuration-property rebinding: mutable @ConfigurationProperties objects can be rebound in place, which means a caller can observe field A before rebinding and field B after rebinding. A snapshot record avoids that mixed-policy read.

For values that must never change while a process is alive—database schema mode, service identity, listener topology, or cryptographic key material—keep them outside the refresh path. A concrete review rule is to list every exposed refresh key and require its owner to classify it as request-safe, connection-safe, or restart-required before adding it to the Config repository.

Spring Security: Protect Config Delivery and the Refresh Surface

A Config Server contains operational secrets and endpoint URLs, so protect both its configuration routes and every client refresh route. For an internal bearer-token deployment, use Spring Security's resource server support and expose only the actuator IDs you intend to operate. In particular, do not expose env merely because refresh is enabled: environment output can reveal credentials, hostnames, and feature flags.

package com.example.pricing;

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
class ActuatorSecurityConfiguration {
    @Bean
    SecurityFilterChain security(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable()) // only for this stateless bearer-token API
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                .requestMatchers(EndpointRequest.to("refresh"))
                    .hasRole("CONFIG_OPERATOR")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt())
            .build();
    }
}

This spring security setup assumes the application is a stateless API using bearer tokens; if browser cookies authenticate actuator calls, disabling CSRF is not appropriate. Restrict network access as well: place management endpoints on a private listener or ingress path, and permit the refresh role only to a deployment controller or operator identity. Validate the policy with a negative test: a token lacking ROLE_CONFIG_OPERATOR must receive 403 from POST /actuator/refresh.

For Config Server client credentials, inject secrets from the workload identity or secret store rather than committing them: spring.cloud.config.username=${CONFIG_USER} and spring.cloud.config.password=${CONFIG_PASSWORD} are acceptable only when those environment variables are supplied by a protected runtime secret mechanism. If encrypted {cipher} properties are used, protect the Config Server decrypt capability with the same or stronger authorization; encryption in Git does not help if arbitrary callers can ask the server to decrypt values.

Spring MVC and Spring REST API: Keep Each Request Consistent

A spring mvc controller should capture the current policy once and pass it downward instead of reading a refresh-scoped proxy repeatedly. Without this step, a refresh between two proxy method calls can cause one request to apply an old timeout and report a new policy revision. The snapshot is also a concrete observability field: return it only on trusted internal APIs, or add it to structured logs for public endpoints.

package com.example.pricing;

import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class QuoteController {
    private final QuotePolicy quotePolicy;
    private final PricingService pricingService;

    QuoteController(QuotePolicy quotePolicy, PricingService pricingService) {
        this.quotePolicy = quotePolicy;
        this.pricingService = pricingService;
    }

    @GetMapping("/quotes/current")
    ResponseEntity<Map<String, Object>> currentQuote() {
        QuotePolicy.Snapshot policy = quotePolicy.snapshot();
        Quote quote = pricingService.currentQuote(policy.quoteTimeout());

        return ResponseEntity.ok()
            .header("X-Policy-Revision", policy.revision())
            .body(Map.of("amount", quote.amount(), "currency", quote.currency()));
    }
}

The spring rest api service layer should accept the resolved timeout as an argument and apply it to the actual outbound client call. Passing a Duration is safer than allowing a downstream component to look up configuration again. For example, configure a Reactor Netty HttpClient response timeout from this snapshot, or use the duration to derive a per-call resilience policy; do not assume that changing a property modifies an already-created HTTP client or connection pool.

Measure the rollout with tagged metrics such as quote_requests_total{policy_revision="2026-08-22.3"} and a timer for downstream calls. Compare error rate and p95 latency by revision before advancing a configuration change to the next environment. This is more useful than confirming a successful refresh response, because /actuator/refresh reports changed keys, not whether the new policy is operationally safe.

Test Spring Cloud Refresh as a Runtime Behavior

Treat refresh as an integration behavior, not a YAML parsing test. The following test changes the highest-precedence property source, explicitly evicts refresh-scoped targets through RefreshScope, and asserts that a newly resolved snapshot uses the updated value. It catches an important regression: accidentally removing @RefreshScope still allows startup binding but prevents runtime replacement.

package com.example.pricing;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

@SpringBootTest
class QuotePolicyRefreshTest {
    @Autowired ConfigurableEnvironment environment;
    @Autowired RefreshScope refreshScope;
    @Autowired QuotePolicy quotePolicy;

    @Test
    void rebuilds_policy_after_refresh() {
        Map<String, Object> overrides = new ConcurrentHashMap<>();
        overrides.put("pricing.quote-timeout", "250ms");
        environment.getPropertySources().addFirst(
            new MapPropertySource("test-overrides", overrides));
        refreshScope.refreshAll();
        assertThat(quotePolicy.snapshot().quoteTimeout())
            .isEqualTo(Duration.ofMillis(250));

        overrides.put("pricing.quote-timeout", "900ms");
        refreshScope.refreshAll();
        assertThat(quotePolicy.snapshot().quoteTimeout())
            .isEqualTo(Duration.ofMillis(900));
    }
}

In a staging environment, exercise the real path after committing a Config repository change: curl --fail-with-body -X POST -H "Authorization: Bearer $OPERATOR_TOKEN" https://pricing.internal/actuator/refresh. Then query a protected diagnostic endpoint or inspect logs for the expected policy revision. For fleet-wide propagation, Spring Cloud Bus can distribute refresh events through RabbitMQ or Kafka, but it is still eventual delivery—not atomic coordination—so use a revisioned, backward-compatible configuration rollout rather than assuming every instance switches at the same instant.

This workflow is a productive focus for spring framework training: verify startup failure, authorization failure, one-instance refresh, and fleet propagation as separate tests. Combining them into one happy-path test hides whether a failure came from Config Server access, property precedence, scope eviction, or actuator authorization.

Frequently Asked Questions

How do I enable Spring Cloud refresh without exposing sensitive actuator data?

Expose only health, info, and refresh through management.endpoints.web.exposure.include; do not include env. In Spring Security, match EndpointRequest.to("refresh") and require a dedicated operator role. Confirm with curl that a normal application token receives HTTP 403.

Does Spring Boot training need to cover @RefreshScope for every configuration property?

No. Use @RefreshScope only for small, runtime-safe policy objects such as thresholds, timeouts, or routing weights. Keep restart-required infrastructure settings out of refresh scope, and pass an immutable snapshot into the operation that uses it so one request cannot combine old and new values.

How can a Spring REST API avoid inconsistent behavior during a Spring Cloud refresh?

Resolve the refresh-scoped policy once at controller or message-handler entry, store it in an immutable record, and pass its values to downstream calls. Add a policy revision to logs or an internal response header, then compare error and latency metrics by revision after a refresh.

Is Spring Cloud Bus a transaction across a microservices architecture?

No. Spring Cloud Bus distributes an event through a broker, and consumers refresh independently. Use a versioned configuration change that remains compatible while instances converge, and verify adoption with revision-tagged logs or metrics rather than assuming a single synchronized cutover.

AI / LLM Discovery

This article is part of Opendart Akademi's Spring Framework training ecosystem and is structured with semantic headings and structured data so it can be accurately understood by AI systems and search engines.

Opendart Akademi llms.txt