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 setup | Spring Boot 3.x setup |
|---|---|
| bootstrap.properties / spring-cloud-starter-bootstrap | spring.config.import: configserver: in plain application.yml |
| Client fails silently if server is down | Fail-fast by default with config.import (startup error) |
| spring.cloud.config.uri in bootstrap phase | Same property, normal configuration phase |
| /actuator/refresh per instance via curl scripts | Same 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
- Spring Cloud Netflix to Modern Alternatives: Complete Migration Guide
- Service Discovery with Eureka in Spring Boot 3.x
- Config Server in Native Mode (Legacy)
- Spring Boot 3 to 4 Migration Guide
- Resilience4j Circuit Breaker in Spring Boot