Config Client Bootstrap and Property Source Integration
Config Client Bootstrap and Property Source Integration
When the SaaS backend’s order-service starts, it needs database URLs, feature flags, and API keys before it can create a DataSource, configure security, or register with the service registry. These values live on the Config Server. The config client must fetch them during the earliest phase of startup, before the ApplicationContext refreshes and starts creating beans.
The spring.config.import Mechanism
Spring Boot 3 uses ConfigData API for external configuration. The config client hooks into this with a custom ConfigDataLocationResolver and ConfigDataLoader.
In order-service’s application.yml:
spring:
application:
name: order-service
config:
import: "configserver:http://config-server:8888"
During startup, Spring Boot’s ConfigDataEnvironmentPostProcessor processes all spring.config.import entries. When it encounters the configserver: prefix, it delegates to ConfigServerConfigDataLocationResolver.
The resolver creates ConfigServerConfigDataResource objects, one per profile. The corresponding ConfigServerConfigDataLoader makes HTTP requests to the Config Server:
GET http://config-server:8888/order-service/default
GET http://config-server:8888/order-service/prod
The response is a Spring Cloud Environment with an ordered list of PropertySource objects. Each PropertySource maps to a configuration file in the backend repository.
The loader converts these into Spring Boot PropertySource objects and adds them to the Environment. This happens before ApplicationContext.refresh(), before any @Bean methods run, before auto-configuration classes are processed. When @Value("${spring.datasource.url}") is resolved later, the value from Config Server is already in the Environment.
ConfigDataLocationResolver in Detail
ConfigServerConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigServerConfigDataResource>:
public class ConfigServerConfigDataLocationResolver implements
ConfigDataLocationResolver<ConfigServerConfigDataResource> {
@Override
public boolean isResolvable(
ConfigDataLocationResolverContext context,
ConfigDataLocation location) {
return location.hasPrefix("configserver:");
}
@Override
public List<ConfigServerConfigDataResource> resolve(
ConfigDataLocationResolverContext context,
ConfigDataLocation location) {
// Extract Config Server URI from the location
// Create ConfigServerConfigDataResource for each profile
// Return the list of resources to load
}
}
The resolver reads Config Server connection properties from the Environment as it exists at resolution time. This means spring.cloud.config.uri, spring.cloud.config.username, and spring.cloud.config.password must be available before the config import runs. They can come from:
- The same
application.yml(properties beforespring.config.importare available). - Environment variables:
SPRING_CLOUD_CONFIG_URI. - System properties:
-Dspring.cloud.config.uri=....
You cannot set these properties in a file that is itself served by Config Server. That would be circular.
Property Source Precedence
After the config client fetches remote properties, the Environment contains multiple property sources in this order (highest to lowest precedence):
- Command-line arguments
- System properties (
System.getProperties()) - OS environment variables
- Config Server properties (profile-specific)
- Config Server properties (default)
- Local
application-{profile}.yml - Local
application.yml @PropertySourceannotations
Config Server properties sit between OS environment variables and local application properties. This means:
- Config Server values override your local
application.yml. - Environment variables override Config Server values.
- System properties override everything except command-line arguments.
For the SaaS backend, this ordering is intentional. The Config Server is the source of truth for environment-specific configuration. Local application.yml holds defaults. Environment variables handle per-instance overrides (container-specific settings like HOSTNAME or POD_NAME).
# Config Server (order-service/application-prod.yml):
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/orders
hikari:
maximum-pool-size: 20
# Local application.yml (in order-service's jar):
spring:
datasource:
url: jdbc:postgresql://localhost:5432/orders # Overridden by Config Server
hikari:
maximum-pool-size: 10 # Overridden by Config Server
# Environment variable on a specific container:
# SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=30
# Overrides Config Server value for this instance only
Fail-Fast Configuration
By default, if the Config Server is unreachable, the config client logs a warning and continues startup with whatever local properties are available. This is dangerous. The application starts with missing or wrong configuration. It might connect to the wrong database, skip security configuration, or operate with default settings that are unsafe in production.
// BROKEN: No fail-fast. Config Server is down.
// order-service starts with local application.yml values.
// spring.datasource.url points to localhost:5432 instead of prod-db:5432.
// The application connects to a local (possibly empty) database.
// No error. No warning in the UI. Data goes to the wrong database.
Enable fail-fast:
spring:
cloud:
config:
fail-fast: true
With fail-fast: true, if the Config Server is unreachable, the application fails to start with a clear error:
Could not locate PropertySource and the fail fast property is set,
failing: Could not locate PropertySource:
I/O error on GET request for
"http://config-server:8888/order-service/prod":
Connection refused
But fail-fast alone is too aggressive. If the Config Server is restarting (a brief window during deployment), the client should retry before giving up.
Retry Configuration
Add retry support:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
Configure retry parameters:
spring:
cloud:
config:
fail-fast: true
retry:
initial-interval: 2000 # First retry after 2 seconds
max-interval: 10000 # Max delay between retries
max-attempts: 6 # Total attempts (1 initial + 5 retries)
multiplier: 1.5 # Exponential backoff multiplier
The retry sequence with these settings:
- Attempt 1: immediate. Fails.
- Attempt 2: wait 2000ms. Fails.
- Attempt 3: wait 3000ms (2000 * 1.5). Fails.
- Attempt 4: wait 4500ms (3000 * 1.5). Fails.
- Attempt 5: wait 6750ms (4500 * 1.5). Fails.
- Attempt 6: wait 10000ms (capped at max-interval). Fails.
- Application fails to start.
Total wait time before failure: approximately 26 seconds. This covers most Config Server restart scenarios.
// CORRECT: Fail-fast with retry. Application either starts with
// correct configuration or fails clearly.
// application.yml:
// spring:
// cloud:
// config:
// fail-fast: true
// retry:
// initial-interval: 2000
// max-attempts: 6
// max-interval: 10000
// multiplier: 1.5
Profile-Specific Property Loading
The config client sends active profiles to the Config Server. The server returns profile-specific property sources in precedence order:
GET /order-service/prod,us-east
Returns:
order-service-us-east.yml(highest precedence)order-service-prod.ymlorder-service.ymlapplication-us-east.ymlapplication-prod.ymlapplication.yml(lowest precedence)
Profile-specific files override general files. Application-specific files override shared application.* files. The ordering mirrors local Spring Boot profile loading, extended to the remote property sources.
For the SaaS backend, this enables environment-and-region-specific configuration:
# application.yml (shared across all services, all environments)
app:
audit:
enabled: true
# application-prod.yml (all services in production)
app:
audit:
detailed: true
# order-service-prod.yml (order-service in production)
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/orders
# order-service-prod,us-east.yml (order-service in prod, US East)
spring:
datasource:
url: jdbc:postgresql://prod-db-us-east:5432/orders
Label Support
The label parameter (default: main) maps to a Git branch, tag, or commit hash. This enables configuration versioning:
spring:
cloud:
config:
label: release-2.3.1
The Config Server checks out the specified label from the Git repository and serves configuration from that point in history. This lets you pin configuration to a specific release, roll back configuration changes by switching labels, and test configuration changes on a branch before merging to main.
For the SaaS backend’s canary deployments, the canary instances use label: canary while the stable instances use label: main. Configuration changes are tested on the canary branch first, then merged to main for the full fleet.
The Bootstrap Timing Guarantee
The critical guarantee: config client properties are available before @Bean methods run. This means:
@Value("${spring.datasource.url}")in a@Configurationclass resolves to the Config Server value.@ConfigurationPropertiesbindings include Config Server properties.- Conditional annotations like
@ConditionalOnPropertyevaluate against Config Server values. - Auto-configuration classes see the full, merged
Environment.
If you need to validate that Config Server properties are present, use @ConfigurationProperties with @Validated:
@ConfigurationProperties(prefix = "app.billing")
@Validated
public record BillingProperties(
@NotBlank String apiKey,
@NotNull URI endpoint,
@Min(1) int maxRetries
) {}
If app.billing.api-key is missing from both Config Server and local properties, the application fails to start with a validation error. This is the defense against silent misconfiguration.