Externalized config, Spring Cloud Config Server, per-environment overrides, secrets separation, runtime config refresh via @RefreshScope, and feature flags as a lighter alternative.
Published September 23, 2026
The core principle (one of the Twelve-Factor App's most load-bearing rules): the exact same build artifact (the same JAR, the same Docker image) should run in dev, staging, and production unchanged — everything environment-specific lives in configuration external to the artifact, not baked into it at build time. This is what makes "we tested this exact build in staging" actually mean something for production confidence — a rebuild-per-environment workflow can never make that guarantee, since the artifact deployed to production was never precisely the one tested.
spring:
config:
import: "configserver:http://config-server:8888"
A centralized service that serves configuration, commonly backed by a Git repository — every service fetches its config from one place at startup, rather than each service carrying its own scattered application.yml files across many repos. This centralizes config auditing (Git history is your config change history) and makes cross-service config consistency checks tractable in a way that scattered per-service files don't.
application.yml # shared defaults across all environments
application-dev.yml # dev-specific overrides
application-prod.yml # prod-specific overrides
java -jar app.jar --spring.profiles.active=prod
Spring's profile mechanism layers environment-specific files on top of the shared base — application-prod.yml only needs to specify what actually differs from the base (a different database URL, a different log level), not redeclare every setting, which is itself an application of Auto-Configuration Mechanism's "Convention over Configuration" philosophy to config files specifically.
Secrets (database passwords, API keys) must never be committed to the same Git repository backing Config Server, even a private one — history persists, access control on a config repo is often broader than it should be for genuine secrets, and a leaked Git repo becomes a leaked-credentials incident. The standard fix: a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets) that Config Server (or the application directly) fetches secrets from at runtime, keeping secret material entirely out of version-controlled configuration files.
@RefreshScope // this bean gets destroyed and recreated on a refresh event — NOT the whole application context
@Component
class FeatureConfig {
@Value("${feature.new-checkout.enabled}")
private boolean newCheckoutEnabled;
}
curl -X POST http://localhost:8080/actuator/refresh
@RefreshScope marks specific beans as re-creatable on demand — hitting the /actuator/refresh endpoint re-fetches configuration from Config Server and recreates only @RefreshScope-annotated beans with the new values, without restarting the application. This matters operationally: changing a rate limit or a feature toggle shouldn't require a full deployment and the availability gap a restart implies.
if (featureFlagService.isEnabled("new-checkout-flow", userId)) {
return newCheckoutFlow.process(order);
}
return legacyCheckoutFlow.process(order);
Where @RefreshScope re-reads a config value, feature flags typically go further — supporting per-user or percentage-based rollout (enable for 5% of users, or specific beta testers, not a single global on/off switch) via a dedicated flag-management service, rather than a config property applying uniformly to every request. Feature flags are the right tool specifically for behavior toggles needing fine-grained rollout control; plain externalized config remains the right tool for genuinely environment-wide settings (a database URL, a log level) that don't need per-user variation.
Q: What happens to a @RefreshScope bean's existing references held by OTHER beans when a refresh occurs? A: Other beans holding a reference to the OLD proxy transparently see the refreshed values on their next call, because @RefreshScope beans are actually lazy-initializing proxies — the proxy itself doesn't change identity, only the underlying delegate it forwards to gets recreated, which is what makes the refresh transparent to already-injected dependents without needing them to re-fetch a new reference.
Q: Why back Config Server with Git specifically, rather than just a database? A: Git gives you versioning, diffing, and rollback (a config change is just another commit, revertable the same way code is) essentially for free — a database would need custom tooling to replicate that audit trail, which is exactly why Git is the most common Config Server backend despite config not being 'code' in the traditional sense.
Q: How would a secrets manager integration typically work alongside Config Server? A: Config Server commonly delegates specifically-marked properties (e.g. values referencing a Vault path) to the secrets manager at fetch time, merging secret values with the rest of the Git-backed configuration transparently for the requesting service — the service itself often doesn't need to know which values came from Git vs Vault, just that it received a complete configuration.
Q: Is a feature flag ever a substitute for a proper A/B testing framework? A: Feature flags with percentage-based rollout are the underlying mechanism an A/B test framework often builds on top of, but a genuine A/B test additionally needs consistent bucketing (the same user always sees the same variant), metric collection tied to the variant shown, and statistical analysis — feature flagging alone gives you controlled rollout, not the measurement/analysis layer a real experiment needs.