or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin-jmx.mdansi-support.mdaot-native-image.mdapplication-info.mdavailability.mdbootstrap.mdbootstrapping.mdbuilder.mdcloud-platform.mdconfiguration-annotations.mdconfiguration-data.mdconfiguration-properties.mdconversion.mddiagnostics.mdenvironment-property-sources.mdindex.mdjson-support.mdlifecycle-events.mdlogging.mdorigin-tracking.mdresource-loading.mdretry-support.mdssl-tls.mdstartup-metrics.mdsupport.mdsystem-utilities.mdtask-execution.mdthreading.mdutilities.mdvalidation.mdweb-support.md
tile.json

configuration-properties.mddocs/

Spring Boot Configuration Properties

Quick Reference

This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.

Core Annotations

AnnotationPurposeThread SafetyKey Features
@ConfigurationPropertiesBind external properties to objectsThread-safe (Spring managed)Prefix-based binding, relaxed naming, type conversion
@EnableConfigurationPropertiesEnable specific properties classesN/A (meta-annotation)Explicit registration, component scanning alternative
@ConfigurationPropertiesScanAuto-scan for properties classesN/A (meta-annotation)Package-level scanning, base package specification
@ConstructorBindingUse constructor for bindingN/A (binding mode)Immutable properties, record support
@NestedConfigurationPropertyMark nested property objectsN/A (marker)Complex object hierarchies
@DeprecatedConfigurationPropertyMark deprecated propertiesN/A (documentation)Migration guidance, IDE warnings
@ValidatedEnable JSR-303 validationN/A (validation trigger)Startup-time validation, fail-fast

Binding API Components

ComponentPurposeThread SafetyKey Use Cases
BinderCore property binding engineThread-safeProgrammatic binding, custom sources
BindResult<T>Binding operation resultThread-safe (immutable)Success/failure handling, value extraction
Bindable<T>Target type descriptorThread-safe (immutable)Generic types, nested bindings
BindHandlerBinding lifecycle callbacksImplementation dependentValidation, transformation, logging
PropertyMapperFluent property mappingNot thread-safeThird-party configuration, conditional mapping
ConfigurationPropertySourceProperty source abstractionThread-safeCustom property sources, name resolution

Bind Handler Implementations

HandlerPurposeThread SafetyBehavior
IgnoreErrorsBindHandlerIgnore binding errorsThread-safeContinue on error, use defaults
IgnoreTopLevelConverterNotFoundBindHandlerIgnore missing convertersThread-safeAllow partial binding
NoUnboundElementsBindHandlerFail on unknown propertiesThread-safeStrict validation
ValidationBindHandlerJSR-303 validationThread-safeFail on validation errors

ConfigurationPropertiesBean

MethodPurposeThread SafetyReturns
get(ApplicationContext, Object, String)Get bean for instanceThread-safeConfigurationPropertiesBean or null
getAll(ApplicationContext)Get all properties beansThread-safeMap of bean name to ConfigurationPropertiesBean
forValueObject(Class, String)Create for value objectThread-safeConfigurationPropertiesBean

Property Naming Conventions

FormatExampleSupported Contexts
Kebab casemy-propertyProperties files (recommended)
Camel casemyPropertyProperties files, YAML
Underscoremy_propertyEnvironment variables
Upper caseMY_PROPERTYEnvironment variables

Overview

Spring Boot's Configuration Properties binding provides a type-safe way to bind external configuration (properties files, environment variables, command-line arguments) to Java objects. This system consists of annotations for marking configuration classes, a powerful binding API, and utilities for mapping properties.

Package: org.springframework.boot.context.properties Core Binding Package: org.springframework.boot.context.properties.bind

Core Annotations

@ConfigurationProperties

Marks a class or @Bean method for external configuration binding.

package org.springframework.boot.context.properties;

/**
 * Annotation for externalized configuration. Add this to a class definition or a
 * @Bean method in a @Configuration class if you want to bind and validate
 * some external Properties (e.g. from a .properties file).
 *
 * Binding is either performed by calling setters on the annotated class or, if
 * @ConstructorBinding is in use, by binding to the constructor parameters.
 *
 * Note that contrary to @Value, SpEL expressions are not evaluated since property
 * values are externalized.
 *
 * @since 1.0.0
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {

    /**
     * The prefix of the properties that are valid to bind to this object.
     * Synonym for prefix(). A valid prefix is defined by one or more words
     * separated with dots (e.g. "acme.system.feature").
     *
     * @return the prefix of the properties to bind
     */
    @AliasFor("prefix")
    String value() default "";

    /**
     * The prefix of the properties that are valid to bind to this object.
     * Synonym for value(). A valid prefix is defined by one or more words
     * separated with dots (e.g. "acme.system.feature").
     *
     * @return the prefix of the properties to bind
     */
    @AliasFor("value")
    String prefix() default "";

    /**
     * Flag to indicate that when binding to this object invalid fields should
     * be ignored. Invalid means invalid according to the binder that is used,
     * and usually this means fields of the wrong type (or that cannot be
     * coerced into the correct type).
     *
     * @return the flag value (default false)
     */
    boolean ignoreInvalidFields() default false;

    /**
     * Flag to indicate that when binding to this object unknown fields should
     * be ignored. An unknown field could be a sign of a mistake in the Properties.
     *
     * @return the flag value (default true)
     */
    boolean ignoreUnknownFields() default true;
}

Usage Example:

@ConfigurationProperties(prefix = "app.database")
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;
    private int maxConnections = 10;

    // Getters and setters
    public String getUrl() { return url; }
    public void setUrl(String url) { this.url = url; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public int getMaxConnections() { return maxConnections; }
    public void setMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
    }
}

Properties file:

app.database.url=jdbc:postgresql://localhost:5432/mydb
app.database.username=admin
app.database.password=secret
app.database.max-connections=20

@EnableConfigurationProperties

Enables support for @ConfigurationProperties beans and optionally registers specific classes.

package org.springframework.boot.context.properties;

/**
 * Enable support for @ConfigurationProperties annotated beans.
 * @ConfigurationProperties beans can be registered in the standard way
 * (for example using @Bean methods) or, for convenience, can be specified
 * directly on this annotation.
 *
 * @since 1.0.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {

    /**
     * The bean name of the configuration properties validator.
     * @since 2.2.0
     */
    String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

    /**
     * Convenient way to quickly register @ConfigurationProperties annotated
     * beans with Spring. Standard Spring Beans will also be scanned regardless
     * of this value.
     *
     * @return @ConfigurationProperties annotated beans to register
     */
    Class<?>[] value() default {};
}

Usage Example:

@Configuration
@EnableConfigurationProperties({DatabaseProperties.class, CacheProperties.class})
public class AppConfiguration {
    // Configuration beans
}

@ConfigurationPropertiesScan

Configures base packages for scanning @ConfigurationProperties classes.

package org.springframework.boot.context.properties;

/**
 * Configures the base packages used when scanning for
 * @ConfigurationProperties classes. One of basePackageClasses(), basePackages()
 * or its alias value() may be specified to define specific packages to scan.
 * If specific packages are not defined scanning will occur from the package
 * of the class with this annotation.
 *
 * Note: Classes annotated or meta-annotated with @Component will not be
 * picked up by this annotation.
 *
 * @since 2.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigurationPropertiesScanRegistrar.class)
@EnableConfigurationProperties
public @interface ConfigurationPropertiesScan {

    /**
     * Alias for the basePackages() attribute. Allows for more concise annotation
     * declarations e.g.: @ConfigurationPropertiesScan("org.my.pkg") instead of
     * @ConfigurationPropertiesScan(basePackages="org.my.pkg").
     *
     * @return the base packages to scan
     */
    @AliasFor("basePackages")
    String[] value() default {};

    /**
     * Base packages to scan for configuration properties. value() is an alias
     * for (and mutually exclusive with) this attribute.
     *
     * Use basePackageClasses() for a type-safe alternative to String-based
     * package names.
     *
     * @return the base packages to scan
     */
    @AliasFor("value")
    String[] basePackages() default {};

    /**
     * Type-safe alternative to basePackages() for specifying the packages to
     * scan for configuration properties. The package of each class specified
     * will be scanned.
     *
     * Consider creating a special no-op marker class or interface in each
     * package that serves no purpose other than being referenced by this attribute.
     *
     * @return classes from the base packages to scan
     */
    Class<?>[] basePackageClasses() default {};
}

Usage Example:

@SpringBootApplication
@ConfigurationPropertiesScan("com.example.config")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@NestedConfigurationProperty

Indicates that a property should be treated as a nested type.

package org.springframework.boot.context.properties;

/**
 * Indicates that a property in a @ConfigurationProperties object should be
 * treated as if it were a nested type. This annotation has no bearing on
 * the actual binding processes, but it is used by the
 * spring-boot-configuration-processor as a hint that a property is not bound
 * as a single value. When this is specified, a nested group is created for
 * the property and its type is harvested.
 *
 * This has no effect on collections and maps as these types are automatically
 * identified. Also, the annotation is not necessary if the target type is an
 * inner class of the @ConfigurationProperties object.
 *
 * @since 1.2.0
 */
@Target({ ElementType.FIELD, ElementType.RECORD_COMPONENT, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nested
public @interface NestedConfigurationProperty {
}

Usage Example:

@ConfigurationProperties("app.server")
public class ServerProperties {

    @NestedConfigurationProperty
    private final Host host = new Host();

    private int port = 8080;

    public Host getHost() { return host; }
    public int getPort() { return port; }
    public void setPort(int port) { this.port = port; }

    public static class Host {
        private String name;
        private String ip;

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }

        public String getIp() { return ip; }
        public void setIp(String ip) { this.ip = ip; }
    }
}

Properties file:

app.server.port=9090
app.server.host.name=localhost
app.server.host.ip=127.0.0.1

@ConstructorBinding

Indicates that constructor binding should be used instead of JavaBean binding.

package org.springframework.boot.context.properties.bind;

/**
 * Annotation that can be used to indicate which constructor to use when binding
 * configuration properties using constructor arguments rather than by calling
 * setters. A single parameterized constructor implicitly indicates that
 * constructor binding should be used unless the constructor is annotated
 * with @Autowired.
 *
 * @since 3.0.0
 */
@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {
}

Usage Example:

@ConfigurationProperties("app.service")
public class ServiceProperties {

    private final String name;
    private final int timeout;
    private final boolean enabled;

    @ConstructorBinding
    public ServiceProperties(String name, int timeout, boolean enabled) {
        this.name = name;
        this.timeout = timeout;
        this.enabled = enabled;
    }

    public String getName() { return name; }
    public int getTimeout() { return timeout; }
    public boolean isEnabled() { return enabled; }
}

With Java Records (no annotation needed):

@ConfigurationProperties("app.service")
public record ServiceProperties(String name, int timeout, boolean enabled) {
}

@DeprecatedConfigurationProperty

Marks a configuration property as deprecated for documentation purposes.

package org.springframework.boot.context.properties;

/**
 * Indicates that a getter in a @ConfigurationProperties object is deprecated.
 * This annotation has no bearing on the actual binding processes, but it is
 * used by the spring-boot-configuration-processor to add deprecation meta-data.
 *
 * This annotation must be used on the getter of the deprecated element.
 *
 * @since 1.3.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeprecatedConfigurationProperty {

    /**
     * The reason for the deprecation.
     * @return the deprecation reason
     */
    String reason() default "";

    /**
     * The field that should be used instead (if any).
     * @return the replacement field
     */
    String replacement() default "";

    /**
     * The version in which the property became deprecated.
     * @return the version
     */
    String since() default "";
}

Usage Example:

@ConfigurationProperties("app.legacy")
public class LegacyProperties {

    private String oldUrl;
    private String newUrl;

    @DeprecatedConfigurationProperty(
        replacement = "app.legacy.new-url",
        reason = "Use newUrl for better configuration",
        since = "2.0.0"
    )
    public String getOldUrl() { return oldUrl; }
    public void setOldUrl(String oldUrl) { this.oldUrl = oldUrl; }

    public String getNewUrl() { return newUrl; }
    public void setNewUrl(String newUrl) { this.newUrl = newUrl; }
}

@ConfigurationPropertiesBinding

Qualifier annotation for beans needed to configure configuration properties binding, such as custom converters.

package org.springframework.boot.context.properties;

/**
 * Qualifier for beans that are needed to configure the binding of
 * @ConfigurationProperties (e.g. Converters). Bean methods that declare a
 * @ConfigurationPropertiesBinding bean should be static to ensure that
 * "bean is not eligible for getting processed by all BeanPostProcessors"
 * warnings are not produced.
 *
 * @since 1.3.0
 */
@Qualifier(ConfigurationPropertiesBinding.VALUE)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesBinding {
    String VALUE = "org.springframework.boot.context.properties.ConfigurationPropertiesBinding";
}

Usage Example:

@Configuration
public class CustomConverterConfiguration {

    @Bean
    @ConfigurationPropertiesBinding
    public static Converter<String, CustomType> customTypeConverter() {
        return new Converter<String, CustomType>() {
            @Override
            public CustomType convert(String source) {
                return CustomType.parse(source);
            }
        };
    }

    @Bean
    @ConfigurationPropertiesBinding
    public static GenericConverter customGenericConverter() {
        return new GenericConverter() {
            @Override
            public Set<ConvertiblePair> getConvertibleTypes() {
                return Set.of(new ConvertiblePair(String.class, ComplexType.class));
            }

            @Override
            public Object convert(Object source, TypeDescriptor sourceType,
                                TypeDescriptor targetType) {
                return ComplexType.fromString((String) source);
            }
        };
    }
}

Configuration Properties Exceptions

Spring Boot provides several exception classes for handling configuration property binding failures.

ConfigurationPropertiesBindException

package org.springframework.boot.context.properties;

/**
 * Exception thrown when @ConfigurationProperties binding fails.
 *
 * @since 2.0.0
 */
public class ConfigurationPropertiesBindException extends BeanCreationException {

    /**
     * Creates a new exception for the given bean and cause.
     *
     * @param bean the configuration properties bean
     * @param cause the underlying cause
     */
    public ConfigurationPropertiesBindException(ConfigurationPropertiesBean bean, Exception cause);

    /**
     * Returns the type of the bean that failed to bind.
     *
     * @return the bean type
     */
    public Class<?> getBeanType();

    /**
     * Returns the @ConfigurationProperties annotation from the bean.
     *
     * @return the annotation
     */
    public ConfigurationProperties getAnnotation();
}

Usage:

This exception is thrown automatically by Spring Boot when configuration property binding fails. Catch it to handle binding errors gracefully:

try {
    context.getBean(MyProperties.class);
} catch (ConfigurationPropertiesBindException ex) {
    Class<?> beanType = ex.getBeanType();
    ConfigurationProperties annotation = ex.getAnnotation();
    System.err.println("Failed to bind " + annotation.prefix() + " to " + beanType.getName());
}

