Appearance
Centralized Configuration with Spring Cloud Config
Every service so far carries its own application.properties inside its JAR. With one service that is fine. With many, it causes four problems.
Four problems with local configuration
1. Rebuild and redeploy for every change. order-service reads a message from its properties:
properties
server.port=8081
spring.application.name=order-service
custom.message=hello worldjava
@Service
public class OrderService {
@Value("${custom.message}")
private String message;
public String getOrder() {
return "fetch the Order details with message: " + message;
}
}To change custom.message: edit the file, rebuild the JAR, redeploy the service. One line, a full release.
2. Inconsistent configuration across services. A large order-service is split into pre-order-service and post-order-service. Both use the same database:
properties
db.url=jdbc:mysql://localhost:3306/order-dbThe value now lives in two files. Change it in one and forget the other, and the services disagree.
3. No runtime updates. application.properties is read once, at startup. A running service never sees a changed value; it must be restarted.
4. Slow rollback. Someone changes db.url to a misconfigured production database, and the services using it crash. To fix it you edit each affected service's file, rebuild and redeploy each one, under pressure.
The solution: three layers
Spring Cloud Config moves configuration out of the services:
- A Git repository holds the property files of all services. Plain files, no code.
- A config server, itself a small Spring Boot application, reads that repository and serves the properties over HTTP.
- Each microservice is a config client: at startup it asks the config server for its properties.
A change becomes a commit to one repository. It is versioned, reviewed, and rolled back with git revert, and no service is rebuilt.
Step 1: the configuration repository
Create a Git repository containing only property files. Folders are allowed; here one holds the global files and one holds order-service's files:
text
central-config/
├── global/
│ ├── application.properties
│ ├── application-dev.properties
│ ├── application-qa.properties
│ └── application-prod.properties
└── orderservice/
├── order-service.properties
├── order-service-dev.properties
├── order-service-qa.properties
└── order-service-prod.propertiesThe file names follow four patterns (profiles such as dev, qa and prod are covered in the Spring Boot book):
| File | Applies to |
|---|---|
application.properties | all services, all profiles |
application-{profile}.properties | all services, one profile |
{application}.properties | one service, all profiles |
{application}-{profile}.properties | one service, one profile |
{application} is the service's spring.application.name, exactly: order-service in the service means order-service*.properties in the repository.
Precedence
When a property is defined in several places, which wins? For order-service running with profile dev, from highest to lowest:
order-service-dev.properties: this service, this profileapplication-dev.properties: all services, this profileorder-service.properties: this service, all profilesapplication.properties: all services, all profiles- the service's own
application-dev.properties(local, inside its JAR) - the service's own
application.properties(local)
The profile-specific files come first, even the global one: application-dev beats order-service.properties. Within the same level, the service-specific file beats the global one. The service's local files come last: they are only a fallback.
Put one property, custom.message, in every file with a different value ("Hello from order dev", "Hello from global dev", "Hello from order default", "Hello from global default") and ask the config server for order-service with profile dev. It returns the sources in exactly that order:
text
orderservice/order-service-dev.properties custom.message = Hello from order dev
global/application-dev.properties custom.message = Hello from global dev
orderservice/order-service.properties custom.message = Hello from order default
global/application.properties custom.message = Hello from global defaultThe first source wins for each property. A property found only in a lower source still applies: a key defined only in global/application.properties reaches order-service too.
Step 2: the config server
Create a project on start.spring.io with the Config Server dependency:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>Enable it on the main class:
java
@SpringBootApplication
@EnableConfigServer // load the repository and expose the config endpoints
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}Point it at the repository:
properties
server.port=8888
# the configuration repository
spring.cloud.config.server.git.uri=https://github.com/your-org/central-config
# folders to look in (only needed when the files are in folders)
spring.cloud.config.server.git.search-paths=global,orderservice
# clone the repository at startup instead of on the first request
spring.cloud.config.server.git.clone-on-start=true
# branch to read (main is the default)
spring.cloud.config.server.git.default-label=mainsearch-paths: without folders, leave it out. With folders, list every folder to scan.clone-on-start=true: the repository is downloaded when the server starts. Withfalse, it is downloaded on the first request instead. Either way it is cached. Cloning at startup also reveals a wrong URL or bad credentials immediately.default-label: the Git branch (or tag) to read. A request can name a different one.
A private repository
Most configuration repositories are private. Create an access token and give the server a username and the token:
- GitLab: project → Settings → Access Tokens → add new token.
- GitHub: profile → Settings → Developer settings → Personal access tokens → Fine-grained tokens.
properties
spring.cloud.config.server.git.uri=https://github.com/your-org/central-config
spring.cloud.config.server.git.username=${GIT_USERNAME}
spring.cloud.config.server.git.password=${GIT_ACCESS_TOKEN}Never write the token into the file. Read it from environment variables (in IntelliJ: Run → Edit Configurations → Environment variables) or from a secrets manager.
Try the server
@EnableConfigServer exposes endpoints such as /{application}/{profile}/{label}:
text
GET localhost:8888/order-service/dev/mainreturns, as JSON, the property sources in precedence order shown above. GET localhost:8888/order-service-dev.properties returns the merged result as a plain properties file. The label is optional; without it, the server uses default-label.
Step 3: a service as config client
In order-service, add the Config Client dependency:
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>and tell it where the server is:
properties
spring.application.name=order-service
spring.config.import=optional:configserver:http://localhost:8888
spring.profiles.active=dev
# local fallback values
custom.message=Hello from local default!spring.application.namemust match the file names in the repository.spring.config.import=configserver:<url>: at startup, fetch properties from the config server and add them to the environment as if they were local. In production, look the server up through Eureka instead of hardcoding its address.optional:makes the config server non-fatal: if it is down or unreachable, the service starts with its local properties.spring.profiles.active=dev: which profile's files to request. It can also be set at launch (--spring.profiles.active=prod).
A local application-dev.properties (custom.message=Hello from local dev!) completes the setup. The controller:
java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Value("${custom.message}")
private String message;
@GetMapping("/fetch")
public String getOrders() {
return "fetched orders and message: " + message;
}
}At startup the log shows the fetch:
text
Fetching config from server at : http://localhost:8888
Located environment: name=order-service, profiles=[dev], label=null, version=ff40240f...The version is the Git commit the values came from. Then:
| Situation | GET /orders/fetch |
|---|---|
profile dev | Hello from order dev |
profile qa | Hello from order qa |
config server down, optional: | Hello from local default! (local fallback) |
config server down, no optional: | the service does not start: ConfigClientFailFastException: Could not locate PropertySource and the resource is not optional, failing |
The local values appear only when nothing from the server overrides them, confirming that local files have the lowest precedence.
Choose deliberately: optional: keeps services starting during a config-server outage, but they then run on local values, which may be outdated. Without it, a missing config server stops deployments rather than letting a service start with the wrong settings.
Still missing: runtime updates
Problems 1, 2 and 4 are solved: values live in one repository, shared values are written once, and a rollback is a Git revert. Problem 3 remains. order-service read its properties at startup; after a commit, it still shows the old value until it restarts.
Spring Cloud can reload values without a restart, using @RefreshScope, which is triggered through a Spring Boot Actuator endpoint.
Next: Chapter 13 covers @RefreshScope: how a running service picks up a changed value, and why only some beans see it.
Check yourself
- Name the four problems with keeping configuration inside each service.
- What are the three layers of Spring Cloud Config?
- Which services and profiles does each of these apply to:
application.properties,application-dev.properties,order-service.properties,order-service-dev.properties? - For order-service with profile dev, list the sources from highest to lowest precedence.
- A property is defined only in
global/application.properties. Does order-service see it? - What do
search-paths,clone-on-startanddefault-labeldo? - How should credentials for a private repository be supplied?
- What does
GET /order-service/dev/mainon the config server return? - What must
spring.application.namematch? - What does
optional:inspring.config.importchange, and what happens without it when the server is down? - Which of the four problems does this chapter not solve yet?
Answers
- Every change needs a rebuild and redeploy; shared values are duplicated and drift apart; values are read only at startup; rollback means editing, rebuilding and redeploying each service.
- A Git repository of property files; a config server that reads it and serves the properties; services as config clients that fetch them at startup.
- All services, all profiles; all services, dev only; order-service, all profiles; order-service, dev only.
order-service-dev,application-dev,order-service,application(all from the repository), then the service's localapplication-dev, then its localapplication.properties.- Yes; lower sources still supply properties that higher ones do not define.
- The folders to scan in the repository; whether to clone at startup (rather than on the first request); the Git branch (label) to read by default.
- As
git.usernameandgit.password(an access token), read from environment variables or a secrets manager, never written in the file. - The property sources for that application, profile and branch, in precedence order.
- The
{application}part of the file names in the repository. - It makes the config server non-fatal: if it is unreachable, the service starts with local properties. Without it, startup fails with
ConfigClientFailFastException. - Runtime updates: a running service still needs a restart to see changed values.