Profiles, Properties, and Type-Safe Configuration
Spring Boot configuration is the mechanism that lets one application artifact behave correctly in different places: a developer laptop, a test suite, staging, and production. Profiles choose which environment-specific beans or documents are active. Properties supply values from files, environment variables, command-line arguments, and other sources. Type-safe configuration binds related values into Java objects so the application can validate them once and inject them where needed.
The outcome is not merely avoiding hard-coded strings. In a Spring Boot service, configuration controls ports, database locations, feature flags, cache sizes, client timeouts, credentials, and operational behavior. Used well, it gives each environment a clear contract while keeping application code stable. Used poorly, it produces startup surprises, hidden defaults, and production behavior that differs from what developers thought they tested.
How Boot Builds Configuration
During startup, Spring Boot creates an Environment. The environment is a layered collection of property sources. Typical sources include default properties, packaged application.properties or application.yml, profile-specific files, operating system environment variables, Java system properties, and command-line arguments. When code asks for server.port, Boot resolves the value by checking these sources in precedence order. Higher-priority sources override lower-priority ones.
Boot also supports relaxed binding. The same logical property can often be written as app.mail.host, APP_MAIL_HOST, or app.mail.host in different source formats. This matters because Kubernetes, shell environments, and configuration files all have different naming conventions. Relaxed binding lets the application model stay consistent while deployment tooling uses its native style.
A profile is a named condition. When the dev profile is active, Boot loads profile-specific documents and enables beans guarded by @Profile("dev"). Profiles are best used for structural environment differences, such as using an in-memory mail sender locally and a real provider in staging. They should not become a large matrix of business rules. If behavior needs to change at runtime or per tenant, a profile is usually the wrong tool.
Type-safe configuration uses @ConfigurationProperties to bind a group of properties under a prefix into a Java class or record. This is different from scattering @Value("${...}") across services. A configuration properties class names the whole setting group, declares types, centralizes defaults, and can participate in validation. The result is easier to test and easier to fail fast at startup.
Property and Profile Anatomy
The core syntax is simple. A property key is a dotted name, a value comes from a source, and a consuming component reads the resolved result. YAML adds nesting, while properties files use full keys. Profile-specific files use names such as application-dev.yml, and multi-document YAML can activate one document with spring.config.activate.on-profile.
app:
mail:
host: localhost
port: 2525
from-address: dev@example.test
retry-attempts: 2
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.example.com
port: 587
from-address: noreply@example.com
retry-attempts: 5
With no active profile, the first document supplies local mail settings. With the prod profile active, the second document overrides the matching app.mail keys. Unmentioned keys continue to come from lower-priority sources, so profile documents should be reviewed as overlays rather than complete replacements.
Profiles are activated with spring.profiles.active, an environment variable such as SPRING_PROFILES_ACTIVE=prod, a command-line argument, or test annotations such as @ActiveProfiles. A packaged application should usually not hard-code production activation inside the jar. The environment that runs the jar should decide which profile is active.
Example 1: Plain Property Resolution
The first example uses a single setting. It shows the basic rule: code asks for one logical key, while deployment can supply the value from several places.
@org.springframework.web.bind.annotation.RestController
class GreetingController {
private final String greeting;
GreetingController(@org.springframework.beans.factory.annotation.Value("${app.greeting:Hello}") String greeting) {
this.greeting = greeting;
}
@org.springframework.web.bind.annotation.GetMapping("/greeting")
String greeting() {
return greeting;
}
}
If no value is configured, /greeting returns Hello because the expression includes a default after the colon. If the application starts with --app.greeting=Welcome, command-line arguments have high precedence, and the endpoint returns Welcome. This is convenient for isolated values, but it scales poorly when several related settings must be understood together.
Example 2: Profile-Specific Beans
The second example changes an implementation by profile. Local development can log email messages without contacting an external provider, while production can use a real sender.
interface MailSender {
void send(String to, String subject);
}
@org.springframework.context.annotation.Profile("dev")
@org.springframework.stereotype.Component
class LoggingMailSender implements MailSender {
public void send(String to, String subject) {
System.out.println("DEV mail to " + to + ": " + subject);
}
}
@org.springframework.context.annotation.Profile("prod")
@org.springframework.stereotype.Component
class SmtpMailSender implements MailSender {
public void send(String to, String subject) {
System.out.println("SMTP mail to " + to + ": " + subject);
}
}
With SPRING_PROFILES_ACTIVE=dev, Spring registers LoggingMailSender. A call to send("alex@example.test", "Receipt") prints DEV mail to alex@example.test: Receipt. With prod, Spring registers SmtpMailSender and prints SMTP mail to alex@example.test: Receipt. If neither profile is active, no MailSender bean exists and startup fails when another bean requires it. That failure is useful because it exposes an incomplete environment instead of silently selecting the wrong behavior.
Example 3: Type-Safe Mail Settings
The third example groups mail properties into one validated type. A service can receive a complete MailProperties object instead of several unrelated strings and integers.
@org.springframework.boot.context.properties.ConfigurationProperties(prefix = "app.mail")
@org.springframework.validation.annotation.Validated
public record MailProperties(
@jakarta.validation.constraints.NotBlank String host,
@jakarta.validation.constraints.Min(1) @jakarta.validation.constraints.Max(65535) int port,
@jakarta.validation.constraints.Email String fromAddress,
@jakarta.validation.constraints.Min(0) int retryAttempts) {
}
@org.springframework.boot.SpringBootApplication
@org.springframework.boot.context.properties.EnableConfigurationProperties(MailProperties.class)
public class DemoApplication {
public static void main(String[] args) {
org.springframework.boot.SpringApplication.run(DemoApplication.class, args);
}
}
When app.mail.port=587, binding converts the text value into an int. When app.mail.port=70000, validation fails during startup because the value is outside the allowed port range. The expected behavior is a startup error that points at app.mail.port and the violated maximum. This fail-fast behavior is preferable to discovering the bad value only when the first email is sent.
Design Choices and Trade-Offs
Use @Value for one-off settings close to infrastructure code, especially when a default is harmless and obvious. Use @ConfigurationProperties when settings form a concept: mail, billing, file storage, cache tuning, or a remote API client. Grouped settings give you names, types, validation, metadata generation, and simpler tests.
Use profiles for environment shape, not routine feature management. A profile can select an in-memory adapter, a mock integration, or a production connector. A feature flag should usually be a property because it may need to be changed independently of the entire runtime environment. Avoid profile names that combine too many concerns, such as prod-us-east-blue-experiment-a. Separate deployment location, rollout state, and environment class when possible.
Defaults are a trade-off. They make local development easier, but they can hide missing production configuration. Safe defaults are fine for values such as a local greeting or small cache size. Credentials, external hosts, and destructive behavior should generally have no production default. Let the application fail at startup when required values are absent.
Failure Modes and Troubleshooting
A common symptom is that the application starts with a value different from the one in application.yml. The cause is usually property source precedence. A command-line argument, environment variable, or profile-specific document is overriding the file. Diagnose by checking startup arguments, active profiles, and the environment variable form of the key. If Actuator is available and secured, the env endpoint can show property origins. Correct the issue by removing the higher-priority override or moving the intended value into the source that should own it.
Another symptom is No qualifying bean for an interface that has profile-specific implementations. The cause is that no active profile matches any implementation, or that two active profiles register competing implementations without a primary bean. Diagnose by checking spring.profiles.active and reading the bean annotations. Correct it by activating exactly one intended profile, adding a non-profile fallback, or using explicit conditions for more precise selection.
A third symptom is a binding or validation failure at startup. The cause can be a misspelled prefix, an invalid type such as ten for an integer, or a constraint violation. Diagnose by reading the failure analysis and comparing the property key with the @ConfigurationProperties prefix and field names. Correct the key, value format, or validation rule. Do not suppress validation to make startup pass; fix the configuration contract.
Security, Performance, and Reliability
Configuration often contains sensitive values or values that affect sensitive operations. Keep secrets out of source-controlled application files. Supply them through a secret manager, platform environment, or mounted file designed for that purpose. Also review logs and diagnostic endpoints: property inspection tools are useful, but they must be restricted and sanitized.
Performance settings should be typed and bounded. Connection pool sizes, timeouts, batch sizes, and retry counts can overload dependencies when accidentally set too high. Validation constraints make bad values visible before traffic arrives. Reliability improves when all required settings are checked during startup and when optional settings have documented defaults.
Hands-On Lab
Prerequisites: a small Spring Boot project with web support, configuration properties support, and validation on the classpath. The project should run locally with a standard build command.
- Add the
app.mailYAML shown earlier toapplication.yml, including theprodprofile document. - Create the
MailPropertiesrecord and enable it from the application class. - Create a controller endpoint that returns
host,port, andfromAddressfrom the injectedMailProperties. Do not return secrets. - Run the application with no active profile and request the endpoint. Verify that the response contains
localhost,2525, anddev@example.test. - Run again with
SPRING_PROFILES_ACTIVE=prod. Verify that the response containssmtp.example.com,587, andnoreply@example.com. - Set
APP_MAIL_PORT=70000and start the application. Verify that startup fails before the endpoint is available. - Cleanup by removing the temporary environment variable and deleting the diagnostic endpoint if it was created only for the lab.
The important verification is not just that the app starts. Confirm which profile is active, which value wins when two sources define the same key, and whether invalid configuration prevents startup.
Assessment Exercises
- A service has
application.ymlwithapp.limit=20,application-prod.ymlwithapp.limit=50, and starts with--app.limit=5plus theprodprofile. Which value should code receive, and why? - Refactor three scattered
@Valueinjections for an HTTP client base URL, timeout, and retry count into one configuration properties type. Which validation constraints would you add? - Design profile usage for a payment adapter that must be fake in local development, sandbox in staging, and live in production. What should be profiles, and what should remain ordinary properties?
- An application sometimes connects to the staging database in production. List the diagnostic steps you would take to find the winning value and its source.
- Explain why a missing required external host should usually fail startup instead of falling back to
localhostin production.
Summary
Profiles, properties, and type-safe configuration are Spring Boot’s way of separating stable application code from environment-specific values and implementations. Properties provide values, profiles select conditional configuration, and @ConfigurationProperties turns related settings into validated objects. The practical goal is explicit behavior: the right values win, invalid environments fail early, and each setting group has a clear owner and testable contract.