IncompatibleConfigurationException

package org.springframework.boot.context.properties;

/**
 * Exception thrown when the application has configured an incompatible set
 * of @ConfigurationProperties keys.
 *
 * @since 2.4.0
 */
public class IncompatibleConfigurationException extends RuntimeException {

    /**
     * Creates a new exception for the given incompatible keys.
     *
     * @param incompatibleKeys the incompatible configuration keys
     */
    public IncompatibleConfigurationException(String... incompatibleKeys);

    /**
     * Returns the incompatible configuration keys.
     *
     * @return the incompatible keys
     */
    public Collection<String> getIncompatibleKeys();
}

Usage:

Throw this exception when multiple incompatible properties are configured:

@ConfigurationProperties("app")
public class AppProperties {

    private String legacyUrl;
    private String newUrl;

    @PostConstruct
    public void validate() {
        if (legacyUrl != null && newUrl != null) {
            throw new IncompatibleConfigurationException(
                "app.legacy-url",
                "app.new-url"
            );
        }
    }
}

MutuallyExclusiveConfigurationPropertiesException

package org.springframework.boot.context.properties.source;

/**
 * Exception thrown when more than one mutually exclusive configuration property
 * has been configured.
 *
 * @since 2.6.0
 */
public class MutuallyExclusiveConfigurationPropertiesException extends RuntimeException {

    /**
     * Creates a new exception for the given configured and mutually exclusive names.
     *
     * @param configuredNames the names that have been configured
     * @param mutuallyExclusiveNames all the mutually exclusive names
     */
    public MutuallyExclusiveConfigurationPropertiesException(
        Collection<String> configuredNames,
        Collection<String> mutuallyExclusiveNames);

    /**
     * Returns the names that have been configured.
     *
     * @return the configured names
     */
    public Set<String> getConfiguredNames();

    /**
     * Returns all the names that are mutually exclusive.
     *
     * @return the mutually exclusive names
     */
    public Set<String> getMutuallyExclusiveNames();

    /**
     * Throws an exception if multiple non-null values are present in the entries.
     *
     * @param entries a consumer that accepts a map to populate with name/value pairs
     * @throws MutuallyExclusiveConfigurationPropertiesException if multiple non-null values
     */
    public static void throwIfMultipleNonNullValuesIn(
        Consumer<Map<String, @Nullable Object>> entries);

    /**
     * Throws an exception if multiple values matching the predicate are present.
     *
     * @param entries a consumer that accepts a map to populate with name/value pairs
     * @param predicate the predicate to test values
     * @param <V> the value type
     * @throws MutuallyExclusiveConfigurationPropertiesException if multiple matching values
     * @since 3.3.7
     */
    public static <V> void throwIfMultipleMatchingValuesIn(
        Consumer<Map<String, @Nullable V>> entries,
        Predicate<@Nullable V> predicate);
}

Usage:

Validate mutually exclusive properties:

@ConfigurationProperties("app.database")
public class DatabaseProperties {

    private String jdbcUrl;
    private DataSourceConfig datasource;
    private JndiConfig jndi;

    @PostConstruct
    public void validate() {
        MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn(
            (entries) -> {
                entries.put("app.database.jdbc-url", jdbcUrl);
                entries.put("app.database.datasource", datasource);
                entries.put("app.database.jndi", jndi);
            }
        );
    }
}

UnboundConfigurationPropertiesException

package org.springframework.boot.context.properties.bind;

/**
 * BindException thrown when ConfigurationPropertySource elements were left unbound.
 *
 * @since 2.0.0
 */
public class UnboundConfigurationPropertiesException extends RuntimeException {

    /**
     * Creates a new exception for the given unbound properties.
     *
     * @param unboundProperties the properties that were not bound
     */
    public UnboundConfigurationPropertiesException(Set<ConfigurationProperty> unboundProperties);

    /**
     * Returns the properties that were not bound.
     *
     * @return the unbound properties as an immutable set
     */
    public Set<ConfigurationProperty> getUnboundProperties();
}

Usage:

This exception is thrown when strict binding is enabled and properties remain unbound:

import org.springframework.boot.context.properties.bind.*;

Binder binder = Binder.get(environment);
BindHandler handler = new NoUnboundElementsBindHandler();

try {
    MyProperties props = binder.bind("app", Bindable.of(MyProperties.class), handler)
        .get();
} catch (UnboundConfigurationPropertiesException ex) {
    Set<ConfigurationProperty> unbound = ex.getUnboundProperties();
    unbound.forEach(prop -> {
        System.err.println("Unbound property: " + prop.getName());
    });
}

@ConfigurationPropertiesSource

package org.springframework.boot.context.properties;

/**
 * Indicates that the annotated type is a source of configuration properties metadata.
 * Serves as a hint to the spring-boot-configuration-processor to generate full
 * metadata for the type.
 *
 * @since 4.0.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesSource {
}

Usage:

Use this annotation on types that provide configuration metadata but are in different modules:

@ConfigurationPropertiesSource
public class ServerConfig {

    private int port = 8080;
    private String host = "localhost";

    // Getters and setters
}

@ConfigurationProperties("app.server")
public class AppServerProperties {

    private ServerConfig config = new ServerConfig();

    public ServerConfig getConfig() {
        return config;
    }

    public void setConfig(ServerConfig config) {
        this.config = config;
    }
}

Configuration Property Source API

The source package provides low-level access to configuration properties before binding.

ConfigurationPropertySource

package org.springframework.boot.context.properties.source;

/**
 * A source of ConfigurationProperties.
 *
 * @since 2.0.0
 */
@FunctionalInterface
public interface ConfigurationPropertySource {

    /**
     * Returns a single configuration property from the source.
     *
     * @param name the property name
     * @return the configuration property or null
     */
    @Nullable
    ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);

    /**
     * Returns the state of any descendants of the given name.
     *
     * @param name the name to check
     * @return the property state
     */
    default ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name);

    /**
     * Returns a filtered variant of this source.
     *
     * @param filter the filter to apply
     * @return the filtered source
     */
    default ConfigurationPropertySource filter(Predicate<ConfigurationPropertyName> filter);

    /**
     * Returns a variant of this source with name aliases.
     *
     * @param aliases the aliases to apply
     * @return the source with aliases
     */
    default ConfigurationPropertySource withAliases(ConfigurationPropertyNameAliases aliases);

    /**
     * Returns a variant of this source with a prefix.
     *
     * @param prefix the prefix to add
     * @return the source with prefix
     * @since 2.5.0
     */
    default ConfigurationPropertySource withPrefix(@Nullable String prefix);

    /**
     * Returns the underlying source object.
     *
     * @return the underlying source or null
     */
    default @Nullable Object getUnderlyingSource();

    /**
     * Creates a ConfigurationPropertySource from a PropertySource.
     *
     * @param source the property source
     * @return the configuration property source or null
     * @since 2.4.0
     */
    static @Nullable ConfigurationPropertySource from(PropertySource<?> source);
}

ConfigurationPropertyNameAliases

package org.springframework.boot.context.properties.source;

import java.util.Iterator;
import java.util.List;

/**
 * Maintains a mapping of ConfigurationPropertyName aliases.
 * Allows configuration properties to be accessed using alternative names.
 *
 * @since 2.0.0
 */
public final class ConfigurationPropertyNameAliases implements Iterable<ConfigurationPropertyName> {

    /**
     * Creates an empty set of aliases.
     */
    public ConfigurationPropertyNameAliases();

    /**
     * Creates aliases with an initial mapping.
     *
     * @param name the configuration property name
     * @param aliases the aliases for the name
     */
    public ConfigurationPropertyNameAliases(String name, String... aliases);

    /**
     * Creates aliases with an initial mapping.
     *
     * @param name the configuration property name
     * @param aliases the aliases for the name
     */
    public ConfigurationPropertyNameAliases(ConfigurationPropertyName name, ConfigurationPropertyName... aliases);

    /**
     * Adds aliases for a configuration property name.
     *
     * @param name the configuration property name
     * @param aliases the aliases to add
     */
    public void addAliases(String name, String... aliases);

    /**
     * Adds aliases for a configuration property name.
     *
     * @param name the configuration property name
     * @param aliases the aliases to add
     */
    public void addAliases(ConfigurationPropertyName name, ConfigurationPropertyName... aliases);

    /**
     * Gets all aliases for the given name.
     *
     * @param name the configuration property name
     * @return list of aliases (may be empty)
     */
    public List<ConfigurationPropertyName> getAliases(ConfigurationPropertyName name);

    /**
     * Gets the name for the given alias.
     *
     * @param alias the alias
     * @return the name or null if no name is found
     */
    public ConfigurationPropertyName getNameForAlias(ConfigurationPropertyName alias);

    @Override
    public Iterator<ConfigurationPropertyName> iterator();
}

Usage Example:

import org.springframework.boot.context.properties.source.*;

// Create aliases for legacy property names
ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
aliases.addAliases("server.port", "port", "server.http.port");
aliases.addAliases("spring.datasource.url", "database.url", "db.url");

// Apply aliases to a configuration property source
ConfigurationPropertySource source = ConfigurationPropertySources.get(environment).iterator().next();
ConfigurationPropertySource aliasedSource = source.withAliases(aliases);

// Now properties can be accessed via their aliases
// "port" will resolve to "server.port"
// "database.url" will resolve to "spring.datasource.url"

Common Use Cases:

  • Supporting legacy property names during migration
  • Providing shorter alternative names for frequently used properties
  • Supporting multiple naming conventions (e.g., camelCase vs kebab-case alternatives)

ConfigurationProperty

package org.springframework.boot.context.properties.source;

/**
 * A single configuration property obtained from a ConfigurationPropertySource
 * consisting of a name, value and optional origin.
 *
 * @since 2.0.0
 */
public final class ConfigurationProperty
    implements OriginProvider, Comparable<ConfigurationProperty> {

    /**
     * Creates a new configuration property.
     *
     * @param name the property name
     * @param value the property value
     * @param origin the property origin (may be null)
     */
    public ConfigurationProperty(
        ConfigurationPropertyName name,
        Object value,
        @Nullable Origin origin);

    /**
     * Returns the configuration property source that provided this property.
     *
     * @return the source or null
     * @since 2.6.0
     */
    public @Nullable ConfigurationPropertySource getSource();

    /**
     * Returns the name of the property.
     *
     * @return the property name
     */
    public ConfigurationPropertyName getName();

    /**
     * Returns the value of the property.
     *
     * @return the property value
     */
    public Object getValue();

    /**
     * Returns the origin of the property.
     *
     * @return the origin or null
     */
    @Override
    public @Nullable Origin getOrigin();

    @Override
    public int compareTo(ConfigurationProperty other);
}

ConfigurationPropertyName

package org.springframework.boot.context.properties.source;

/**
 * A configuration property name composed of elements separated by dots.
 * User created names may contain "a-z", "0-9", "-", must be lower-case
 * and must start with an alphanumeric character.
 *
 * @since 2.0.0
 */
public final class ConfigurationPropertyName
    implements Comparable<ConfigurationPropertyName> {

    /**
     * An empty configuration property name.
     */
    public static final ConfigurationPropertyName EMPTY;

    /**
     * Returns true if the name is empty.
     *
     * @return true if empty
     */
    public boolean isEmpty();

    /**
     * Returns true if the last element is indexed.
     *
     * @return true if indexed
     */
    public boolean isLastElementIndexed();

    /**
     * Returns true if the name contains any indexed elements.
     *
     * @return true if has indexed element
     * @since 2.2.10
     */
    public boolean hasIndexedElement();

    /**
     * Returns true if the specified element is indexed.
     *
     * @param elementIndex the element index
     * @return true if indexed
     */
    boolean isIndexed(int elementIndex);

    /**
     * Returns true if the element contains only digits.
     *
     * @param elementIndex the element index
     * @return true if numeric
     */
    public boolean isNumericIndex(int elementIndex);

    /**
     * Returns the last element in the specified form.
     *
     * @param form the form to use
     * @return the last element
     */
    public String getLastElement(Form form);

    /**
     * Returns the element at the specified index in the specified form.
     *
     * @param elementIndex the element index
     * @param form the form to use
     * @return the element
     */
    public String getElement(int elementIndex, Form form);

    /**
     * Returns the total number of elements.
     *
     * @return the number of elements
     */
    public int getNumberOfElements();

    /**
     * Returns a new name by appending the suffix.
     *
     * @param suffix the suffix to append
     * @return the new name
     */
    public ConfigurationPropertyName append(@Nullable String suffix);

    /**
     * Returns a new name by appending another name.
     *
     * @param suffix the name to append
     * @return the new name
     * @since 2.5.0
     */
    public ConfigurationPropertyName append(@Nullable ConfigurationPropertyName suffix);

    /**
     * Returns the parent of this name.
     *
     * @return the parent or null
     */
    public ConfigurationPropertyName getParent();

    /**
     * Returns a new name chopped to the specified size.
     *
     * @param size the size to chop to
     * @return the chopped name
     */
    public ConfigurationPropertyName chop(int size);

    /**
     * Returns a sub-name starting at the offset.
     *
     * @param offset the starting offset
     * @return the sub-name
     * @since 2.5.0
     */
    public ConfigurationPropertyName subName(int offset);

    /**
     * Returns true if this name is a parent of the specified name.
     *
     * @param name the name to check
     * @return true if parent
     */
    public boolean isParentOf(ConfigurationPropertyName name);

    /**
     * Returns true if this name is an ancestor of the specified name.
     *
     * @param name the name to check
     * @return true if ancestor
     */
    public boolean isAncestorOf(ConfigurationPropertyName name);

    @Override
    public int compareTo(ConfigurationPropertyName other);

    /**
     * Checks if a character sequence is a valid configuration property name.
     *
     * @param name the name to check
     * @return true if valid
     */
    public static boolean isValid(@Nullable CharSequence name);

    /**
     * Creates a name from a character sequence.
     *
     * @param name the name
     * @return the configuration property name
     * @throws InvalidConfigurationPropertyNameException if invalid
     */
    public static ConfigurationPropertyName of(@Nullable CharSequence name);

    /**
     * Creates a name if valid, otherwise returns null.
     *
     * @param name the name
     * @return the configuration property name or null
     * @since 2.3.1
     */
    public static @Nullable ConfigurationPropertyName ofIfValid(@Nullable CharSequence name);

    /**
     * Adapts a name using a custom separator.
     *
     * @param name the name to adapt
     * @param separator the separator character
     * @return the adapted name
     */
    public static ConfigurationPropertyName adapt(CharSequence name, char separator);

    /**
     * The form used for a configuration property name element.
     */
    public enum Form {
        /**
         * The original form as specified.
         */
        ORIGINAL,

        /**
         * Lower-case with only alphanumeric and dashes.
         */
        DASHED,

        /**
         * Lower-case with only alphanumeric (for equals/hashCode).
         */
        UNIFORM
    }
}

