Project: Multi-Environment Configuration
Project: Multi-Environment Configuration
Throughout this tutorial you have studied every building block of Spring configuration in isolation: profiles, property sources, @Value, @Conditional, and the Environment abstraction. This final lesson brings all of them together in a realistic Spring Boot 3 project. The goal is a codebase that works identically on a developer laptop, a CI test runner, and a production server — with no manual file edits, no commented-out blocks, and no environment-specific code paths embedded in business logic.
The Target Architecture
The application exposes a REST API backed by a relational database. It uses asynchronous email sending and integrates with an external payment gateway. The configuration strategy must satisfy four requirements:
- Dev — fast startup, H2 in-memory database, email printed to the console, payment gateway stubbed out.
- Test — deterministic, no network calls, H2 or Testcontainers, all external clients replaced with fakes.
- Staging — mirrors production topology but uses non-critical credentials and a sandboxed payment gateway.
- Prod — PostgreSQL, real SMTP, live payment gateway, strict connection limits, no debug logging.
Property File Layout
Spring Boot resolves a well-defined property source hierarchy. For a profile named prod, it loads (in priority order, highest first):
- OS environment variables and JVM system properties (
-Dkey=value) application-prod.properties(or.yml)application.properties(base defaults)
Keep only values that are genuinely environment-agnostic in the base file. Every environment-specific secret or URL belongs in the profile file or — better — in an environment variable on the target machine.
${ENV_VAR} syntax in application-prod.properties tells Spring to resolve the value from the real environment at startup. The file itself can safely be committed to version control because it contains no actual credentials.
Profile-Driven Bean Selection
Beans that require real infrastructure belong behind profiles. Create a dedicated configuration class for each external integration:
Business services depend on AppMailSender — the interface — not the concrete implementation. Spring injects the correct bean automatically based on the active profile. No if/else in business code, no boolean flags checked at runtime.
Binding Configuration Properties to a Typed Class
Scattering @Value injections across many classes makes the configuration surface hard to see and hard to validate. Group related properties into a single @ConfigurationProperties record:
Spring Boot validates the properties at startup. If payment.gateway.base-url is missing in a profile, the application fails immediately with a clear error rather than throwing a NullPointerException deep inside a request handler.
spring-boot-configuration-processor to your build. It generates IDE metadata so that application.properties gets autocompletion and documentation for every @ConfigurationProperties class. It is a compile-only annotation processor — zero runtime cost.
Conditional Infrastructure Beans
Some beans should not even be created if a feature flag is off. Use @ConditionalOnProperty for this:
The matchIfMissing = true on the sandbox bean ensures that if the property is absent (for example in a unit test that does not load any profile-specific file), the safe stub is used — not the live client.
Activating Profiles
There are three standard ways to activate a profile:
- JVM argument (CI / Docker):
-Dspring.profiles.active=prod - Environment variable (12-factor / Kubernetes):
SPRING_PROFILES_ACTIVE=prod - Test annotation:
@ActiveProfiles("test")on a JUnit test class
In a containerised deployment, setting SPRING_PROFILES_ACTIVE as an environment variable in the pod spec or Docker run command is the preferred approach: it keeps the JAR identical across environments and the environment decides its own role.
Test Configuration
Integration tests need their own slice of configuration. Spring Boot provides @SpringBootTest combined with a dedicated application-test.properties:
application-test.properties under src/test/resources, not src/main/resources. Spring Boot will find it automatically when the test profile is active, and it will never be bundled into the production JAR.
Summary: The Rules That Make It Work
A multi-environment Spring Boot configuration that holds up in practice follows a small set of rules:
- One base file for defaults that are truly shared; one profile file per environment for overrides.
- Secrets in environment variables only — never in files that are committed to version control.
- Profile-driven beans instead of runtime
if/elsechecks in business logic. - Typed
@ConfigurationPropertiesfor all custom properties — validated at startup, autocompleted in the IDE. - Test profile in
src/test/resources— fast, deterministic, fully isolated from real infrastructure.
When these rules are followed, you can hand the same JAR to any environment and trust that it will configure itself correctly from the surrounding context — which is exactly what the twelve-factor app methodology demands and what modern deployment pipelines expect.