Spring Cloud Config Server on Spring Boot 3.x: Git Mode, Native Mode, and Runtime Refresh

Spring Cloud Config is, alongside Eureka, one of the two survivors of the original Spring Cloud stack — still maintained, still the standard answer for centralized configuration outside Kubernetes. This post is a complete rewrite of my 2020 Config Server tutorials for Spring Boot 3.x: a Config Server backed by Git (with native/filesystem mode for local development), clients using the modern spring.config.import mechanism instead of the long-gone bootstrap.properties, and runtime refresh that actually works.

Tested with: Spring Boot 3.4, Spring Cloud 2024.0, Java 21. Part of the modernized series anchored by the Spring Cloud Netflix migration guide.

What Changed Since the Spring Boot 2.x Tutorials

2020-era setupSpring Boot 3.x setup
bootstrap.properties / spring-cloud-starter-bootstrapspring.config.import: configserver: in plain application.yml
Client fails silently if server is downFail-fast by default with config.import (startup error)
spring.cloud.config.uri in bootstrap phaseSame property, normal configuration phase
/actuator/refresh per instance via curl scriptsSame endpoint; Spring Cloud Bus for fleet-wide refresh

The bootstrap-context removal is the change that breaks copy-paste from old tutorials (including my own): a bootstrap.properties file is simply ignored on Boot 3.x unless you add a legacy compatibility starter — don’t; the new mechanism is better.

Step 1: The Config Server

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

Git Backend (Production)

server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/your-org/config-repo
          default-label: main          # old tutorials assume 'master' — update this
          clone-on-start: true         # fail at startup, not first request
          timeout: 10
          # For private repos:
          username: ${GIT_USER}
          password: ${GIT_TOKEN}       # a PAT, injected via environment — never committed

Native Backend (Local Development)

spring:
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          search-locations: file:///home/ankur/config-repo

Native mode serves config straight from a directory — perfect for local work, wrong for production (no history, no audit, no rollback). My rule: native on laptops, Git everywhere else.

In the config repo, files follow the {application}-{profile}.yml convention:

config-repo/
├── application.yml              # shared by ALL services
├── student-service.yml          # service-specific defaults
├── student-service-prod.yml     # service + profile
└── student-service-dev.yml

Step 2: The Client — No bootstrap.properties

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
# application.yml — the ONLY config file; bootstrap.properties is dead
spring:
  application:
    name: student-service              # selects student-service*.yml in the repo
  config:
    import: "configserver:http://localhost:8888"
  cloud:
    config:
      fail-fast: true                  # refuse to start without config
      retry:
        max-attempts: 6                # needs spring-retry + AOP on classpath

To make the server optional in local dev, use optional:configserver:http://localhost:8888 — but keep it non-optional in production; starting a payment service with default config because the config server was briefly down is not a failure mode you want available.

Step 3: Verify and Consume

The server exposes every combination over HTTP — the fastest debugging tool you have:

curl http://localhost:8888/student-service/dev
curl http://localhost:8888/student-service/prod/main   # app/profile/label
{
  "name": "student-service",
  "profiles": ["dev"],
  "label": null,
  "version": "f6c3a2e9b41d...",
  "propertySources": [
    { "name": "https://github.com/your-org/config-repo/student-service-dev.yml",
      "source": { "app.banner": "Student Service (DEV)", "app.page-size": 25 } },
    { "name": "https://github.com/your-org/config-repo/application.yml",
      "source": { "management.endpoints.web.exposure.include": "health,info,refresh" } }
  ]
}

Note the precedence in that output: more specific sources are listed first and win — service-profile beats service beats shared application.yml.

Step 4: Runtime Refresh

@RestController
@RefreshScope   // bean is rebuilt when /actuator/refresh fires
public class BannerController {

    @Value("${app.banner}")
    private String banner;

    @GetMapping("/banner")
    public String banner() {
        return banner;
    }
}
# after pushing a change to the config repo:
curl -X POST http://localhost:8080/actuator/refresh
# ["app.banner"]   <- the keys that changed

Two things the old tutorials never warned about. First, @RefreshScope rebuilds the bean lazily on next access — stateful beans lose state, so keep refreshable beans thin. Second, POSTing to every instance does not scale; past a handful of services, add Spring Cloud Bus (RabbitMQ/Kafka) so one /actuator/busrefresh fans out to the fleet. @ConfigurationProperties beans, by contrast, are rebound automatically on refresh without @RefreshScope — prefer them for grouped settings.

Pitfalls

Secrets in the config repo. A Git-backed config server makes it temptingly easy to commit passwords. Don’t: use the server’s encryption support ({cipher} values with a keystore) or better, reference a secrets manager (Vault backend) and keep only non-secret config in Git.

default-label mismatch. Older servers default to master; new repos use main. The symptom is an empty property source and a confusing No such label error — set default-label explicitly.

Config server as a single point of failure. With fail-fast: true, a down config server blocks restarts of everything. Run at least two instances behind a load balancer (or register it in Eureka with spring.cloud.config.discovery.enabled=true) — and enable client retry as shown above.

On Kubernetes? ConfigMaps + Secrets cover most of this natively. Config Server still earns its place for VM fleets, hybrid estates, or when you want Git history as your audit log for configuration.

AI Prompts for Config Server Work

Migrate Off bootstrap.properties

Convert this Spring Boot 2.x config-client setup (bootstrap.properties + application.properties) to Spring Boot 3.x spring.config.import style: [paste both files here]. Preserve fail-fast and retry behaviour and flag any property that no longer exists.

What it does: Handles the one mandatory breaking change for every config client upgrading past Boot 2.4.

When to use it: Per service during a Boot 2 → 3 upgrade.

Design the Repo Layout

I have [N] services across [list environments]. Design a config-repo layout using application/profile file conventions: what goes in shared application.yml vs per-service files, how to avoid duplication, and which properties should never be in Git.

What it does: Produces a precedence-aware repo structure before organic growth produces an unmaintainable one.

When to use it: When standing up the config repo, or when cleaning up one that grew wild.

Plan Secret Handling

My config repo currently contains these kinds of values: [describe here]. Classify them into plain config, {cipher}-encrypted values, and secrets-manager material, and produce the Config Server encryption setup for the middle category.

What it does: Separates configuration from secrets with concrete setup steps instead of a vague “use Vault”.

When to use it: Before the security review finds the passwords for you.

Add Fleet-Wide Refresh

I have [N] instances across [M] services using @RefreshScope with manual /actuator/refresh calls. Design the Spring Cloud Bus setup (broker choice, dependencies, busrefresh flow, webhook from the Git host) to replace per-instance refresh.

What it does: Upgrades refresh from curl scripts to an event-driven pipeline triggered by Git pushes.

When to use it: When per-instance refresh stops scaling — in practice, beyond 3–4 services.

Diagnose Missing Properties

My client starts but properties from the config server are missing. Here are the client application.yml, the server config, and the output of curl http://config-server:8888/{app}/{profile}: [paste all three]. Diagnose where the chain breaks — naming, label, profile, import order, or precedence.

What it does: Walks the resolution chain in the order failures actually occur.

When to use it: The standard debugging prompt for “my property is not arriving”.

Conclusion

Spring Cloud Config on Boot 3.x is leaner than the version you learned: one client file, explicit spring.config.import, fail-fast by default. Use Git mode with an explicit default-label and encrypted or externalized secrets, keep native mode for laptops, prefer @ConfigurationProperties over scattered @Value fields, and add Spring Cloud Bus before refresh-by-curl becomes your operational bottleneck.

See Also

Further Reading

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.