Usage Example:

import org.springframework.boot.context.properties.source.*;
import org.springframework.core.env.Environment;

@Component
public class ConfigPropertyInspector {

    private final Environment environment;

    public ConfigPropertyInspector(Environment environment) {
        this.environment = environment;
    }

    public void inspectProperty(String propertyName) {
        ConfigurationPropertyName name = ConfigurationPropertyName.of(propertyName);

        // Get property sources
        Iterable<ConfigurationPropertySource> sources =
            ConfigurationPropertySources.get(environment);

        for (ConfigurationPropertySource source : sources) {
            ConfigurationProperty property = source.getConfigurationProperty(name);
            if (property != null) {
                System.out.println("Property: " + property.getName());
                System.out.println("Value: " + property.getValue());
                System.out.println("Origin: " + property.getOrigin());
                System.out.println("Source: " + property.getSource());
                break;
            }
        }
    }
}

Binder API

The Binder class provides the low-level binding API for converting configuration properties to typed objects.

Binder Class

package org.springframework.boot.context.properties.bind;

/**
 * A container object which Binds objects from one or more
 * ConfigurationPropertySources.
 *
 * @since 2.0.0
 */
public class Binder {

    /**
     * Create a new Binder instance for the specified sources. A
     * DefaultFormattingConversionService will be used for all conversion.
     *
     * @param sources the sources used for binding
     */
    public Binder(ConfigurationPropertySource... sources);

    /**
     * Create a new Binder instance for the specified sources. A
     * DefaultFormattingConversionService will be used for all conversion.
     *
     * @param sources the sources used for binding
     */
    public Binder(Iterable<ConfigurationPropertySource> sources);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     * @param conversionService the conversion service to convert values (or null
     *                         to use ApplicationConversionService)
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver,
                  @Nullable ConversionService conversionService);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     * @param conversionService the conversion service to convert values (or null
     *                         to use ApplicationConversionService)
     * @param propertyEditorInitializer initializer used to configure the property
     *                                  editors that can convert values (or null if
     *                                  no initialization is required)
     * @since 2.0.0
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver,
                  @Nullable ConversionService conversionService,
                  Consumer<PropertyEditorRegistry> propertyEditorInitializer);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     * @param conversionService the conversion service to convert values (or null
     *                         to use ApplicationConversionService)
     * @param propertyEditorInitializer initializer used to configure the property
     *                                  editors that can convert values (or null if
     *                                  no initialization is required)
     * @param defaultBindHandler the default bind handler to use if none is specified
     *                          when binding
     * @since 2.2.0
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver,
                  @Nullable ConversionService conversionService,
                  Consumer<PropertyEditorRegistry> propertyEditorInitializer,
                  BindHandler defaultBindHandler);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     * @param conversionService the conversion service to convert values (or null
     *                         to use ApplicationConversionService)
     * @param propertyEditorInitializer initializer used to configure the property
     *                                  editors that can convert values (or null if
     *                                  no initialization is required)
     * @param defaultBindHandler the default bind handler to use if none is specified
     *                          when binding
     * @param constructorProvider the constructor provider which provides the bind
     *                           constructor to use when binding
     * @since 2.2.1
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver,
                  @Nullable ConversionService conversionService,
                  Consumer<PropertyEditorRegistry> propertyEditorInitializer,
                  BindHandler defaultBindHandler,
                  BindConstructorProvider constructorProvider);

    /**
     * Create a new Binder instance for the specified sources.
     *
     * @param sources the sources used for binding
     * @param placeholdersResolver strategy to resolve any property placeholders
     * @param conversionServices the conversion services to convert values (or null
     *                          to use ApplicationConversionService)
     * @param propertyEditorInitializer initializer used to configure the property
     *                                  editors that can convert values (or null if
     *                                  no initialization is required)
     * @param defaultBindHandler the default bind handler to use if none is specified
     *                          when binding
     * @param constructorProvider the constructor provider which provides the bind
     *                           constructor to use when binding
     * @since 2.5.0
     */
    public Binder(Iterable<ConfigurationPropertySource> sources,
                  @Nullable PlaceholdersResolver placeholdersResolver,
                  @Nullable List<ConversionService> conversionServices,
                  Consumer<PropertyEditorRegistry> propertyEditorInitializer,
                  BindHandler defaultBindHandler,
                  BindConstructorProvider constructorProvider);

    /**
     * Bind the specified target Class using this binder's property sources.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target class
     * @return the binding result (never null)
     */
    public <T> BindResult<T> bind(String name, Class<T> target);

    /**
     * Bind the specified target Bindable using this binder's property sources.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @return the binding result (never null)
     */
    public <T> BindResult<T> bind(String name, Bindable<T> target);

    /**
     * Bind the specified target Bindable using this binder's property sources.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @return the binding result (never null)
     */
    public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target);

    /**
     * Bind the specified target Bindable using this binder's property sources.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @param handler the bind handler (may be null)
     * @return the binding result (never null)
     */
    public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler);

    /**
     * Bind the specified target Bindable using this binder's property sources.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @param handler the bind handler (may be null)
     * @return the binding result (never null)
     */
    public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target,
                                   BindHandler handler);

    /**
     * Bind the specified target Class using this binder's property sources or
     * create a new instance of the specified target Class if the result of the
     * binding is null.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target class
     * @return the bound or created object
     * @since 2.2.0
     */
    public <T> T bindOrCreate(String name, Class<T> target);

    /**
     * Bind the specified target Bindable using this binder's property sources or
     * create a new instance using the type of the Bindable if the result of the
     * binding is null.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @return the bound or created object
     * @since 2.2.0
     */
    public <T> T bindOrCreate(String name, Bindable<T> target);

    /**
     * Bind the specified target Bindable using this binder's property sources or
     * create a new instance using the type of the Bindable if the result of the
     * binding is null.
     *
     * @param <T> the bound type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @param handler the bind handler
     * @return the bound or created object
     * @since 2.2.0
     */
    public <T> T bindOrCreate(String name, Bindable<T> target, BindHandler handler);

    /**
     * Bind the specified target Bindable using this binder's property sources or
     * create a new instance using the type of the Bindable if the result of the
     * binding is null.
     *
     * @param <T> the bound or created type
     * @param name the configuration property name to bind
     * @param target the target bindable
     * @param handler the bind handler (may be null)
     * @return the bound or created object
     * @since 2.2.0
     */
    public <T> T bindOrCreate(ConfigurationPropertyName name, Bindable<T> target,
                              BindHandler handler);

    /**
     * Create a new Binder instance from the specified environment.
     *
     * @param environment the environment source (must have attached
     *                   ConfigurationPropertySources)
     * @return a Binder instance
     */
    public static Binder get(Environment environment);

    /**
     * Create a new Binder instance from the specified environment.
     *
     * @param environment the environment source (must have attached
     *                   ConfigurationPropertySources)
     * @param defaultBindHandler the default bind handler to use if none is specified
     *                          when binding
     * @return a Binder instance
     * @since 2.2.0
     */
    public static Binder get(Environment environment, BindHandler defaultBindHandler);
}

Usage Example:

// Simple binding
Environment environment = context.getEnvironment();
Binder binder = Binder.get(environment);

// Bind to a class
BindResult<DatabaseConfig> result = binder.bind("app.database", DatabaseConfig.class);
if (result.isBound()) {
    DatabaseConfig config = result.get();
    System.out.println("URL: " + config.getUrl());
}

// Bind with Bindable
Bindable<List<String>> bindable = Bindable.listOf(String.class);
BindResult<List<String>> servers = binder.bind("app.servers", bindable);

// Bind or create with defaults
DatabaseConfig config = binder.bindOrCreate("app.database", DatabaseConfig.class);

Bindable<T>

Describes a type that can be bound.

package org.springframework.boot.context.properties.bind;

/**
 * Source that can be bound by a Binder.
 *
 * @param <T> the source type
 * @since 2.0.0
 */
public final class Bindable<T> {

    /**
     * Return the type of the item to bind.
     * @return the type being bound
     */
    public ResolvableType getType();

    /**
     * Return the boxed type of the item to bind.
     * @return the boxed type for the item being bound
     */
    public ResolvableType getBoxedType();

    /**
     * Return a supplier that provides the object value or null.
     * @return the value or null
     */
    public Supplier<T> getValue();

    /**
     * Return any associated annotations that could affect binding.
     * @return the associated annotations
     */
    public Annotation[] getAnnotations();

    /**
     * Return a single associated annotations that could affect binding.
     *
     * @param <A> the annotation type
     * @param type annotation type
     * @return the associated annotation or null
     */
    public <A extends Annotation> A getAnnotation(Class<A> type);

    /**
     * Returns true if the specified bind restriction has been added.
     *
     * @param bindRestriction the bind restriction to check
     * @return if the bind restriction has been added
     * @since 2.5.0
     */
    public boolean hasBindRestriction(BindRestriction bindRestriction);

    /**
     * Returns the BindMethod to be used to bind this bindable, or null if no
     * specific binding method is required.
     *
     * @return the bind method or null
     * @since 3.0.8
     */
    public BindMethod getBindMethod();

    /**
     * Create an updated Bindable instance with the specified annotations.
     *
     * @param annotations the annotations
     * @return an updated Bindable
     */
    public Bindable<T> withAnnotations(Annotation... annotations);

    /**
     * Create an updated Bindable instance with an existing value.
     * Implies that Java Bean binding will be used.
     *
     * @param existingValue the existing value
     * @return an updated Bindable
     */
    public Bindable<T> withExistingValue(T existingValue);

    /**
     * Create an updated Bindable instance with a value supplier.
     *
     * @param suppliedValue the supplier for the value
     * @return an updated Bindable
     */
    public Bindable<T> withSuppliedValue(Supplier<T> suppliedValue);

    /**
     * Create an updated Bindable instance with additional bind restrictions.
     *
     * @param additionalRestrictions any additional restrictions to apply
     * @return an updated Bindable
     * @since 2.5.0
     */
    public Bindable<T> withBindRestrictions(BindRestriction... additionalRestrictions);

    /**
     * Create an updated Bindable instance with a specific bind method.
     *
     * @param bindMethod the method to use to bind the bindable
     * @return an updated Bindable
     * @since 3.0.8
     */
    public Bindable<T> withBindMethod(BindMethod bindMethod);

    /**
     * Create a new Bindable of the type of the specified instance with an
     * existing value equal to the instance.
     *
     * @param <T> the source type
     * @param instance the instance (must not be null)
     * @return a Bindable instance
     */
    public static <T> Bindable<T> ofInstance(T instance);

    /**
     * Create a new Bindable of the specified type.
     *
     * @param <T> the source type
     * @param type the type (must not be null)
     * @return a Bindable instance
     */
    public static <T> Bindable<T> of(Class<T> type);

    /**
     * Create a new Bindable List of the specified element type.
     *
     * @param <E> the element type
     * @param elementType the list element type
     * @return a Bindable instance
     */
    public static <E> Bindable<List<E>> listOf(Class<E> elementType);

    /**
     * Create a new Bindable Set of the specified element type.
     *
     * @param <E> the element type
     * @param elementType the set element type
     * @return a Bindable instance
     */
    public static <E> Bindable<Set<E>> setOf(Class<E> elementType);

    /**
     * Create a new Bindable Map of the specified key and value type.
     *
     * @param <K> the key type
     * @param <V> the value type
     * @param keyType the map key type
     * @param valueType the map value type
     * @return a Bindable instance
     */
    public static <K, V> Bindable<Map<K, V>> mapOf(Class<K> keyType, Class<V> valueType);

    /**
     * Create a new Bindable of the specified type.
     *
     * @param <T> the source type
     * @param type the type (must not be null)
     * @return a Bindable instance
     */
    public static <T> Bindable<T> of(ResolvableType type);

    /**
     * Restrictions that can be applied when binding values.
     * @since 2.5.0
     */
    public enum BindRestriction {
        /**
         * Do not bind direct ConfigurationProperty matches.
         */
        NO_DIRECT_PROPERTY
    }
}

Usage Example:

// Simple type binding
Bindable<String> stringBindable = Bindable.of(String.class);

// Collection binding
Bindable<List<Integer>> listBindable = Bindable.listOf(Integer.class);
Bindable<Set<String>> setBindable = Bindable.setOf(String.class);
Bindable<Map<String, Integer>> mapBindable = Bindable.mapOf(String.class, Integer.class);

// Binding with existing instance
DatabaseConfig existing = new DatabaseConfig();
Bindable<DatabaseConfig> withExisting = Bindable.of(DatabaseConfig.class)
    .withExistingValue(existing);

// Binding with annotations
Bindable<ServerConfig> withValidation = Bindable.of(ServerConfig.class)
    .withAnnotations(new Validated() {
        @Override
        public Class<? extends Annotation> annotationType() {
            return Validated.class;
        }
    });

BindResult<T>

A container object to return the result of a bind operation.

package org.springframework.boot.context.properties.bind;

/**
 * A container object to return the result of a Binder bind operation. May contain
 * either a successfully bound object or an empty result.
 *
 * @param <T> the result type
 * @since 2.0.0
 */
public final class BindResult<T> {

    /**
     * Return the object that was bound or throw a NoSuchElementException if no
     * value was bound.
     *
     * @return the bound value (never null)
     * @throws NoSuchElementException if no value was bound
     */
    public T get() throws NoSuchElementException;

    /**
     * Returns true if a result was bound.
     * @return if a result was bound
     */
    public boolean isBound();

    /**
     * Invoke the specified consumer with the bound value, or do nothing if no
     * value has been bound.
     *
     * @param consumer block to execute if a value has been bound
     */
    public void ifBound(Consumer<? super T> consumer);

    /**
     * Apply the provided mapping function to the bound value, or return an
     * updated unbound result if no value has been bound.
     *
     * @param <U> the type of the result of the mapping function
     * @param mapper a mapping function to apply to the bound value
     * @return a BindResult describing the result of applying a mapping function
     *         to the value of this BindResult
     */
    public <U> BindResult<U> map(Function<? super T, ? extends U> mapper);

    /**
     * Return the object that was bound, or other if no value has been bound.
     *
     * @param other the value to be returned if there is no bound value
     *              (may be null)
     * @return the value, if bound, otherwise other
     */
    public T orElse(T other);

    /**
     * Return the object that was bound, or the result of invoking other if no
     * value has been bound.
     *
     * @param other a Supplier of the value to be returned if there is no bound value
     * @return the value, if bound, otherwise the supplied other
     */
    public T orElseGet(Supplier<? extends T> other);

