Spring Configuration & Profiles

The Environment Abstraction

18 min Lesson 7 of 13

The Environment Abstraction

Every non-trivial Spring application needs to read values from the outside world: database URLs, feature flags, API keys, timeouts. Spring bundles all of that behind a single, unified interface called Environment. Rather than scattering calls to System.getenv(), System.getProperty(), and Properties.load() across your codebase, you interact with one object that knows how to search multiple sources in a defined order of precedence.

This lesson focuses on what the Environment is, where it gets its data from, and — crucially — which source wins when the same key appears in more than one place.

The Environment Interface

org.springframework.core.env.Environment extends two sub-interfaces:

  • PropertyResolver — look up property values by key, resolve ${...} placeholders, perform type conversion.
  • EnvironmentCapable — exposes the active and default profile names.

In most code you use the richer ConfigurableEnvironment sub-interface, which additionally exposes the ordered list of property sources so you can add, remove, or reorder them programmatically.

You rarely need to inject Environment directly. @Value, @ConfigurationProperties, and application.properties are all backed by the same Environment underneath. Injecting it directly is most useful in infrastructure code, condition classes, or tests.

PropertySources — Where Values Come From

An Environment holds an ordered MutablePropertySources list. Each entry is a PropertySource<?> — a named bucket that knows how to answer "do you have key X, and if so what is its value?"

Spring Boot 3 populates the following sources automatically, listed from highest to lowest precedence:

  1. Command-line arguments (--server.port=9090)
  2. SPRING_APPLICATION_JSON environment variable or system property (inline JSON)
  3. OS environment variables
  4. JVM system properties (-Dspring.profiles.active=prod)
  5. Profile-specific application files (application-{profile}.properties / .yml)
  6. Application property files (application.properties / application.yml)
  7. Default properties set via SpringApplication.setDefaultProperties()
The rule of thumb: the closer a value is to the running process (command line, OS env var), the higher its precedence. Values baked into files packaged inside the JAR sit near the bottom — they are defaults, not overrides.

Resolving Properties Programmatically

Inject Environment like any other bean:

import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @Component public class DataSourceInfo { private final Environment env; public DataSourceInfo(Environment env) { this.env = env; } public void printConfig() { // getProperty returns null if the key is absent String url = env.getProperty("spring.datasource.url"); // getProperty with a default value int poolSize = env.getProperty("app.pool.size", Integer.class, 10); // getRequiredProperty throws MissingRequiredPropertiesException if absent String apiKey = env.getRequiredProperty("app.payment.api-key"); System.out.println("url=" + url + ", pool=" + poolSize); } }

The getProperty(String, Class<T>) overload performs automatic type conversion via Spring's ConversionService, so you get an Integer back from a string value without any manual parsing.

Inspecting the PropertySources List

Cast Environment to ConfigurableEnvironment to see (or modify) the list of sources at runtime:

import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; // inside a @Bean or ApplicationRunner ConfigurableEnvironment configEnv = (ConfigurableEnvironment) environment; MutablePropertySources sources = configEnv.getPropertySources(); for (PropertySource<?> source : sources) { System.out.printf("%-45s (%s)%n", source.getName(), source.getClass().getSimpleName()); }

Running this on a typical Spring Boot application prints something like:

commandLineArgs (SimpleCommandLinePropertySource) systemEnvironment (SystemEnvironmentPropertySource) systemProperties (PropertiesPropertySource) Config resource 'application-prod.properties' (OriginTrackedMapPropertySource) Config resource 'application.properties' (OriginTrackedMapPropertySource)

The list is ordered — the first source that contains a key wins. This is the exact mechanism behind precedence.

Adding a Custom PropertySource

You sometimes need to inject values from a non-standard location: a database, a vault, a remote config server. The right hook is an ApplicationContextInitializer or an EnvironmentPostProcessor (Spring Boot), both of which run before any beans are created.

import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.boot.SpringApplication; import java.util.Map; // Register in META-INF/spring.factories or META-INF/spring/ // org.springframework.boot.env.EnvironmentPostProcessor=com.example.VaultPostProcessor public class VaultPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication application) { // Simulate fetching secrets from a vault Map<String, Object> secrets = Map.of( "app.payment.api-key", fetchFromVault("payment-key") ); // addFirst = highest precedence; addLast = lowest env.getPropertySources().addFirst( new MapPropertySource("vault", secrets) ); } private String fetchFromVault(String path) { // real implementation calls your secrets backend return "sk-live-secret"; } }
addFirst vs addLast matters. If you call addLast, your custom source sits below systemEnvironment — an OS environment variable with the same key will silently override it. Use addFirst only for sources that should take highest authority (e.g. a central secrets manager). Use addLast for fallback defaults.

Placeholder Resolution and Type Conversion

The Environment can resolve nested ${...} placeholders in any string you hand it:

// application.properties base.url=https://api.example.com orders.url=${base.url}/orders notifications.url=${base.url}/notifications

When you call env.getProperty("orders.url") Spring recursively expands ${base.url} before returning the result. This composable placeholder mechanism is what makes shared base properties useful across profiles.

Querying Active Profiles

Environment is also the authoritative source for the active profile list — the same object you interact with for properties also controls profile-gated beans:

String[] active = env.getActiveProfiles(); // e.g. ["prod", "eu-west"] String[] defaults = env.getDefaultProfiles(); // ["default"] unless overridden boolean isProd = env.acceptsProfiles( org.springframework.core.env.Profiles.of("prod") );

The Profiles.of() factory (Spring 5.1+) accepts profile expressions including negation (!dev) and conjunction (prod & eu-west), giving you fine-grained conditional logic in infrastructure code without reaching for @Conditional.

Common Pitfalls

  • Reading properties too early. Accessing env.getProperty() inside a @PostConstruct works fine, but doing so in a static initializer or constructor of an ApplicationContextInitializer may run before all sources are loaded.
  • Relaxed binding vs direct lookup. Spring Boot's relaxed binding (e.g. APP_DB_URLapp.db.url) applies when you use @ConfigurationProperties. A raw env.getProperty("APP_DB_URL") call uses exact-match only — it will not find the dot-notation key.
  • YAML anchors. YAML &anchor / *alias syntax is resolved by the YAML parser before Spring sees the values — they are not the same as Spring ${placeholder} references.

Summary

The Environment abstraction is Spring's unified view of all configuration sources. It holds an ordered MutablePropertySources list; the first source that supplies a key wins. Spring Boot pre-populates that list with command-line arguments, OS environment variables, JVM system properties, and property files in a well-defined precedence order. You can inject Environment directly to resolve properties with type conversion, inspect or modify the sources list by casting to ConfigurableEnvironment, and add custom sources (e.g. from a vault) via an EnvironmentPostProcessor. The same object also exposes the active profiles, making it the single source of truth for all runtime configuration decisions.