DEV Community

Silver_dev
Silver_dev

Posted on

Spring Boot Under the Hood. Part 2: Where Your application.yml Actually Goes

2.1. What Is an Environment and Why You Need One

The Environment is a central Spring object responsible for two things:

  1. Property sources — where the configuration is read from (files, env vars, args).
  2. Profiles — which profiles are active.

The interface hierarchy:

Environment
  └─ ConfigurableEnvironment
        ├─ StandardEnvironment                   (NONE)
        ├─ StandardServletEnvironment            (SERVLET)
        └─ StandardReactiveWebEnvironment        (REACTIVE)
Enter fullscreen mode Exit fullscreen mode

StandardServletEnvironment additionally mixes in ServletConfig and ServletContext as property sources.

2.2. Creating the Environment — getOrCreateEnvironment()

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    return this.applicationContextFactory.createEnvironment(
        this.webApplicationType);
}
Enter fullscreen mode Exit fullscreen mode

In ApplicationContextFactory (Boot 3):

case SERVLET:
    return new StandardServletEnvironment();
case REACTIVE:
    return new StandardReactiveWebEnvironment();
default:
    return new StandardEnvironment();
Enter fullscreen mode Exit fullscreen mode

Boot 4: the factory is gone — the Environment is created by the module (spring-boot-webmvcStandardServletEnvironment).

2.3. MutablePropertySources — the Order of Precedence

Inside the Environment sits a MutablePropertySources — an ordered list of PropertySource objects. The one higher up wins.

The order for StandardServletEnvironment (top = highest priority):

# PropertySource Example
1 commandLineArgs --server.port=9090
2 servletConfigInitParams <init-param>
3 servletContextInitParams <context-param>
4 systemProperties -Dserver.port=9090
5 systemEnvironment SERVER_PORT=9090
6 random random.int, random.uuid
7 Config resource 'application.yml' the configuration file
8 defaultProperties SpringApplication.setDefaultProperties()

Key point: @PropertySource, application-{profile}.yml, and spring.config.import entries are inserted between the system sources and defaultProperties, each at its own index.

You can verify it like this:

@Autowired
private ConfigurableEnvironment env;

env.getPropertySources().forEach(ps ->
    System.out.println(ps.getName()));
Enter fullscreen mode Exit fullscreen mode

2.4. configureEnvironment() — What Happens Before Files Are Read

protected void configureEnvironment(ConfigurableEnvironment environment,
        String[] args) {
    if (this.addCommandLineProperties) {
        addCommandLineProperties(environment, args);   // CommandLinePropertySource
    }
    configureProfiles(environment, args);              // active profiles
}
Enter fullscreen mode Exit fullscreen mode

What happens here:

SimpleCommandLinePropertySource parses --key=value.
Profiles are read from spring.profiles.active (if passed via args or system properties) before any files are loaded.

2.5. EnvironmentPostProcessor — the Extension Point

A key SPI. Loaded from META-INF/spring.factories:

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
...
Enter fullscreen mode Exit fullscreen mode

The interface:

public interface EnvironmentPostProcessor {
    void postProcessEnvironment(ConfigurableEnvironment environment,
        SpringApplication application);
}
Enter fullscreen mode Exit fullscreen mode

Ordered via @Order. Invoked by listeners.environmentPrepared(), i.e., before the ApplicationContext exists.

A custom example — adding your own property source from a database:

public class DbPropertySourcePostProcessor implements EnvironmentPostProcessor {
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment env,
            SpringApplication app) {
        env.getPropertySources().addLast(
            new MapPropertySource("dbConfig", loadFromDb()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration:

# META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.DbPropertySourcePostProcessor
Enter fullscreen mode Exit fullscreen mode

2.6. Loading Configuration Files (Boot 2.4+)

This is the meatiest part. Since 2.4 the whole mechanism has been completely rewritten — ConfigDataEnvironmentPostProcessor was introduced.

The chain:

ConfigDataEnvironmentPostProcessor.postProcessEnvironment()
  └─ ConfigDataEnvironment.processAndApply()
        ├─ ConfigDataLocationResolver   (for each location)
        │     └─ StandardConfigDataLocationResolver
        ├─ ConfigDataLoader             (for each resource)
        │     └─ StandardConfigDataLoader → PropertySource
        └─ Environment.getPropertySources().addXxx(...)
Enter fullscreen mode Exit fullscreen mode

Key classes:

  • ConfigDataEnvironment — the orchestrator.
  • ConfigDataLocationResolver — turns the string file:./config/ into a ConfigDataResource.
  • ConfigDataLoader — loads a ConfigDataResource into ConfigData (a set of property sources).
  • ConfigData — an immutable container: a name + property sources.

What's searched by default

Default locations (in increasing order of precedence):

  1. classpath:/
  2. classpath:/config/
  3. file:./
  4. file:./config/
  5. file:./config/*/

Files: application.properties / application.yml / application.yaml.

Plus profile-specific ones: application-{profile}.yml.

spring.config.*

Property What it does
spring.config.name the base name (default: application)
spring.config.location replaces the default locations
spring.config.additional-location adds to the defaults
spring.config.import imports other sources
spring.config.on-not-found fail / ignore

Example:

spring:
  config:
    import:
      - optional:file:./external.yml
      - configtree:/run/secrets/
Enter fullscreen mode Exit fullscreen mode

Prefixes:

optional: — don't fail if it's missing.
file: / classpath: — explicit protocol.
configtree: — a directory of files, where each file is a property (the file name is the key).

YAML multi-document:

# application.yml
server:
  port: 8080
---
spring:
  config:
    activate:
      on-profile: prod
server:
  port: 9090
Enter fullscreen mode Exit fullscreen mode

The second document is only activated in the prod profile.

Boot 4: the whole mechanism remains, but it's modular — spring-boot-config-data is a separate JAR.

2.7. Profiles

Activation
Methods (in decreasing order of precedence):

  1. --spring.profiles.active=dev,metrics (args)
  2. -Dspring.profiles.active=dev (system property)
  3. SPRING_PROFILES_ACTIVE=dev (env var)
  4. spring.profiles.active in application.yml
  5. SpringApplication.setAdditionalProfiles("dev") (programmatically)

Profile groups (2.4+)

spring:
  profiles:
    group:
      "prod": "proddb,prodmq"
      "dev":  "devdb,devmq"
Enter fullscreen mode Exit fullscreen mode

Activate prodproddb and prodmq are enabled.

include / default

spring:
  profiles:
    include: common
    default: local
Enter fullscreen mode Exit fullscreen mode
  • include — adds profiles to the active ones.
  • default — applies if nothing is activated.

Checking in code

@Profile("dev")
@Configuration
class DevConfig {}

@Autowired
private Environment env;

if (env.acceptsProfiles(Profiles.of("prod & !test"))) { ... }
Enter fullscreen mode Exit fullscreen mode

Profiles.of() supports expression syntax: & (and), | (or), ! (not).

2.8. @ConfigurationProperties — Typed Access

Basic example

@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    Duration timeout,
    List<String> recipients
) {}
Enter fullscreen mode Exit fullscreen mode

Activation:

@SpringBootApplication
@ConfigurationPropertiesScan   // ← scans all @ConfigurationProperties classes
public class App {}
Enter fullscreen mode Exit fullscreen mode

Or explicitly:

@EnableConfigurationProperties(MailProperties.class)
Enter fullscreen mode Exit fullscreen mode

How binding works

The chain:

ConfigurationPropertiesBindingPostProcessor   (BeanPostProcessor)
  └─ ConfigurationPropertiesBinder
        ├─ Binder                    (Spring Framework)
        ├─ PropertySources           ← from the Environment
        ├─ ConversionService         ← type conversion
        └─ Validator                 ← JSR-380
Enter fullscreen mode Exit fullscreen mode

ConfigurationPropertiesBindingPostProcessor is registered automatically via ConfigurationPropertiesAutoConfiguration.

Relaxed binding

Spring Boot matches keys in any style:

Format in the file Matches the field
app.mail.host-name hostName
app.mail.host_name hostName
app.mail.hostName hostName
APP_MAIL_HOSTNAME hostname

The canonical format is kebab-case.

The evolution of @ConstructorBinding

  • Boot 2.x — required for immutable (constructor-bound) beans.
  • Boot 3.0+ — no longer needed for records and classes with a single constructor.
  • Boot 3.x — still used when a class has multiple constructors.
  • Boot 4 — effectively legacy, rarely used.

Validation

@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
    @NotBlank private String host;
    @Min(1) private int port;
    // getters/setters
}
Enter fullscreen mode Exit fullscreen mode

At startup, if validation fails → BindValidationExceptionApplicationFailedEvent.

Binding complex types

  • Duration10s, 5m, 1h.
  • DataSize10MB, 512KB.
  • List<String> — app.mail.recipients=a,b,c or a YAML list.
  • Map<String, X> — nested keys.
  • Enum — by name (case-insensitive).

Boot 3 vs Boot 4

Aspect Boot 3 Boot 4
@ConstructorBinding Optional Legacy
Module spring-boot spring-boot-configuration-properties (extracted)
Record binding Supported Supported
AOT processing Present Improved (reflection-free by default)

2.9. Diagram: a Property's Journey from File to Bean

application.yml
      │
      ▼
StandardConfigDataLoader
      │
      ▼
ConfigData (PropertySource "Config resource '...'")
      │
      ▼
MutablePropertySources.addLast()     ← inside the Environment
      │
      ▼
Binder.bind(prefix, targetClass)
      │  (via ConfigurationPropertiesBinder)
      ▼
MailProperties (bean)
Enter fullscreen mode Exit fullscreen mode

2.10. Key Takeaways

  1. The Environment is created before the ApplicationContext — which is why an EnvironmentPostProcessor can do pretty much anything.
  2. ConfigDataEnvironmentPostProcessor is the entry point for loading application.yml (since Boot 2.4).
  3. The order of property sources decides everything — higher in the list = wins.
  4. spring.config.import is the only proper way to plug in external configuration in Boot 3/4.
  5. @ConfigurationProperties + records is the modern standard; @ConstructorBinding is no longer needed.
  6. Profile groups (spring.profiles.group.*) — handy for composition.
  7. Boot 4: the configuration mechanism has been extracted into a separate module, spring-boot-config-data.

Top comments (0)