    /**
     * Return the object that was bound, or throw an exception to be created by
     * the provided supplier if no value has been bound.
     *
     * @param <X> the type of the exception to be thrown
     * @param exceptionSupplier the supplier which will return the exception to be thrown
     * @return the present value
     * @throws X if there is no value present
     */
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
        throws X;
}

Usage Example:

Binder binder = Binder.get(environment);

// Check if bound
BindResult<String> result = binder.bind("app.name", String.class);
if (result.isBound()) {
    String name = result.get();
}

// Use ifBound
result.ifBound(name -> System.out.println("App name: " + name));

// Provide default value
String appName = result.orElse("default-app");

// Provide default via supplier
String appName2 = result.orElseGet(() -> "computed-default");

// Throw exception if not bound
String appName3 = result.orElseThrow(
    () -> new IllegalStateException("app.name is required")
);

// Map the result
BindResult<Integer> length = result.map(String::length);

BindHandler

Callback interface for handling logic during element binding.

package org.springframework.boot.context.properties.bind;

/**
 * Callback interface that can be used to handle additional logic during element
 * Binder binding.
 *
 * @since 2.0.0
 */
public interface BindHandler {

    /**
     * Default no-op bind handler.
     */
    BindHandler DEFAULT = new BindHandler() {};

    /**
     * Called when binding of an element starts but before any result has been
     * determined.
     *
     * @param <T> the bindable source type
     * @param name the name of the element being bound
     * @param target the item being bound
     * @param context the bind context
     * @return the actual item that should be used for binding (may be null)
     */
    default <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
                                     BindContext context) {
        return target;
    }

    /**
     * Called when binding of an element ends with a successful result.
     * Implementations may change the ultimately returned result or perform
     * addition validation.
     *
     * @param name the name of the element being bound
     * @param target the item being bound
     * @param context the bind context
     * @param result the bound result (never null)
     * @return the actual result that should be used (may be null)
     */
    default Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                            BindContext context, Object result) {
        return result;
    }

    /**
     * Called when binding of an element ends with an unbound result and a newly
     * created instance is about to be returned. Implementations may change the
     * ultimately returned result or perform addition validation.
     *
     * @param name the name of the element being bound
     * @param target the item being bound
     * @param context the bind context
     * @param result the newly created instance (never null)
     * @return the actual result that should be used (must not be null)
     * @since 2.2.2
     */
    default Object onCreate(ConfigurationPropertyName name, Bindable<?> target,
                           BindContext context, Object result) {
        return result;
    }

    /**
     * Called when binding fails for any reason (including failures from
     * onSuccess or onCreate calls). Implementations may choose to swallow
     * exceptions and return an alternative result.
     *
     * @param name the name of the element being bound
     * @param target the item being bound
     * @param context the bind context
     * @param error the cause of the error (if the exception stands it may be re-thrown)
     * @return the actual result that should be used (may be null)
     * @throws Exception if the binding isn't valid
     */
    default Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                            BindContext context, Exception error) throws Exception {
        throw error;
    }

    /**
     * Called when binding finishes with either bound or unbound result. This
     * method will not be called when binding failed, even if a handler returns
     * a result from onFailure.
     *
     * @param name the name of the element being bound
     * @param target the item being bound
     * @param context the bind context
     * @param result the bound result (may be null)
     * @throws Exception if the binding isn't valid
     */
    default void onFinish(ConfigurationPropertyName name, Bindable<?> target,
                         BindContext context, Object result) throws Exception {
    }
}

Usage Example:

// Custom BindHandler that logs binding operations
public class LoggingBindHandler implements BindHandler {

    private static final Logger log = LoggerFactory.getLogger(LoggingBindHandler.class);

    @Override
    public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
                                     BindContext context) {
        log.debug("Starting bind: {}", name);
        return target;
    }

    @Override
    public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                           BindContext context, Object result) {
        log.debug("Bound successfully: {} = {}", name, result);
        return result;
    }

    @Override
    public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                           BindContext context, Exception error) throws Exception {
        log.error("Binding failed for: {}", name, error);
        throw error;
    }
}

// Use custom handler
Binder binder = Binder.get(environment);
BindHandler handler = new LoggingBindHandler();
BindResult<DatabaseConfig> result = binder.bind("app.database",
    Bindable.of(DatabaseConfig.class), handler);

PlaceholdersResolver

Strategy for resolving property placeholders.

package org.springframework.boot.context.properties.bind;

/**
 * Strategy interface used to resolve placeholder values.
 *
 * @since 2.0.0
 */
@FunctionalInterface
public interface PlaceholdersResolver {

    /**
     * No-op PropertyResolver.
     */
    PlaceholdersResolver NONE = (value) -> value;

    /**
     * Resolve placeholders in the given value.
     * @param value the value to resolve (may be {@code null})
     * @return the resolved value (may be {@code null})
     */
    @Nullable
    Object resolvePlaceholders(Object value);
}

Usage Example:

// Custom PlaceholdersResolver
PlaceholdersResolver resolver = value -> {
    if (value instanceof String str) {
        return str.replace("${home}", System.getProperty("user.home"));
    }
    return value;
};

// Use with Binder
Iterable<ConfigurationPropertySource> sources =
    ConfigurationPropertySources.get(environment);
Binder binder = new Binder(sources, resolver);

BindConstructorProvider

Strategy interface for determining which constructor to use for binding.

package org.springframework.boot.context.properties.bind;

/**
 * Strategy interface used to determine a specific constructor to use when binding.
 *
 * @since 2.2.1
 */
@FunctionalInterface
public interface BindConstructorProvider {

    /**
     * Default BindConstructorProvider implementation that only returns a value
     * when there's a single constructor and when the bindable has no existing value.
     */
    BindConstructorProvider DEFAULT = new DefaultBindConstructorProvider();

    /**
     * Return the bind constructor to use for the given bindable, or null if
     * constructor binding is not supported.
     *
     * @param bindable the bindable to check
     * @param isNestedConstructorBinding if this binding is nested within a
     *                                   constructor binding
     * @return the bind constructor or null
     */
    Constructor<?> getBindConstructor(Bindable<?> bindable,
                                     boolean isNestedConstructorBinding);

    /**
     * Return the bind constructor for the given type considering nested constructor binding.
     * @param type the type to return
     * @param isNestedConstructorBinding whether nested constructor binding is being used
     * @return the bind constructor or {@code null}
     * @since 3.0.0
     */
    default Constructor<?> getBindConstructor(Class<?> type,
                                              boolean isNestedConstructorBinding) {
        return getBindConstructor(Bindable.of(type), isNestedConstructorBinding);
    }
}

Binding Framework Support Classes

BindContext

Context information for use by BindHandler implementations.

package org.springframework.boot.context.properties.bind;

/**
 * Context information for use by {@link BindHandler BindHandlers}.
 *
 * @since 2.0.0
 */
public interface BindContext {

    /**
     * Return the binder that is performing the current bind operation.
     * @return the binder
     */
    Binder getBinder();

    /**
     * Return the current depth of the binding. Root binding starts with a depth of {@code 0}. Each sub-binding increases the depth by {@code 1}.
     * @return the depth of the current binding
     */
    int getDepth();

    /**
     * Return the configuration property sources used by the binder.
     * @return the sources
     */
    Iterable<ConfigurationPropertySource> getSources();

    /**
     * Return the {@link ConfigurationProperty} being bound or {@code null} if the property has not yet been determined.
     * @return the configuration property (may be {@code null})
     */
    @Nullable
    ConfigurationProperty getConfigurationProperty();
}

BindMethod

Enum defining configuration property binding methods.

package org.springframework.boot.context.properties.bind;

/**
 * Bind method that can be used when binding an object.
 *
 * @since 2.5.0
 */
public enum BindMethod {

    /**
     * Java Bean binding using getter/setter methods.
     */
    JAVA_BEAN,

    /**
     * Value object binding using constructor parameters.
     */
    VALUE_OBJECT
}

BindException

Exception thrown when binding fails.

package org.springframework.boot.context.properties.bind;

/**
 * Exception thrown when binding fails.
 *
 * @since 2.0.0
 */
public class BindException extends RuntimeException implements OriginProvider {

    /**
     * Return the name of the configuration property being bound.
     * @return the property name
     */
    public ConfigurationPropertyName getName();

    /**
     * Return the target being bound.
     * @return the bind target
     */
    public Bindable<?> getTarget();

    /**
     * Return the configuration property being bound.
     * @return the configuration property (may be {@code null})
     */
    @Nullable
    public ConfigurationProperty getProperty();

    /**
     * Return the origin of the property or {@code null} if the origin is not known.
     * @return the property origin
     */
    @Nullable
    public Origin getOrigin();
}

AbstractBindHandler

Abstract base class for BindHandler implementations.

package org.springframework.boot.context.properties.bind;

/**
 * Abstract base class for BindHandler implementations.
 *
 * @since 2.0.0
 */
public abstract class AbstractBindHandler implements BindHandler {

    /**
     * Create a new binding handler instance.
     */
    public AbstractBindHandler();

    /**
     * Create a new binding handler instance with a specific parent.
     *
     * @param parent the parent handler
     */
    public AbstractBindHandler(BindHandler parent);

    @Override
    public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
                                     BindContext context);

    @Override
    public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                           BindContext context, Object result);

    @Override
    public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                           BindContext context, Exception error) throws Exception;

    @Override
    public void onFinish(ConfigurationPropertyName name, Bindable<?> target,
                        BindContext context, Object result) throws Exception;
}

DataObjectPropertyName

Internal utility to help when dealing with data object property names.

package org.springframework.boot.context.properties.bind;

/**
 * Internal utility to help when dealing with data object property names.
 *
 * @since 2.2.3
 */
public abstract class DataObjectPropertyName {

    /**
     * Return the specified Java Bean property name in dashed form.
     *
     * @param name the source name
     * @return the dashed form
     */
    public static String toDashedForm(String name);
}

Usage Example:

// Convert camelCase to dashed-form
String dashed = DataObjectPropertyName.toDashedForm("maxConnections");
// Returns: "max-connections"

String dashed2 = DataObjectPropertyName.toDashedForm("databaseURL");
// Returns: "database-u-r-l"

Binding Annotations

@DefaultValue

Annotation to specify default values when binding to immutable properties.

package org.springframework.boot.context.properties.bind;

/**
 * Annotation that can be used to specify the default value when binding to an
 * immutable property. This annotation can also be used with nested properties to
 * indicate that a value should always be bound (rather than binding null).
 * The value from this annotation will only be used if the property is not found
 * in the property sources used by the Binder.
 *
 * NOTE: This annotation does not support property placeholder resolution and the
 * value must be constant.
 *
 * @since 2.2.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER })
@Documented
public @interface DefaultValue {

    /**
     * The default value of the property. Can be an array of values for
     * collection or array-based properties.
     *
     * @return the default value of the property
     */
    String[] value() default {};
}

Usage Example:

@ConfigurationProperties("app.service")
public class ServiceProperties {

    private final String name;
    private final int timeout;
    private final List<String> endpoints;

    @ConstructorBinding
    public ServiceProperties(
            String name,
            @DefaultValue("30") int timeout,
            @DefaultValue({"http://default1.example.com", "http://default2.example.com"})
            List<String> endpoints) {
        this.name = name;
        this.timeout = timeout;
        this.endpoints = endpoints;
    }

    // Getters
}

@Name

Annotation to specify the name when binding to a property.

package org.springframework.boot.context.properties.bind;

/**
 * Annotation that can be used to specify the name when binding to a property.
 * This annotation may be required when binding to names that clash with reserved
 * language keywords.
 *
 * When naming a JavaBean-based property, annotate the field. When naming a
 * constructor-bound property, annotate the constructor parameter or record component.
 *
 * @since 2.4.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Documented
public @interface Name {

    /**
     * The name of the property to use for binding.
     *
     * @return the property name
     */
    String value();
}

Usage Example:

@ConfigurationProperties("app.config")
public class AppConfig {

    // Use @Name to avoid clash with Java keyword
    @Name("class")
    private String className;

    public String getClassName() { return className; }
    public void setClassName(String className) { this.className = className; }
}

// Constructor binding example
@ConfigurationProperties("app.server")
public record ServerConfig(
    @Name("class") String serverClass,
    int port
) {}

@Nested

Meta-annotation indicating a field is a nested type.

package org.springframework.boot.context.properties.bind;

/**
 * Meta-annotation that should be added to annotations that indicate a field is
 * a nested type. Used to ensure that correct reflection hints are registered.
 *
 * @since 3.0.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@Documented
public @interface Nested {
}

Validation Support

ValidationBindHandler

BindHandler to apply validators to bound results.

package org.springframework.boot.context.properties.bind.validation;

/**
 * BindHandler to apply Validators to bound results.
 *
 * @since 2.0.0
 */
public class ValidationBindHandler extends AbstractBindHandler {

    /**
     * Create a new ValidationBindHandler instance.
     *
     * @param validators the validators to apply
     */
    public ValidationBindHandler(Validator... validators);

    /**
     * Create a new ValidationBindHandler instance.
     *
     * @param parent the parent handler
     * @param validators the validators to apply
     */
    public ValidationBindHandler(BindHandler parent, Validator... validators);
}

BindValidationException

Error thrown when validation fails during a bind operation.

package org.springframework.boot.context.properties.bind.validation;

/**
 * Error thrown when validation fails during a bind operation.
 *
 * @since 2.0.0
 */
public class BindValidationException extends RuntimeException {

    /**
     * Return the validation errors that caused the exception.
     *
     * @return the validation errors
     */
    public ValidationErrors getValidationErrors();
}

ValidationErrors

A collection of ObjectErrors caused by bind validation failures.

package org.springframework.boot.context.properties.bind.validation;

/**
 * A collection of ObjectErrors caused by bind validation failures.
 * Where possible, included FieldErrors will be OriginProvider.
 *
 * @since 2.0.0
 */
public class ValidationErrors implements Iterable<ObjectError> {

    /**
     * Return the name of the item that was being validated.
     *
     * @return the name of the item
     */
    public ConfigurationPropertyName getName();

    /**
     * Return the properties that were bound before validation failed.
     *
     * @return the bound properties
     */
    public Set<ConfigurationProperty> getBoundProperties();

    /**
     * Return true if there are validation errors.
     *
     * @return true if there are errors
     */
    public boolean hasErrors();

    /**
     * Return the list of all validation errors.
     *
     * @return the errors
     */
    public List<ObjectError> getAllErrors();

    @Override
    public Iterator<ObjectError> iterator();
}

Usage Example:

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import jakarta.validation.Validator;
import jakarta.validation.Validation;

@ConfigurationProperties("app.database")
@Validated
public class DatabaseProperties {

    @NotNull
    private String url;

    @Min(1)
    private int maxConnections = 10;

    // Getters and setters
}

// Using ValidationBindHandler programmatically
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
BindHandler handler = new ValidationBindHandler(validator);

