How to structure Spring Boot configuration across profiles, environment variables, and Spring Cloud Config Server — so your app behaves correctly at every stage without hardcoded values.
Configuration is one of those things that works fine in development and quietly breaks everything in production. Not because Spring Boot’s configuration system is difficult — it’s actually excellent — but because teams don’t think through the layering until something goes wrong at 2am.
This post covers how to structure configuration properly: when to use profiles, when to use environment variables, how @ConfigurationProperties makes the whole thing typesafe, and when a Config Server makes sense.
Spring Boot resolves configuration from multiple sources in a defined priority order. The most important layers, from highest to lowest priority:
--server.port=9090)SERVER_PORT=9090)application-{profile}.yml / application-{profile}.propertiesapplication.yml / application.properties@ConfigurationProperties fieldsThis hierarchy is your friend. It means you can set sensible defaults in application.yml, override per-environment in profile files, and override anything at deploy time with environment variables — without touching code.
The primary use case for profiles is environment-specific infrastructure config: database URLs, message broker endpoints, feature flags that differ between dev and production.
application.yml:
spring:
datasource:
driver-class-name: org.postgresql.Driver
app:
feature-flags:
new-pricing-algorithm: false
application-dev.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp_dev
username: dev
password: dev
app:
feature-flags:
new-pricing-algorithm: true
application-production.yml:
spring:
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
Notice the pattern: application-production.yml doesn’t contain actual credentials. It contains environment variable references. The real values live in your deployment platform (ECS task definition, Kubernetes secret, SSM parameter). The profile file is committed to source control safely.
A common mistake is using profiles as a secrets store — real passwords in application-staging.yml, committed to the repo. The fix is to always use ${ENV_VAR} placeholders in profile files for anything sensitive. The profile file describes the structure; the environment provides the values.
The environment variable naming convention matters here. Spring Boot automatically converts DATABASE_URL to spring.datasource.url via relaxed binding — underscores become dots, uppercase becomes lowercase. For nested properties: APP_FEATURE_FLAGS_NEW_PRICING_ALGORITHM maps to app.feature-flags.new-pricing-algorithm.
Injecting config with @Value("${some.property}") works but doesn’t scale. For any group of related properties, @ConfigurationProperties is strictly better:
@ConfigurationProperties(prefix = "app.trading")
@Validated
public record TradingConfig(
@NotNull String exchangeUrl,
@Positive int maxOpenPositions,
@NotNull Duration heartbeatInterval,
RiskConfig risk
) {
public record RiskConfig(
@DecimalMin("0.0") @DecimalMax("1.0") double maxExposureRatio,
@Positive BigDecimal stopLossThreshold
) {}
}
app:
trading:
exchange-url: ${EXCHANGE_URL}
max-open-positions: 10
heartbeat-interval: PT30S
risk:
max-exposure-ratio: 0.15
stop-loss-threshold: 500.00
Register it:
@SpringBootApplication
@EnableConfigurationProperties(TradingConfig.class)
public class TradingApplication { ... }
The benefits over @Value:
spring-boot-configuration-processor dependencyDuration, DataSize, and other types are parsed automatically from YAML strings like PT30S or 10MBFor tests that need specific config without loading a profile:
@SpringBootTest
@TestPropertySource(properties = {
"app.trading.max-open-positions=5",
"app.trading.risk.max-exposure-ratio=0.10"
})
class TradingServiceIntegrationTest { ... }
Or with a dedicated test config file:
@SpringBootTest
@ActiveProfiles("test")
class TradingServiceIntegrationTest { ... }
With a matching application-test.yml in src/test/resources. Test profiles should not be in src/main/resources — there’s no reason for production code to know about test configuration.
A Config Server makes sense when:
The Config Server is a Spring Boot app that serves config from a Git repository (or Vault, or S3):
# config-server application.yml
spring:
cloud:
config:
server:
git:
uri: https://github.com/your-org/config-repo
search-paths: '{application}'
Clients add the Spring Cloud Config dependency and point to the server:
# client bootstrap.yml
spring:
application:
name: trading-service
config:
import: "configserver:http://config-server:8888"
The server serves trading-service.yml, trading-service-production.yml, and so on from the Git repo. Clients refresh on restart or, if you wire it up, on a /actuator/refresh call.
The tradeoff: Config Server adds a dependency in your startup path. If the server is unavailable, your service won’t start unless you configure a fallback. For most teams, SSM Parameter Store or Secrets Manager is a simpler alternative that AWS manages for you — Spring Cloud AWS has first-class support for both.
Profiles aren’t just for config. They gate beans:
@Service
@Profile("!production")
public class StubPaymentGateway implements PaymentGateway { ... }
@Service
@Profile("production")
public class StripePaymentGateway implements PaymentGateway { ... }
Use this sparingly — swapping infrastructure implementations this way makes it hard to detect config errors that only appear in production. For most cases, prefer a single implementation with behaviour controlled by config values.
Use spring.config.activate.on-profile in multi-document YAML: Rather than multiple files, you can use document separators in a single application.yml:
---
spring:
config:
activate:
on-profile: production
datasource:
url: ${DATABASE_URL}
Fail fast on missing required values: Add @Validated to your @ConfigurationProperties class. A missing required property fails at startup with a clear message rather than an NPE somewhere in business logic.
Log your active profiles at startup: Add logging.level.org.springframework.boot: DEBUG during development — Spring Boot logs which property sources it’s loading and in what order. Invaluable when properties aren’t resolving the way you expect.
If you’re trying to untangle configuration that has grown organically across a multi-environment system, get in touch.