Custom Spring Boot binding for external configuration

Spring Boot’s Binder already resolves hierarchical properties from YAML files, environment variables, command-line arguments and imported configuration sources. Creating a custom Spring Boot binder for external configuration properties is worthwhile when an upstream system sends compact values that need validation before reaching business code.

The example models an Australian merchant service: AU-SYD and AU-MEL represent Sydney, NSW and Melbourne, VIC; prices settle in AUD; and Australia/Sydney accounts for AEST/AEDT daylight saving. It resembles configuration used by sellers operating .com.au stores, where checkout may need standard-rated 10% GST included.

Define the external contract

Begin with a typed configuration contract rather than passing loosely formatted strings through the application. The records separate the external representation from the values used by the merchant service.

record MerchantLocation(String country, String city, String state) {}
record MerchantLocations(Set<MerchantLocation> values) {}

record MerchantProperties(
        MerchantLocations locations,
        String currency,
        boolean gstInclusive,
        String timeZone) {}

The external property can use the compact value AU-SYD,AU-MEL, while application code receives validated location objects. Keeping this boundary explicit also makes future expansion to other Australian cities straightforward.

Convert compact location values

A Converter is the appropriate extension point when an external value needs custom parsing. Spring Boot applies registered converters while binding the property.

public final class MerchantLocationsConverter
        implements Converter<String, MerchantLocations> {

    public MerchantLocations convert(String source) {
        Set<MerchantLocation> values = Arrays.stream(source.split(","))
                .map(String::trim)
                .filter(StringUtils::hasText)
                .map(this::parse)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        if (values.isEmpty()) {
            throw new IllegalArgumentException("No locations supplied");
        }
        return new MerchantLocations(values);
    }

    private MerchantLocation parse(String item) {
        return switch (item.toUpperCase(Locale.ROOT)) {
            case "AU-SYD" -> new MerchantLocation("AU", "Sydney", "NSW");
            case "AU-MEL" -> new MerchantLocation("AU", "Melbourne", "VIC");
            default -> throw new IllegalArgumentException(
                    "Unsupported location: " + item);
        };
    }
}

The converter rejects unsupported or malformed codes before the application starts. Its error identifies the offending value without exposing credentials or other sensitive configuration.

Register conversions and construct the binder

Register the converter with Boot’s shared application conversion service, then obtain the standard Binder. This retains Boot’s built-in handling for enums, durations, data sizes and relaxed property names.

ApplicationConversionService.configure(service ->
        service.addConverter(new MerchantLocationsConverter()));

Binder binder = Binder.get(environment);

Package that behaviour in a small class that controls the prefix and produces useful errors for missing configuration.

public <T> T bind(String prefix, Class<T> type) {
    return binder.bind(prefix, Bindable.of(type))
            .orElseThrow(() -> new IllegalStateException(
                    "Missing configuration prefix: " + prefix));
}

The wrapper adds project-specific conventions without reimplementing property-source resolution, profile handling or relaxed binding.

Load an external property source

Spring Boot can import a file maintained outside the application artefact:

spring:
  config:
    import: "optional:file:/etc/javawhizz/merchant.yml"

The imported file can contain the merchant contract:

merchant:
  locations: AU-SYD,AU-MEL
  currency: AUD
  gst-inclusive: true
  time-zone: Australia/Sydney

Expose the bound object as a bean:

@Bean
MerchantProperties merchantProperties(ExternalConfigurationBinder binder) {
    return binder.bind("merchant", MerchantProperties.class);
}

An environment variable such as MERCHANT_LOCATIONS=AU-SYD,AU-MEL enters the same binding path. Spring Boot’s normal property-source precedence determines which value wins, so operators can replace an imported value without changing application code.

Fail fast on invalid configuration

Conversion failures should stop startup instead of silently falling back to defaults. Boot reports these as binding failures, while the custom binder supplies a clear message when the entire prefix is absent.

Add semantic checks after conversion for rules the type system cannot express, such as requiring AUD for an Australian settlement account or accepting only approved Australian time zones. If the configuration includes payment credentials, report only safe metadata in logs; never print the complete bound object.

Fail-fast behaviour is particularly valuable during deployment because a bad external value is easier to diagnose before an order reaches an Australian checkout flow.

Verify binding and overrides

Test the converter separately for valid and invalid inputs, then verify the complete Spring context. This catches registration mistakes that a unit test of the parser would miss.

@SpringBootTest(properties = {
        "merchant.locations=AU-SYD,AU-MEL",
        "merchant.currency=AUD",
        "merchant.gst-inclusive=true",
        "merchant.time-zone=Australia/Sydney"
})
class MerchantPropertiesTests {

    @Autowired MerchantProperties properties;

    @Test
    void bindsAustralianLocations() {
        assertThat(properties.locations().values())
                .extracting(MerchantLocation::city)
                .containsExactlyInAnyOrder("Sydney", "Melbourne");
    }
}

Also test malformed codes, missing required properties and environment-variable overrides. The finished application receives typed locations and validated merchant settings while Spring Boot retains control over configuration precedence.