try {
    BindResult<DatabaseProperties> result = binder.bind("app.database",
        Bindable.of(DatabaseProperties.class), handler);
} catch (BindValidationException ex) {
    ValidationErrors errors = ex.getValidationErrors();
    System.out.println("Validation failed for: " + errors.getName());
    errors.getAllErrors().forEach(error ->
        System.out.println("  - " + error.getDefaultMessage())
    );
}

BindHandler Implementations

Spring Boot provides several built-in BindHandler implementations for common binding scenarios.

IgnoreErrorsBindHandler

package org.springframework.boot.context.properties.bind.handler;

/**
 * BindHandler that can be used to ignore binding errors.
 *
 * @since 2.0.0
 */
public class IgnoreErrorsBindHandler extends AbstractBindHandler {
    public IgnoreErrorsBindHandler();
    public IgnoreErrorsBindHandler(BindHandler parent);
}

Usage Example:

Binder binder = Binder.get(environment);
BindHandler handler = new IgnoreErrorsBindHandler();

// Binding will not fail even if there are type conversion errors
BindResult<MyConfig> result = binder.bind("app.config",
    Bindable.of(MyConfig.class), handler);

IgnoreTopLevelConverterNotFoundBindHandler

package org.springframework.boot.context.properties.bind.handler;

/**
 * BindHandler that can be used to ignore ConversionFailedException for the
 * top level, but not for nested elements.
 *
 * @since 2.0.0
 */
public class IgnoreTopLevelConverterNotFoundBindHandler extends AbstractBindHandler {
    public IgnoreTopLevelConverterNotFoundBindHandler();
    public IgnoreTopLevelConverterNotFoundBindHandler(BindHandler parent);
}

NoUnboundElementsBindHandler

package org.springframework.boot.context.properties.bind.handler;

/**
 * BindHandler that can be used to enforce that all configuration properties
 * under the root name have been consumed.
 *
 * @since 2.0.0
 */
public class NoUnboundElementsBindHandler extends AbstractBindHandler {
    public NoUnboundElementsBindHandler();
    public NoUnboundElementsBindHandler(BindHandler parent);
}

Usage Example:

// Fail if there are properties that weren't bound
BindHandler handler = new NoUnboundElementsBindHandler();
binder.bind("app.database", Bindable.of(DatabaseConfig.class), handler);
// Throws BindException if app.database.unknown-property exists

ValidationBindHandler

package org.springframework.boot.context.properties.bind.validation;

/**
 * BindHandler to apply Validators to bound results.
 *
 * @since 2.0.0
 */
public class ValidationBindHandler extends AbstractBindHandler {
    /**
     * Create a new ValidationBindHandler instance.
     *
     * @param validators the validators to apply
     */
    public ValidationBindHandler(Validator... validators);

    /**
     * Create a new ValidationBindHandler instance.
     *
     * @param parent the parent handler
     * @param validators the validators to apply
     */
    public ValidationBindHandler(BindHandler parent, Validator... validators);
}

Usage Example:

import jakarta.validation.Validator;
import jakarta.validation.Validation;

// Create validator
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

// Use validation handler
BindHandler handler = new ValidationBindHandler(validator);
BindResult<MyConfig> result = binder.bind("app.config",
    Bindable.of(MyConfig.class), handler);
// Throws BindValidationException if validation fails

ConfigurationPropertiesBean

Provides access to @ConfigurationProperties bean details.

package org.springframework.boot.context.properties;

/**
 * Provides access to @ConfigurationProperties bean details, regardless of if the
 * annotation was used directly or on a @Bean factory method. This class can be
 * used to access all configuration properties beans in an ApplicationContext, or
 * individual beans on a case-by-case basis.
 *
 * @since 2.2.0
 */
public final class ConfigurationPropertiesBean {

    /**
     * Return the name of the Spring bean.
     * @return the bean name
     */
    public String getName();

    /**
     * Return the actual Spring bean instance.
     * @return the bean instance
     */
    public Object getInstance();

    /**
     * Return the ConfigurationProperties annotation for the bean. The annotation
     * may be defined on the bean itself or from the factory method that create
     * the bean (usually a @Bean method).
     *
     * @return the configuration properties annotation
     */
    public ConfigurationProperties getAnnotation();

    /**
     * Return a Bindable instance suitable that can be used as a target for the
     * Binder.
     *
     * @return a bind target for use with the Binder
     */
    public Bindable<?> asBindTarget();

    /**
     * Return all @ConfigurationProperties beans contained in the given
     * application context. Both directly annotated beans, as well as beans
     * that have @ConfigurationProperties annotated factory methods are included.
     *
     * @param applicationContext the source application context
     * @return a map of all configuration properties beans keyed by the bean name
     */
    public static Map<String, ConfigurationPropertiesBean> getAll(
        ApplicationContext applicationContext);

    /**
     * Return a ConfigurationPropertiesBean instance for the given bean details
     * or null if the bean is not a @ConfigurationProperties object. Annotations
     * are considered both on the bean itself, as well as any factory method.
     *
     * @param applicationContext the source application context
     * @param bean the bean to consider
     * @param beanName the bean name
     * @return a configuration properties bean or null
     */
    public static ConfigurationPropertiesBean get(ApplicationContext applicationContext,
                                                  Object bean, String beanName);

    /**
     * Create a ConfigurationPropertiesBean for a value object.
     * Note: This method is package-private and intended for internal use.
     *
     * @param beanType the bean type
     * @param beanName the bean name
     * @return a configuration properties bean
     */
    static ConfigurationPropertiesBean forValueObject(Class<?> beanType, String beanName);
}

Usage Example:

@Component
public class ConfigurationPropertiesReporter {

    private final ApplicationContext context;

    public ConfigurationPropertiesReporter(ApplicationContext context) {
        this.context = context;
    }

    public void reportAllProperties() {
        Map<String, ConfigurationPropertiesBean> beans =
            ConfigurationPropertiesBean.getAll(context);

        beans.forEach((name, bean) -> {
            ConfigurationProperties annotation = bean.getAnnotation();
            System.out.println("Bean: " + name);
            System.out.println("  Prefix: " + annotation.prefix());
            System.out.println("  Type: " + bean.getInstance().getClass().getName());
        });
    }
}

ConstructorBound

Helper class for programmatically binding configuration properties that use constructor injection.

package org.springframework.boot.context.properties;

import org.springframework.beans.factory.BeanFactory;

/**
 * Helper class to programmatically bind configuration properties that use constructor
 * injection.
 *
 * @since 3.0.0
 * @see ConstructorBinding
 */
public abstract class ConstructorBound {

    /**
     * Create an immutable @ConfigurationProperties instance for the specified
     * beanName and beanType using the specified BeanFactory.
     *
     * @param beanFactory the bean factory to use
     * @param beanName the name of the bean
     * @param beanType the type of the bean
     * @return an instance from the specified bean
     * @throws ConfigurationPropertiesBindException if binding fails
     */
    public static Object from(BeanFactory beanFactory, String beanName, Class<?> beanType);
}

Usage Example:

Use this helper to programmatically create immutable configuration properties objects:

import org.springframework.boot.context.properties.ConstructorBound;
import org.springframework.beans.factory.BeanFactory;

@Configuration
public class AppConfiguration {

    private final BeanFactory beanFactory;

    public AppConfiguration(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Bean
    public MyImmutableProperties myProperties() {
        // Create immutable properties instance using constructor binding
        return (MyImmutableProperties) ConstructorBound.from(
            beanFactory,
            "myProperties",
            MyImmutableProperties.class
        );
    }
}

// Immutable properties class with constructor binding
@ConfigurationProperties(prefix = "app")
public record MyImmutableProperties(
    String name,
    int port,
    Duration timeout
) {
    // Constructor binding happens automatically with records
}

ConfigurationPropertiesBindHandlerAdvisor

SPI (Service Provider Interface) for customizing the BindHandler used during @ConfigurationProperties binding. Implementations can add additional validation, transformation, or logging to the binding process.

Interface Definition

package org.springframework.boot.context.properties;

import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;

/**
 * Allows additional functionality to be applied to the BindHandler used by the
 * ConfigurationPropertiesBindingPostProcessor.
 *
 * Implementations are typically registered as beans in the application context and
 * are automatically detected and applied during configuration properties binding.
 *
 * @since 2.1.0
 * @see AbstractBindHandler
 */
@FunctionalInterface
public interface ConfigurationPropertiesBindHandlerAdvisor {

    /**
     * Apply additional functionality to the source bind handler.
     *
     * @param bindHandler the source bind handler
     * @return a replacement bind handler that delegates to the source and provides
     *         additional functionality
     */
    BindHandler apply(BindHandler bindHandler);
}

Key Features

  • SPI Mechanism: Automatically discovered when registered as beans in the application context
  • Chain of Responsibility: Multiple advisors are applied in sequence, creating a chain of handlers
  • Extensible Binding: Add custom validation, transformation, or logging without modifying core binding logic
  • Functional Interface: Can be implemented as lambda for simple use cases
  • Spring Boot Integration: Automatically integrated into ConfigurationPropertiesBindingPostProcessor

Basic Implementation Example

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.stereotype.Component;

/**
 * Custom bind handler advisor that logs all configuration properties binding.
 */
@Component
public class LoggingBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor {

    @Override
    public BindHandler apply(BindHandler bindHandler) {
        return new LoggingBindHandler(bindHandler);
    }

    /**
     * BindHandler that logs binding operations.
     */
    private static class LoggingBindHandler extends AbstractBindHandler {

        LoggingBindHandler(BindHandler parent) {
            super(parent);
        }

        @Override
        public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
                                       BindContext context) {
            System.out.println("Binding property: " + name);
            return super.onStart(name, target, context);
        }

        @Override
        public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Object result) {
            System.out.println("Successfully bound property: " + name + " = " + result);
            return super.onSuccess(name, target, context, result);
        }

        @Override
        public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Exception error) throws Exception {
            System.err.println("Failed to bind property: " + name + " - " + error.getMessage());
            return super.onFailure(name, target, context, error);
        }
    }
}

Advanced Example: Custom Validation

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * Advisor that enforces custom validation rules during binding.
 */
@Component
@Order(1) // Controls order when multiple advisors are present
public class ValidationBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor {

    @Override
    public BindHandler apply(BindHandler bindHandler) {
        return new ValidationBindHandler(bindHandler);
    }

    private static class ValidationBindHandler extends AbstractBindHandler {

        ValidationBindHandler(BindHandler parent) {
            super(parent);
        }

        @Override
        public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Object result) throws Exception {
            // Custom validation after successful binding
            validateResult(name, result);
            return super.onSuccess(name, target, context, result);
        }

        private void validateResult(ConfigurationPropertyName name, Object result) {
            // Example: Validate port numbers
            if (name.toString().endsWith(".port") && result instanceof Integer port) {
                if (port < 1024 || port > 65535) {
                    throw new IllegalArgumentException(
                        "Port " + name + " must be between 1024 and 65535, got: " + port);
                }
            }

            // Example: Validate URLs
            if (name.toString().endsWith(".url") && result instanceof String url) {
                if (!url.startsWith("http://") && !url.startsWith("https://")) {
                    throw new IllegalArgumentException(
                        "URL " + name + " must start with http:// or https://, got: " + url);
                }
            }

            // Example: Validate sensitive properties are not empty
            if (name.toString().contains("password") && result instanceof String pwd) {
                if (pwd.isEmpty()) {
                    throw new IllegalArgumentException(
                        "Password property " + name + " cannot be empty");
                }
            }
        }
    }
}

Example: Property Transformation

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.stereotype.Component;

/**
 * Advisor that transforms property values during binding.
 */
@Component
public class TransformationBindHandlerAdvisor
        implements ConfigurationPropertiesBindHandlerAdvisor {

    @Override
    public BindHandler apply(BindHandler bindHandler) {
        return new TransformationBindHandler(bindHandler);
    }

    private static class TransformationBindHandler extends AbstractBindHandler {

        TransformationBindHandler(BindHandler parent) {
            super(parent);
        }

        @Override
        public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Object result) throws Exception {
            // Transform the result before passing to parent
            Object transformed = transformValue(name, result);
            return super.onSuccess(name, target, context, transformed);
        }

        private Object transformValue(ConfigurationPropertyName name, Object value) {
            // Example: Normalize all string values to lowercase for specific properties
            if (name.toString().startsWith("app.environment") && value instanceof String) {
                return ((String) value).toLowerCase();
            }

            // Example: Trim whitespace from all string values
            if (value instanceof String) {
                return ((String) value).trim();
            }

            // Example: Convert negative durations to positive
            if (value instanceof java.time.Duration duration) {
                return duration.isNegative() ? duration.abs() : duration;
            }

            return value;
        }
    }
}

Example: Audit Logging

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

/**
 * Advisor that maintains an audit trail of all bound properties.
 */
@Component
public class AuditBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor {

    private final List<AuditEntry> auditLog = new ArrayList<>();

    @Override
    public BindHandler apply(BindHandler bindHandler) {
        return new AuditBindHandler(bindHandler, auditLog);
    }

    public List<AuditEntry> getAuditLog() {
        return new ArrayList<>(auditLog);
    }

    private static class AuditBindHandler extends AbstractBindHandler {

        private final List<AuditEntry> auditLog;

        AuditBindHandler(BindHandler parent, List<AuditEntry> auditLog) {
            super(parent);
            this.auditLog = auditLog;
        }

        @Override
        public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Object result) throws Exception {
            // Record successful binding
            auditLog.add(new AuditEntry(
                name.toString(),
                result != null ? result.getClass().getSimpleName() : "null",
                maskSensitiveValue(name, result),
                Instant.now(),
                true,
                null
            ));
            return super.onSuccess(name, target, context, result);
        }

        @Override
        public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                                BindContext context, Exception error) throws Exception {
            // Record failed binding
            auditLog.add(new AuditEntry(
                name.toString(),
                target.getType().toString(),
                null,
                Instant.now(),
                false,
                error.getMessage()
            ));
            return super.onFailure(name, target, context, error);
        }

        private String maskSensitiveValue(ConfigurationPropertyName name, Object value) {
            String nameStr = name.toString();
            // Mask sensitive properties
            if (nameStr.contains("password") || nameStr.contains("secret") ||
                nameStr.contains("token") || nameStr.contains("key")) {
                return "***MASKED***";
            }
            return value != null ? value.toString() : "null";
        }
    }

    public record AuditEntry(
        String propertyName,
        String type,
        String value,
        Instant timestamp,
        boolean success,
        String errorMessage
    ) {}
}

Multiple Advisors with Ordering

When registering multiple advisors, use @Order to control their execution sequence:

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1) // Runs first
public class ValidationBindHandlerAdvisor
        implements ConfigurationPropertiesBindHandlerAdvisor {
    // Validation logic
}

