When should you use an EnvironmentPostProcessor versus the ConfigData API or @ConfigurationProperties, and how do multiple EPPs order against Boot's own ConfigDataEnvironmentPostProcessor?
answer
- @ConfigurationProperties = consume; ConfigData = new location; EPP = imperative mutate
- ConfigDataLocationResolver + ConfigDataLoader for spring.config.import
- ConfigDataEnvironmentPostProcessor order = HIGHEST_PRECEDENCE + 10
- lower order value = earlier; late EPP sees application.properties
- make EPP idempotent, stable source names
basics
~20 sUse an EPP for broad, imperative shaping of the whole Environment early (compute/decrypt/inject property sources). Use the ConfigData API to add a new config location type (like a custom spring.config.import). Use @ConfigurationProperties only to read config into beans. Order EPPs with Ordered/@Order; Boot's ConfigDataEnvironmentPostProcessor runs at very high precedence.
solid answer
~40 sThree different tools. @ConfigurationProperties/@Value just *consume* properties into beans — no mechanism to add sources. The ConfigData API (ConfigDataLocationResolver + ConfigDataLoader, Boot 2.4+) is the modern way to teach Boot a new config *location* so users can write spring.config.import=myscheme:...; it's declarative, profile- and import-aware, and cache-friendly. An EnvironmentPostProcessor is the imperative escape hatch: run arbitrary code over the whole ConfigurableEnvironment to compute defaults, decrypt secrets, or mutate/reorder any source — useful when what you need isn't tied to a single location or predates ConfigData. Ordering: Boot loads config files via its own ConfigDataEnvironmentPostProcessor at order HIGHEST_PRECEDENCE + 10, so implement Ordered to run after it (higher order value) if you must see application.properties/yaml, or before it to influence which files/profiles it loads.
code
java · 31 linespackage com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
// Runs AFTER Boot's ConfigDataEnvironmentPostProcessor (order HIGHEST_PRECEDENCE + 10)
// because LOWEST_PRECEDENCE is a larger value => later. So application.properties
// is already loaded and we can override it.
public class LateOverrideEnvironmentPostProcessor
implements EnvironmentPostProcessor, Ordered {
@Override
public void postProcessEnvironment(ConfigurableEnvironment env,
SpringApplication application) {
// e.g. decrypt or override values now that file config is present
env.getPropertySources().addFirst(
new MapPropertySource("decryptedSecrets",
Map.of("db.password", decrypt(env.getProperty("db.password")))));
}
private String decrypt(String cipher) { /* ... */ return cipher; }
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; // run late
}
}go deeper
Just knows @ConfigurationProperties reads config; EPP is for changing it early.
Can distinguish consuming vs producing config and knows EPPs mutate sources.
Chooses ConfigData API vs EPP correctly and handles ordering relative to file loading.
Designs library config integration, reasons about ConfigDataEnvironmentPostProcessor ordering, profile timing, idempotency, and composability trade-offs.
## Three tools, three jobs ### 1. @ConfigurationProperties / @Value — *consume* config These bind already-resolved properties into beans. They add nothing to the Environment and run at **bean time** (context refresh). Reach for them 95% of the time — when you just want to read config. They are irrelevant if your goal is to *produce* or *transform* config. ### 2. ConfigData API — add a new config *location type* (Boot 2.4+) The modern config system is built on two SPIs: - **`ConfigDataLocationResolver`** — recognizes a location string (e.g. `spring.config.import=vault://secret/app`) and resolves it to `ConfigDataResource`s. - **`ConfigDataLoader`** — loads a resolved resource into `ConfigData` (property sources). Register them in `spring.factories`. This is **declarative and location-oriented**: users opt in with `spring.config.import`, it's fully **profile-aware** (imported docs can have profile-specific sections), participates in Boot's ordering and caching, and composes cleanly. Choose this when your config has a natural **location/URI** and you want first-class integration (the way `configtree:`, `vault:`, `consul:` work). ### 3. EnvironmentPostProcessor — imperative, whole-Environment shaping The **escape hatch**: arbitrary code with the entire `ConfigurableEnvironment` in hand. Best when: - The transformation isn't tied to one location — e.g. **decrypt any `{cipher}` value across all sources**, derive properties from other properties, or **reorder/remove** sources. - You need to run **very early**, even to influence which config files load. - You're targeting older Boot or a case ConfigData doesn't model. Downside: imperative, less composable, no built-in profile/import semantics — you own correctness. ## Decision guide - Just reading config → `@ConfigurationProperties`. - Adding a resolvable *source/location* users import → **ConfigData API**. - Broad imperative mutation / decryption / computed defaults / reordering → **EnvironmentPostProcessor**. ## Ordering with Boot's own EPP Boot loads `application.properties`/`application.yml` (and processes `spring.config.import`) via **`ConfigDataEnvironmentPostProcessor`**, itself an `EnvironmentPostProcessor` registered at order **`Ordered.HIGHEST_PRECEDENCE + 10`** — i.e. it runs **very early**. Since lower order value = earlier: - Give your EPP a **higher order value** (e.g. `LOWEST_PRECEDENCE`) to run **after** config files are loaded when you need to *read or override* application.properties/yaml. - Give it a value **lower** than `HIGHEST_PRECEDENCE + 10` to run **before** config-data loading — e.g. to set a default profile or inject a source that the file-loading logic should see. Control this by implementing `org.springframework.core.Ordered` (or `@Order`). If you `addBefore("applicationConfig: ...", ...)` you must run *after* the source exists, or the named source won't be present and the call throws. ## Gotchas at this level - **Profiles**: activating profiles from an EPP is fragile if it runs before/after config-data processing; prefer setting `spring.profiles.active` as a property early, and understand that ConfigData resolves profile-specific documents itself. - **Idempotency & duplicates**: EPPs may run more than once across event types in some flows; make mutation idempotent and use stable source names. - **Testability**: EPPs are plain objects — unit-test by constructing one and passing a `StandardEnvironment`; no Spring context needed. - **Library authors**: prefer ConfigData for anything users will `import`; reserve EPPs for cross-cutting transforms so you don't fight Boot's ordering.
- You want users to write spring.config.import=acme:core to pull config from your system. EPP or ConfigData API?ConfigData API — implement a ConfigDataLocationResolver to recognize the acme: prefix and a ConfigDataLoader to load it. That's the location-oriented, profile- and import-aware mechanism; an EPP would be a clumsier imperative substitute.
- Your EPP needs to read application.yml values but they're null. Why, and how do you fix it?Your EPP runs before ConfigDataEnvironmentPostProcessor (order HIGHEST_PRECEDENCE + 10) has loaded the config files. Implement Ordered with a higher order value (e.g. LOWEST_PRECEDENCE) so it runs after config-data loading.
saying these in an interview costs you the question
- Recommending an EnvironmentPostProcessor to add a custom spring.config.import location instead of the ConfigData API.
- Assuming an EPP always sees application.properties regardless of ordering.
- Thinking @ConfigurationProperties can add property sources to the Environment.