@Component
@Order(2) // Runs second
public class TransformationBindHandlerAdvisor
        implements ConfigurationPropertiesBindHandlerAdvisor {
    // Transformation logic
}

@Component
@Order(3) // Runs third
public class LoggingBindHandlerAdvisor
        implements ConfigurationPropertiesBindHandlerAdvisor {
    // Logging logic
}

The advisors are applied in order, creating a chain:

Original Handler -> Validation -> Transformation -> Logging -> Actual Binding

Lambda-Based Advisor

For simple use cases, you can define advisors as lambda expressions:

import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BindHandlerConfiguration {

    @Bean
    public ConfigurationPropertiesBindHandlerAdvisor simpleLoggingAdvisor() {
        return bindHandler -> new AbstractBindHandler(bindHandler) {
            @Override
            public <T> Bindable<T> onStart(ConfigurationPropertyName name,
                                           Bindable<T> target,
                                           BindContext context) {
                System.out.println("Binding: " + name);
                return super.onStart(name, target, context);
            }
        };
    }
}

Integration with AbstractBindHandler

AbstractBindHandler provides a convenient base class for custom bind handlers:

import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;

public class CustomBindHandler extends AbstractBindHandler {

    public CustomBindHandler(BindHandler parent) {
        super(parent);
    }

    // Override any of these lifecycle methods:

    @Override
    public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
                                   BindContext context) {
        // Called when binding starts for a property
        return super.onStart(name, target, context);
    }

    @Override
    public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                            BindContext context, Object result) throws Exception {
        // Called when binding succeeds
        return super.onSuccess(name, target, context, result);
    }

    @Override
    public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                            BindContext context, Exception error) throws Exception {
        // Called when binding fails
        return super.onFailure(name, target, context, error);
    }

    @Override
    public void onFinish(ConfigurationPropertyName name, Bindable<?> target,
                         BindContext context, Object result) throws Exception {
        // Called when binding finishes (success or failure)
        super.onFinish(name, target, context, result);
    }
}

Use Cases

  • Custom Validation: Enforce business rules beyond JSR-303 validation
  • Property Transformation: Normalize, sanitize, or convert property values
  • Audit Logging: Track which properties are bound and their values
  • Security: Mask or validate sensitive properties
  • Debugging: Log binding operations during development
  • Metrics: Collect statistics about property binding
  • Conditional Binding: Apply different rules based on environment or profiles
  • Migration Support: Handle deprecated properties and provide warnings

Best Practices

  1. Delegate to Parent: Always call super methods in AbstractBindHandler to maintain the handler chain
  2. Use Ordering: Apply @Order when using multiple advisors to control execution sequence
  3. Mask Sensitive Data: Never log passwords, tokens, or other sensitive values
  4. Handle Exceptions Gracefully: Catch and wrap exceptions appropriately
  5. Keep Performance in Mind: Advisors run for every property; avoid expensive operations
  6. Test Thoroughly: Test with various property configurations and edge cases
  7. Document Behavior: Clearly document what transformations or validations your advisor applies

ConfigurationPropertiesBindingPostProcessor

BeanPostProcessor that binds @ConfigurationProperties annotated beans to external configuration from property sources.

package org.springframework.boot.context.properties;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.PriorityOrdered;

/**
 * BeanPostProcessor to bind PropertySources to beans annotated with
 * @ConfigurationProperties.
 *
 * @since 1.0.0
 */
public class ConfigurationPropertiesBindingPostProcessor
    implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {

    /**
     * The bean name that this post-processor is registered with.
     */
    public static final String BEAN_NAME =
        "org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor";

    /**
     * Register a ConfigurationPropertiesBindingPostProcessor bean if one is not
     * already registered.
     *
     * @param registry the bean definition registry
     * @since 2.2.0
     */
    public static void register(BeanDefinitionRegistry registry);
}

Usage Example:

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomRegistrarConfiguration {

    // Manually register the post-processor if needed
    public void registerPostProcessor(BeanDefinitionRegistry registry) {
        ConfigurationPropertiesBindingPostProcessor.register(registry);
    }
}

ConfigurationPropertiesBinder

Internal class for binding @ConfigurationProperties beans.

package org.springframework.boot.context.properties;

/**
 * Internal class used by the ConfigurationPropertiesBindingPostProcessor to
 * handle the actual @ConfigurationProperties binding.
 *
 * @since 2.2.0
 */
class ConfigurationPropertiesBinder {

    /**
     * Bind the given configuration properties bean.
     *
     * @param propertiesBean the properties bean to bind
     * @return the binding result
     */
    BindResult<?> bind(ConfigurationPropertiesBean propertiesBean);

    /**
     * Bind the given configuration properties bean or create a new instance if
     * the result is null.
     *
     * @param propertiesBean the properties bean to bind
     * @return the bound or created object
     */
    Object bindOrCreate(ConfigurationPropertiesBean propertiesBean);

    /**
     * Get the ConfigurationPropertiesBinder from the given BeanFactory.
     *
     * @param beanFactory the bean factory
     * @return the ConfigurationPropertiesBinder
     */
    static ConfigurationPropertiesBinder get(BeanFactory beanFactory);
}

BoundConfigurationProperties

Bean to record and provide bound @ConfigurationProperties.

package org.springframework.boot.context.properties;

/**
 * Bean to record and provide bound @ConfigurationProperties.
 *
 * @since 2.3.0
 */
public class BoundConfigurationProperties {

    /**
     * Get the configuration property bound to the given name.
     *
     * @param name the property name
     * @return the bound property or null
     */
    public ConfigurationProperty get(ConfigurationPropertyName name);

    /**
     * Get all bound properties.
     *
     * @return a map of all bound properties
     */
    public Map<ConfigurationPropertyName, ConfigurationProperty> getAll();

    /**
     * Return the BoundConfigurationProperties from the given ApplicationContext
     * if it is available.
     *
     * @param context the context to search
     * @return a BoundConfigurationProperties or null
     */
    public static BoundConfigurationProperties get(ApplicationContext context);
}

Usage Example:

@Component
public class BoundPropertiesReporter {

    private final ApplicationContext context;

    public BoundPropertiesReporter(ApplicationContext context) {
        this.context = context;
    }

    public void reportBoundProperties() {
        BoundConfigurationProperties bound = BoundConfigurationProperties.get(context);
        if (bound != null) {
            Map<ConfigurationPropertyName, ConfigurationProperty> all = bound.getAll();
            all.forEach((name, property) -> {
                System.out.println(name + " = " + property.getValue());
            });
        }
    }
}

PropertyMapper

Utility for mapping values from a source to a destination.

package org.springframework.boot.context.properties;

/**
 * Utility that can be used to map values from a supplied source to a destination.
 * Primarily intended to be help when mapping from @ConfigurationProperties to
 * third-party classes.
 *
 * Can filter values based on predicates and adapt values if needed. For example:
 * PropertyMapper map = PropertyMapper.get();
 * map.from(source::getName)
 *   .to(destination::setName);
 * map.from(source::getTimeout)
 *   .when(this::thisYear)
 *   .asInt(Duration::getSeconds)
 *   .to(destination::setTimeoutSecs);
 * map.from(source::isEnabled)
 *   .whenFalse()
 *   .toCall(destination::disable);
 *
 * By default null values and any NullPointerException thrown from the supplier
 * are filtered and will not be applied to consumers. If you want to apply nulls,
 * you can use Source.always().
 *
 * @since 2.0.0
 */
public final class PropertyMapper {

    /**
     * Return a new PropertyMapper instance that applies the given SourceOperator
     * to every source.
     *
     * @param operator the source operator to apply
     * @return a new property mapper instance
     */
    public PropertyMapper alwaysApplying(SourceOperator operator);

    /**
     * Return a new Source from the specified value that can be used to perform
     * the mapping.
     *
     * @param <T> the source type
     * @param value the value
     * @return a Source that can be used to complete the mapping
     */
    public <T> Source<T> from(T value);

    /**
     * Return a new Source from the specified value supplier that can be used to
     * perform the mapping.
     *
     * @param <T> the source type
     * @param supplier the value supplier
     * @return a Source that can be used to complete the mapping
     */
    public <T> Source<T> from(Supplier<? extends T> supplier);

    /**
     * Return the property mapper.
     * @return the property mapper
     */
    public static PropertyMapper get();

    /**
     * An operation that can be applied to a Source.
     */
    @FunctionalInterface
    public interface SourceOperator {
        /**
         * Apply the operation to the given source.
         *
         * @param <T> the source type
         * @param source the source to operate on
         * @return the updated source
         */
        <T> Source<T> apply(Source<T> source);
    }

    /**
     * A source that is in the process of being mapped.
     *
     * @param <T> the source type
     */
    public static final class Source<T> {

        /**
         * Return a source that will use the given supplier to obtain a fallback
         * value to use in place of null.
         *
         * @param fallback the fallback supplier
         * @return a new Source instance
         * @since 4.0.0
         */
        public Source<T> orFrom(Supplier<? extends T> fallback);

        /**
         * Return an adapted version of the source with Integer type.
         *
         * @param <R> the resulting type
         * @param adapter an adapter to convert the current value to a number
         * @return a new adapted source instance
         */
        public <R extends Number> Source<Integer> asInt(Adapter<? super T, ? extends R> adapter);

        /**
         * Return an adapted version of the source changed through the given
         * adapter function.
         *
         * @param <R> the resulting type
         * @param adapter the adapter to apply
         * @return a new adapted source instance
         */
        public <R> Source<R> as(Adapter<? super T, ? extends R> adapter);

        /**
         * Return a filtered version of the source that will only map values
         * that are true.
         *
         * @return a new filtered source instance
         */
        public Source<T> whenTrue();

        /**
         * Return a filtered version of the source that will only map values
         * that are false.
         *
         * @return a new filtered source instance
         */
        public Source<T> whenFalse();

        /**
         * Return a filtered version of the source that will only map values
         * that have a toString() containing actual text.
         *
         * @return a new filtered source instance
         */
        public Source<T> whenHasText();

        /**
         * Return a filtered version of the source that will only map values
         * equal to the specified object.
         *
         * @param object the object to match
         * @return a new filtered source instance
         */
        public Source<T> whenEqualTo(Object object);

        /**
         * Return a filtered version of the source that will only map values
         * that are an instance of the given type.
         *
         * @param <R> the target type
         * @param target the target type to match
         * @return a new filtered source instance
         */
        public <R extends T> Source<R> whenInstanceOf(Class<R> target);

        /**
         * Return a filtered version of the source that won't map values that
         * match the given predicate.
         *
         * @param predicate the predicate used to filter values
         * @return a new filtered source instance
         */
        public Source<T> whenNot(Predicate<T> predicate);

        /**
         * Return a filtered version of the source that won't map values that
         * don't match the given predicate.
         *
         * @param predicate the predicate used to filter values
         * @return a new filtered source instance
         */
        public Source<T> when(Predicate<T> predicate);

        /**
         * Complete the mapping by passing any non-filtered value to the
         * specified consumer. The method is designed to be used with mutable objects.
         *
         * @param consumer the consumer that should accept the value if it's
         *                not been filtered
         */
        public void to(Consumer<? super T> consumer);

        /**
         * Complete the mapping for any non-filtered value by applying the given
         * function to an existing instance and returning a new one. For filtered
         * values, the instance parameter is returned unchanged.
         *
         * @param <R> the result type
         * @param instance the current instance
         * @param mapper the mapping function
         * @return a new mapped instance or the original instance
         * @since 3.0.0
         */
        public <R> R to(R instance, BiFunction<R, ? super T, R> mapper);

        /**
         * Complete the mapping by creating a new instance from the non-filtered value.
         *
         * @param <R> the resulting type
         * @param factory the factory used to create the instance
         * @return the instance
         * @throws NoSuchElementException if the value has been filtered
         */
        public <R> R toInstance(Function<? super T, R> factory);

        /**
         * Complete the mapping by calling the specified method when the value
         * has not been filtered.
         *
         * @param runnable the method to call if the value has not been filtered
         */
        public void toCall(Runnable runnable);

        /**
         * Return a version of this source that can be used to always complete
         * mappings, even if values are null.
         *
         * @return a new Always instance
         * @since 4.0.0
         */
        public Always<T> always();

        /**
         * Adapter used to adapt a value and possibly return a null result.
         *
         * @param <T> the source type
         * @param <R> the result type
         * @since 4.0.0
         */
        @FunctionalInterface
        public interface Adapter<T, R> {
            /**
             * Adapt the given value.
             * @param value the value to adapt
             * @return an adapted value or null
             */
            R adapt(T value);
        }

        /**
         * Allow source mapping to complete using methods that accept nulls.
         *
         * @param <T> the source type
         * @since 4.0.0
         */
        public static class Always<T> {

            /**
             * Return an adapted version of the source changed through the given
             * adapter function.
             *
             * @param <R> the resulting type
             * @param adapter the adapter to apply
             * @return a new adapted source instance
             */
            public <R> Always<R> as(Adapter<? super T, ? extends R> adapter);

            /**
             * Complete the mapping by passing any non-filtered value to the
             * specified consumer. The method is designed to be used with mutable objects.
             *
             * @param consumer the consumer that should accept the value if it's
             *                not been filtered
             */
            public void to(Consumer<? super T> consumer);

            /**
             * Complete the mapping for any non-filtered value by applying the given
             * function to an existing instance and returning a new one. For filtered
             * values, the instance parameter is returned unchanged.
             *
             * @param <R> the result type
             * @param instance the current instance
             * @param mapper the mapping function
             * @return a new mapped instance or the original instance
             */
            public <R> R to(R instance, Mapper<R, ? super T> mapper);

            /**
             * Complete the mapping by creating a new instance from the non-filtered value.
             *
             * @param <R> the resulting type
             * @param factory the factory used to create the instance
             * @return the instance
             * @throws NoSuchElementException if the value has been filtered
             */
            public <R> R toInstance(Factory<? super T, ? extends R> factory);

            /**
             * Complete the mapping by calling the specified method when the value
             * has not been filtered.
             *
             * @param runnable the method to call if the value has not been filtered
             */
            public void toCall(Runnable runnable);

            /**
             * Adapter that support nullable values.
             *
             * @param <T> the source type
             * @param <R> the result type
             */
            @FunctionalInterface
            public interface Adapter<T, R> {
                /**
                 * Adapt the given value.
                 * @param value the value to adapt
                 * @return an adapted value or null
                 */
                R adapt(T value);
            }

            /**
             * Factory that supports nullable values.
             *
             * @param <T> the source type
             * @param <R> the result type
             */
            @FunctionalInterface
            public interface Factory<T, R> {
                /**
                 * Create a new instance for the given nullable value.
                 * @param value the value used to create the instance (may be null)
                 * @return the resulting instance
                 */
                R create(T value);
            }

            /**
             * Mapper that supports nullable values.
             *
             * @param <R> the result type
             * @param <T> the source type
             */
            @FunctionalInterface
            public interface Mapper<R, T> {
                /**
                 * Map a existing instance for the given nullable value.
                 * @param instance the existing instance
                 * @param value the value to map (may be null)
                 * @return the resulting mapped instance
                 */
                R map(R instance, T value);
            }
        }
    }
}

Usage Example:

@Component
public class ServerConfigurer {

    private final ServerProperties serverProperties;
    private final Server server;

    public ServerConfigurer(ServerProperties serverProperties, Server server) {
        this.serverProperties = serverProperties;
        this.server = server;
    }

    public void configure() {
        PropertyMapper map = PropertyMapper.get();

        // Simple mapping
        map.from(serverProperties::getPort)
           .to(server::setPort);

        // Conditional mapping
        map.from(serverProperties::getAddress)
           .whenHasText()
           .to(server::setAddress);

        // Type conversion mapping
        map.from(serverProperties::getTimeout)
           .asInt(Duration::getSeconds)
           .to(server::setTimeoutSeconds);

        // Boolean conditional mapping
        map.from(serverProperties::isCompressionEnabled)
           .whenTrue()
           .toCall(server::enableCompression);

        // Custom adapter
        map.from(serverProperties::getMaxConnections)
           .when(n -> n > 0)
           .as(n -> new ConnectionPool(n))
           .to(server::setConnectionPool);

        // With fallback
        map.from(serverProperties::getHost)
           .orFrom(() -> "localhost")
           .to(server::setHost);
    }
}

@ConfigurationProperties("app.server")
public class ServerProperties {
    private int port = 8080;
    private String address;
    private Duration timeout = Duration.ofSeconds(30);
    private boolean compressionEnabled;
    private int maxConnections = 100;
    private String host;

    // Getters and setters
}

ConfigurationPropertySources

Utility class for working with ConfigurationPropertySource instances.

package org.springframework.boot.context.properties.source;

/**
 * Provides access to ConfigurationPropertySources.
 *
 * @since 2.0.0
 */
public final class ConfigurationPropertySources {

    /**
     * Attach a ConfigurationPropertySource support to the specified Environment.
     * Adapts each PropertySource managed by the environment to a
     * ConfigurationPropertySource and allows classic PropertySourcesPropertyResolver
     * calls to resolve using configuration property names.
     *
     * The attached resolver will dynamically track any additions or removals from
     * the underlying Environment property sources.
     *
     * @param environment the source environment (must be an instance of
     *                   ConfigurableEnvironment)
     */
    public static void attach(Environment environment);

    /**
     * Return a set of ConfigurationPropertySource instances that have previously
     * been attached to the Environment.
     *
     * @param environment the source environment (must be an instance of
     *                   ConfigurableEnvironment)
     * @return an iterable set of configuration property sources
     * @throws IllegalStateException if no configuration property sources have been
     *         attached
     */
    public static Iterable<ConfigurationPropertySource> get(Environment environment);

    /**
     * Return Iterable containing a single new ConfigurationPropertySource adapted
     * from the given Spring PropertySource.
     *
     * @param source the Spring property source to adapt
     * @return an Iterable containing a single newly adapted
     *         SpringConfigurationPropertySource
     */
    public static Iterable<ConfigurationPropertySource> from(PropertySource<?> source);

    /**
     * Return Iterable containing new ConfigurationPropertySource instances adapted
     * from the given Spring PropertySources.
     *
     * @param sources the Spring property sources to adapt
     * @return an Iterable of newly adapted SpringConfigurationPropertySource
     *         instances
     */
    public static Iterable<ConfigurationPropertySource> from(Iterable<PropertySource<?>> sources);

    /**
     * Create a new PropertyResolver that resolves property values against an
     * underlying set of PropertySources.
     *
     * @param propertySources the set of PropertySource objects to use
     * @return a ConfigurablePropertyResolver implementation
     * @since 2.5.0
     */
    public static ConfigurablePropertyResolver createPropertyResolver(
            MutablePropertySources propertySources);

    /**
     * Determines if the specific PropertySource is the ConfigurationPropertySource
     * that was attached to the Environment.
     *
     * @param propertySource the property source to test
     * @return true if this is the attached ConfigurationPropertySource
     */
    public static boolean isAttachedConfigurationPropertySource(PropertySource<?> propertySource);
}

Usage Example:

// Attach configuration property sources to environment
ConfigurableEnvironment environment = new StandardEnvironment();
ConfigurationPropertySources.attach(environment);

// Get attached configuration property sources
Iterable<ConfigurationPropertySource> sources =
    ConfigurationPropertySources.get(environment);

// Create a binder using the sources
Binder binder = new Binder(sources);

// Or use the simplified approach
Binder binder2 = Binder.get(environment);

Complete Working Example

Here's a comprehensive example that demonstrates the various features:

// 1. JavaBean-style Configuration Properties
@ConfigurationProperties(prefix = "app.database")
@Validated
public class DatabaseProperties {

    @NotNull
    private String url;

    @NotNull
    private String username;

    private String password;

    @Min(1)
    @Max(100)
    private int maxConnections = 10;

    @NestedConfigurationProperty
    private PoolSettings pool = new PoolSettings();

    // Getters and setters
    public String getUrl() { return url; }
    public void setUrl(String url) { this.url = url; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public int getMaxConnections() { return maxConnections; }
    public void setMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
    }

    public PoolSettings getPool() { return pool; }
    public void setPool(PoolSettings pool) { this.pool = pool; }

    public static class PoolSettings {
        private int minIdle = 5;
        private int maxIdle = 10;
        private Duration maxWait = Duration.ofSeconds(5);

        // Getters and setters
        public int getMinIdle() { return minIdle; }
        public void setMinIdle(int minIdle) { this.minIdle = minIdle; }

        public int getMaxIdle() { return maxIdle; }
        public void setMaxIdle(int maxIdle) { this.maxIdle = maxIdle; }

        public Duration getMaxWait() { return maxWait; }
        public void setMaxWait(Duration maxWait) { this.maxWait = maxWait; }
    }
}

// 2. Constructor Binding with Record
@ConfigurationProperties(prefix = "app.service")
public record ServiceProperties(
    String name,
    @DefaultValue("30") int timeout,
    boolean enabled,
    List<String> endpoints
) {}

// 3. Constructor Binding with Class
@ConfigurationProperties(prefix = "app.cache")
public class CacheProperties {

    private final String type;
    private final Duration ttl;
    private final Map<String, String> config;

    @ConstructorBinding
    public CacheProperties(
            String type,
            @DefaultValue("1h") Duration ttl,
            Map<String, String> config) {
        this.type = type;
        this.ttl = ttl;
        this.config = config;
    }

    public String getType() { return type; }
    public Duration getTtl() { return ttl; }
    public Map<String, String> getConfig() { return config; }
}

// 4. Configuration with Deprecated Properties
@ConfigurationProperties(prefix = "app.legacy")
public class LegacyProperties {

    private String oldEndpoint;
    private String newEndpoint;

    @DeprecatedConfigurationProperty(
        replacement = "app.legacy.new-endpoint",
        reason = "Use new-endpoint for improved security",
        since = "2.0.0"
    )
    public String getOldEndpoint() { return oldEndpoint; }
    public void setOldEndpoint(String oldEndpoint) { this.oldEndpoint = oldEndpoint; }

    public String getNewEndpoint() { return newEndpoint; }
    public void setNewEndpoint(String newEndpoint) { this.newEndpoint = newEndpoint; }
}

// 5. Enable Configuration Properties
@Configuration
@EnableConfigurationProperties({
    DatabaseProperties.class,
    ServiceProperties.class,
    CacheProperties.class,
    LegacyProperties.class
})
public class AppConfiguration {

    // Use DatabaseProperties in a bean
    @Bean
    public DataSource dataSource(DatabaseProperties properties) {
        HikariConfig config = new HikariConfig();

        // Use PropertyMapper for clean mapping
        PropertyMapper map = PropertyMapper.get();
        map.from(properties::getUrl).to(config::setJdbcUrl);
        map.from(properties::getUsername).to(config::setUsername);
        map.from(properties::getPassword).whenHasText().to(config::setPassword);
        map.from(properties::getMaxConnections).to(config::setMaximumPoolSize);

        DatabaseProperties.PoolSettings pool = properties.getPool();
        map.from(pool::getMinIdle).to(config::setMinimumIdle);
        map.from(pool::getMaxIdle).when(n -> n > 0).to(config::setMaximumPoolSize);
        map.from(pool::getMaxWait).asInt(Duration::toMillis).to(config::setConnectionTimeout);

        return new HikariDataSource(config);
    }

    // Use Binder directly
    @Bean
    public CustomService customService(Environment environment) {
        Binder binder = Binder.get(environment);

        // Bind simple properties
        String appName = binder.bind("app.name", String.class)
            .orElse("default-app");

        // Bind collections
        List<String> servers = binder.bind("app.servers",
                Bindable.listOf(String.class))
            .orElseGet(ArrayList::new);

        // Bind maps
        Map<String, Integer> limits = binder.bind("app.limits",
                Bindable.mapOf(String.class, Integer.class))
            .orElseGet(HashMap::new);

        return new CustomService(appName, servers, limits);
    }
}

// 6. Programmatic Binding Example
@Component
public class DynamicConfigLoader {

    private final Environment environment;

    public DynamicConfigLoader(Environment environment) {
        this.environment = environment;
    }

    public <T> T loadConfig(String prefix, Class<T> configClass) {
        Binder binder = Binder.get(environment);

        // Create custom bind handler for logging
        BindHandler handler = new BindHandler() {
            @Override
            public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
                                   BindContext context, Object result) {
                System.out.println("Successfully bound: " + name + " = " + result);
                return result;
            }

            @Override
            public Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
                                   BindContext context, Exception error) {
                System.err.println("Failed to bind: " + name);
                return null;
            }
        };

        return binder.bindOrCreate(prefix, Bindable.of(configClass), handler);
    }

    public void listAllConfigurationProperties() {
        ApplicationContext context = (ApplicationContext) environment;
        Map<String, ConfigurationPropertiesBean> beans =
            ConfigurationPropertiesBean.getAll(context);

        beans.forEach((name, bean) -> {
            System.out.println("Configuration Bean: " + name);
            System.out.println("  Prefix: " + bean.getAnnotation().prefix());
            System.out.println("  Type: " + bean.getInstance().getClass().getSimpleName());
        });
    }
}

Application Properties File:

# Database configuration
app.database.url=jdbc:postgresql://localhost:5432/myapp
app.database.username=dbuser
app.database.password=secret
app.database.max-connections=20
app.database.pool.min-idle=5
app.database.pool.max-idle=15
app.database.pool.max-wait=10s

# Service configuration
app.service.name=my-service
app.service.timeout=60
app.service.enabled=true
app.service.endpoints[0]=http://api1.example.com
app.service.endpoints[1]=http://api2.example.com

# Cache configuration
app.cache.type=redis
app.cache.ttl=2h
app.cache.config.host=localhost
app.cache.config.port=6379

# Legacy configuration
app.legacy.old-endpoint=http://old.api.com
app.legacy.new-endpoint=https://new.api.com

# Additional properties
app.name=My Application
app.servers[0]=server1.example.com
app.servers[1]=server2.example.com
app.limits.requests=1000
app.limits.connections=100

Common Patterns

Pattern 1: Immutable Configuration with Records and Validation

Use Java records with constructor binding for type-safe, immutable configuration.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;
import java.time.Duration;
import java.util.List;

@ConfigurationProperties(prefix = "app.security")
@Validated
public record SecurityProperties(
    @NotBlank(message = "JWT secret is required")
    @Size(min = 32, message = "JWT secret must be at least 32 characters")
    String jwtSecret,

    @NotNull
    @Min(value = 60, message = "Token validity must be at least 60 seconds")
    Duration tokenValidity,

    @NotEmpty(message = "At least one allowed origin is required")
    List<@Pattern(regexp = "https?://.*", message = "Invalid URL format") String> allowedOrigins,

    @Min(0) @Max(10)
    int maxLoginAttempts,

    @NestedConfigurationProperty
    RateLimitConfig rateLimit
) {

    // Nested configuration
    public record RateLimitConfig(
        @Min(1) int requestsPerMinute,
        @NotNull Duration lockoutDuration
    ) {}

    // Computed properties
    public long getTokenValidityMillis() {
        return tokenValidity.toMillis();
    }

    // Validation logic
    public boolean isOriginAllowed(String origin) {
        return allowedOrigins.stream()
            .anyMatch(allowed -> origin.startsWith(allowed));
    }
}

// Enable in configuration
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityConfiguration {

    @Bean
    public JwtTokenProvider jwtTokenProvider(SecurityProperties properties) {
        return new JwtTokenProvider(
            properties.jwtSecret(),
            properties.tokenValidity()
        );
    }
}

application.yml:

app:
  security:
    jwt-secret: ${JWT_SECRET:your-secret-key-must-be-very-long-and-secure}
    token-validity: 24h
    allowed-origins:
      - https://app.example.com
      - https://admin.example.com
    max-login-attempts: 5
    rate-limit:
      requests-per-minute: 100
      lockout-duration: 15m

Use Cases:

  • Security-critical configuration requiring immutability
  • Configuration shared across multiple components
  • Compile-time safety with records

Pattern 2: Dynamic Configuration Reloading with Actuator

Implement configuration hot-reload using Spring Cloud Config or custom refresh logic.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicReference;

@ConfigurationProperties(prefix = "app.feature-flags")
@RefreshScope
public class FeatureFlagProperties {

    private boolean newCheckoutEnabled;
    private boolean experimentalSearchEnabled;
    private int searchResultLimit = 50;
    private Map<String, Boolean> regionalFeatures = new HashMap<>();

    // Getters and setters
    public boolean isNewCheckoutEnabled() { return newCheckoutEnabled; }
    public void setNewCheckoutEnabled(boolean enabled) { this.newCheckoutEnabled = enabled; }

    public boolean isExperimentalSearchEnabled() { return experimentalSearchEnabled; }
    public void setExperimentalSearchEnabled(boolean enabled) {
        this.experimentalSearchEnabled = enabled;
    }

    public int getSearchResultLimit() { return searchResultLimit; }
    public void setSearchResultLimit(int limit) { this.searchResultLimit = limit; }

    public Map<String, Boolean> getRegionalFeatures() { return regionalFeatures; }
    public void setRegionalFeatures(Map<String, Boolean> features) {
        this.regionalFeatures = features;
    }
}

@Component
public class FeatureFlagService {

    private final AtomicReference<FeatureFlagProperties> properties;

    public FeatureFlagService(FeatureFlagProperties properties) {
        this.properties = new AtomicReference<>(properties);
    }

    public boolean isFeatureEnabled(String featureName) {
        FeatureFlagProperties current = properties.get();
        return switch (featureName) {
            case "new-checkout" -> current.isNewCheckoutEnabled();
            case "experimental-search" -> current.isExperimentalSearchEnabled();
            default -> current.getRegionalFeatures()
                             .getOrDefault(featureName, false);
        };
    }

    public int getSearchLimit() {
        return properties.get().getSearchResultLimit();
    }

    // Called by RefreshScope when configuration changes
    @EventListener(RefreshScopeRefreshedEvent.class)
    public void onRefresh(RefreshScopeRefreshedEvent event) {
        log.info("Feature flags refreshed");
        // Update atomic reference if needed
    }
}

// Configuration refresh endpoint
@RestController
@RequestMapping("/admin/config")
public class ConfigRefreshController {

    private final RefreshEndpoint refreshEndpoint;

    public ConfigRefreshController(RefreshEndpoint refreshEndpoint) {
        this.refreshEndpoint = refreshEndpoint;
    }

    @PostMapping("/refresh")
    public Collection<String> refresh() {
        return refreshEndpoint.refresh();
    }
}

application.yml:

app:
  feature-flags:
    new-checkout-enabled: false
    experimental-search-enabled: true
    search-result-limit: 50
    regional-features:
      europe-premium: true
      asia-beta: false

management:
  endpoints:
    web:
      exposure:
        include: refresh

Use Cases:

  • A/B testing with feature flags
  • Configuration changes without restarts
  • Progressive rollouts

Pattern 3: Multi-Environment Configuration with Profiles

Manage environment-specific configurations using Spring profiles.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

// Base properties applicable to all environments
@ConfigurationProperties(prefix = "app")
public class ApplicationProperties {

    private String name;
    private String version;
    private DatabaseConfig database;
    private CacheConfig cache;

    public static class DatabaseConfig {
        private String url;
        private String username;
        private String password;
        private PoolConfig pool;

        public static class PoolConfig {
            private int minSize = 5;
            private int maxSize = 20;
            private Duration maxWait = Duration.ofSeconds(30);

            // Getters and setters
        }
        // Getters and setters
    }

    public static class CacheConfig {
        private String type;
        private Duration ttl;
        private Map<String, String> config = new HashMap<>();

        // Getters and setters
    }
    // Getters and setters
}

// Development-specific overrides
@Configuration
@Profile("development")
@EnableConfigurationProperties(DevelopmentProperties.class)
public class DevelopmentConfiguration {

    @Bean
    public ApplicationProperties developmentProperties(DevelopmentProperties devProps) {
        // Merge with base properties or use dev-specific settings
        return devProps.toApplicationProperties();
    }
}

@ConfigurationProperties(prefix = "app.dev")
class DevelopmentProperties {
    private boolean debugMode = true;
    private boolean hotReload = true;
    private String mockDataSource;

    public ApplicationProperties toApplicationProperties() {
        ApplicationProperties props = new ApplicationProperties();
        // Configure with dev-specific settings
        return props;
    }
    // Getters and setters
}

// Production-specific configuration
@Configuration
@Profile("production")
@EnableConfigurationProperties(ProductionProperties.class)
public class ProductionConfiguration {

    @Bean
    public ApplicationProperties productionProperties(ProductionProperties prodProps) {
        return prodProps.toApplicationProperties();
    }
}

@ConfigurationProperties(prefix = "app.prod")
class ProductionProperties {
    private SecurityConfig security;
    private MonitoringConfig monitoring;

    public static class SecurityConfig {
        private boolean sslEnabled = true;
        private String certificatePath;
        private List<String> allowedOrigins;
        // Getters and setters
    }

    public static class MonitoringConfig {
        private boolean metricsEnabled = true;
        private boolean tracingEnabled = true;
        private String exporterEndpoint;
        // Getters and setters
    }

    public ApplicationProperties toApplicationProperties() {
        // Configure with production-specific settings
        return new ApplicationProperties();
    }
    // Getters and setters
}

application-development.yml:

app:
  name: My App (Dev)
  database:
    url: jdbc:h2:mem:devdb
    username: sa
    password: ""
    pool:
      min-size: 1
      max-size: 5
  cache:
    type: simple
  dev:
    debug-mode: true
    hot-reload: true
    mock-data-source: classpath:mock-data.json

application-production.yml:

app:
  name: My App
  database:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    pool:
      min-size: 10
      max-size: 50
      max-wait: 60s
  cache:
    type: redis
    ttl: 1h
    config:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT}
  prod:
    security:
      ssl-enabled: true
      certificate-path: /etc/ssl/certs/app.crt
      allowed-origins:
        - https://app.example.com
    monitoring:
      metrics-enabled: true
      tracing-enabled: true
      exporter-endpoint: http://prometheus:9090

Use Cases:

  • Environment-specific database connections
  • Dev vs prod security settings
  • Infrastructure-dependent configuration

Pattern 4: Third-Party Library Configuration with PropertyMapper

Clean configuration of third-party libraries using PropertyMapper.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;

@ConfigurationProperties(prefix = "app.datasource")
public class DataSourceProperties {

    private String url;
    private String username;
    private String password;
    private String driverClassName;
    private PoolProperties pool = new PoolProperties();

    public static class PoolProperties {
        private Integer minimumIdle;
        private Integer maximumPoolSize;
        private Long connectionTimeout;
        private Long idleTimeout;
        private Long maxLifetime;
        private String connectionTestQuery;
        private Boolean autoCommit;
        private String poolName;

        // Getters and setters
    }
    // Getters and setters
}

@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceConfiguration {

    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        HikariConfig config = new HikariConfig();

        // Use PropertyMapper for clean, null-safe mapping
        PropertyMapper map = PropertyMapper.get();

        // Basic properties with null-safe mapping
        map.from(properties::getUrl).whenNonNull().to(config::setJdbcUrl);
        map.from(properties::getUsername).whenNonNull().to(config::setUsername);
        map.from(properties::getPassword).whenNonNull().to(config::setPassword);
        map.from(properties::getDriverClassName).whenNonNull().to(config::setDriverClassName);

        // Pool properties with type conversion
        DataSourceProperties.PoolProperties pool = properties.getPool();
        map.from(pool::getMinimumIdle).to(config::setMinimumIdle);
        map.from(pool::getMaximumPoolSize).to(config::setMaximumPoolSize);
        map.from(pool::getConnectionTimeout).to(config::setConnectionTimeout);
        map.from(pool::getIdleTimeout).to(config::setIdleTimeout);
        map.from(pool::getMaxLifetime).to(config::setMaxLifetime);
        map.from(pool::getConnectionTestQuery).to(config::setConnectionTestQuery);
        map.from(pool::getAutoCommit).to(config::setAutoCommit);
        map.from(pool::getPoolName).to(config::setPoolName);

        // Conditional mapping
        map.from(pool::getPoolName)
           .when(name -> name != null && !name.isBlank())
           .to(config::setPoolName);

        return new HikariDataSource(config);
    }
}

// Alternative: Using PropertyMapper for custom transformations
@Configuration
public class RedisConfiguration {

    @Bean
    public RedisClient redisClient(RedisProperties properties) {
        RedisClientConfig config = new RedisClientConfig();

        PropertyMapper map = PropertyMapper.get();

        // With transformation
        map.from(properties::getHost).to(config::setHost);
        map.from(properties::getPort).to(config::setPort);

        // Conditional with transformation
        map.from(properties::getPassword)
           .whenHasText()
           .as(password -> encryptPassword(password))
           .to(config::setPassword);

        // With default value
        map.from(properties::getDatabase)
           .whenNonNull()
           .to(config::setDatabase);

        return new RedisClient(config);
    }

    private String encryptPassword(String password) {
        // Encryption logic
        return password;
    }
}

application.yml:

app:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: dbuser
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
    pool:
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      auto-commit: true
      pool-name: MyAppPool

Use Cases:

  • Configuring connection pools (HikariCP, Tomcat JDBC)
  • Setting up caching libraries (Redis, Caffeine)
  • Configuring HTTP clients (RestTemplate, WebClient)

Pattern 5: Configuration Migration and Deprecation

Handle property migrations gracefully with deprecation warnings.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

@ConfigurationProperties(prefix = "app.api")
public class ApiProperties {

    private String endpoint;
    private Duration timeout = Duration.ofSeconds(30);
    private RetryConfig retry = new RetryConfig();

    // Deprecated property with migration path
    private String oldEndpoint;

    public String getEndpoint() {
        // Prefer new property, fallback to old
        return endpoint != null ? endpoint : oldEndpoint;
    }

    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
    }

    @Deprecated
    @DeprecatedConfigurationProperty(
        replacement = "app.api.endpoint",
        reason = "Renamed for consistency with other endpoint properties"
    )
    public String getOldEndpoint() {
        return oldEndpoint;
    }

    public void setOldEndpoint(String oldEndpoint) {
        this.oldEndpoint = oldEndpoint;
    }

    public Duration getTimeout() { return timeout; }
    public void setTimeout(Duration timeout) { this.timeout = timeout; }

    public RetryConfig getRetry() { return retry; }
    public void setRetry(RetryConfig retry) { this.retry = retry; }

    public static class RetryConfig {
        private int maxAttempts = 3;
        private Duration backoff = Duration.ofMillis(100);

        // Getters and setters
    }
}

// Environment post-processor for automatic migration
public class PropertyMigrationPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
                                      SpringApplication application) {
        Map<String, Object> migrations = new HashMap<>();

        // Check for old properties and migrate
        String oldEndpoint = environment.getProperty("app.api.old-endpoint");
        if (oldEndpoint != null &&
            environment.getProperty("app.api.endpoint") == null) {

            migrations.put("app.api.endpoint", oldEndpoint);
            log.warn("Property 'app.api.old-endpoint' is deprecated. " +
                     "Please use 'app.api.endpoint' instead.");
        }

        // Check for renamed properties
        String legacyTimeout = environment.getProperty("app.api.connection-timeout");
        if (legacyTimeout != null &&
            environment.getProperty("app.api.timeout") == null) {

            migrations.put("app.api.timeout", legacyTimeout);
            log.warn("Property 'app.api.connection-timeout' is deprecated. " +
                     "Please use 'app.api.timeout' instead.");
        }

        if (!migrations.isEmpty()) {
            MapPropertySource migrationSource =
                new MapPropertySource("configurationPropertiesMigration", migrations);
            environment.getPropertySources().addFirst(migrationSource);
        }
    }
}

// Validation that ensures migration
@Component
public class ConfigurationMigrationValidator implements ApplicationRunner {

    private final Environment environment;

    public ConfigurationMigrationValidator(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        List<String> deprecatedProperties = Arrays.asList(
            "app.api.old-endpoint",
            "app.api.connection-timeout"
        );

        List<String> foundDeprecated = new ArrayList<>();
        for (String property : deprecatedProperties) {
            if (environment.containsProperty(property)) {
                foundDeprecated.add(property);
            }
        }

        if (!foundDeprecated.isEmpty()) {
            log.warn("\n" +
                "=================================================================\n" +
                "DEPRECATION WARNING: The following properties are deprecated:\n" +
                String.join("\n", foundDeprecated) + "\n" +
                "Please update your configuration. Support will be removed in v2.0\n" +
                "=================================================================");
        }
    }
}

application.yml (old format):

app:
  api:
    old-endpoint: https://api.example.com  # Deprecated
    connection-timeout: 60s  # Deprecated

application.yml (new format):

app:
  api:
    endpoint: https://api.example.com
    timeout: 60s
    retry:
      max-attempts: 3
      backoff: 100ms

Use Cases:

  • API versioning and property renaming
  • Configuration schema evolution
  • Backward compatibility during migrations

Best Practices

Configuration Design

  1. Use Type-Safe Configuration: Prefer @ConfigurationProperties over @Value for complex configuration
  2. Validate Properties: Use @Validated with JSR-303 annotations (@NotNull, @Min, @Max, etc.)
  3. Use Constructor Binding for Immutability: Prefer records or constructor binding for immutable configuration
  4. Document Deprecated Properties: Always use @DeprecatedConfigurationProperty when deprecating properties
  5. Use PropertyMapper for Third-Party Integration: When configuring third-party libraries, use PropertyMapper for clean, declarative mapping
  6. Organize Related Properties: Group related properties into nested classes marked with @NestedConfigurationProperty
  7. Provide Sensible Defaults: Use default values in your configuration classes
  8. Use Prefixes Consistently: Establish a naming convention for property prefixes (e.g., app.*, company.*)

Security Considerations

  1. Never Commit Secrets: Use environment variables or secret managers for sensitive data
  2. Validate Input: Always validate external configuration to prevent injection attacks
  3. Use Encrypted Properties: Consider Spring Cloud Config with encryption for sensitive data
  4. Restrict Property Access: Limit who can modify production configuration
  5. Audit Configuration Changes: Log all configuration updates in production

Performance Optimization

  1. Lazy Binding: Use @ConfigurationProperties on demand, not eagerly loaded
  2. Cache Binder Instances: Reuse Binder instances for multiple binding operations
  3. Avoid Complex Conversions: Keep type converters simple and fast
  4. Use Records: Records provide better performance than JavaBeans for immutable properties

Testing Configuration

@SpringBootTest(properties = {
    "app.security.jwt-secret=test-secret-key-for-testing-only",
    "app.security.token-validity=1h",
    "app.security.allowed-origins=http://localhost:3000"
})
class SecurityPropertiesTest {

    @Autowired
    private SecurityProperties properties;

    @Test
    void shouldBindProperties() {
        assertThat(properties.jwtSecret()).isEqualTo("test-secret-key-for-testing-only");
        assertThat(properties.tokenValidity()).isEqualTo(Duration.ofHours(1));
        assertThat(properties.allowedOrigins()).contains("http://localhost:3000");
    }

    @Test
    void shouldValidateInvalidProperties() {
        // Test validation logic
    }
}

Common Pitfalls to Avoid

  1. Don't use @Value for complex configuration - Use @ConfigurationProperties instead
  2. Don't ignore validation errors - Always use @Validated for critical configuration
  3. Don't hardcode environment-specific values - Use profiles or environment variables
  4. Don't forget to document properties - Add JavaDoc to configuration classes
  5. Don't mix binding modes - Choose either setter-based or constructor binding consistently
  6. Don't use mutable collections without copying - Defensive copying for public collections
  7. Don't forget IDE metadata - Generate spring-configuration-metadata.json for IDE support
  8. Don't ignore deprecation warnings - Migrate deprecated properties promptly
  9. Don't expose internal structure - Use DTOs if configuration classes are exposed via APIs
  10. Don't skip integration tests - Test configuration binding with real property sources

Related Documentation

  • Spring Boot Reference: Configuration Properties
  • Spring Boot Auto-configuration
  • Spring Boot Property Binding
  • Spring Framework Environment Abstraction