Spring Boot
ConfigurationProperties
Java
Application Properties
Microservices

Spring Boot Multiple similar ConfigurationProperties with different Prefixes

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Spring Boot @ConfigurationProperties is ideal when you need type-safe configuration for external systems. A common pattern is having several similar configurations, such as multiple API clients, each with a different prefix. This guide shows clean ways to model that setup without duplicating too much code.

Use Separate Classes Per Prefix for Clarity

The most explicit approach is one configuration class per logical dependency. This is easy to read and works well for small and medium projects.

yaml
1clients:
2  billing:
3    base-url: https://billing.internal
4    connect-timeout-ms: 1000
5    read-timeout-ms: 3000
6  shipping:
7    base-url: https://shipping.internal
8    connect-timeout-ms: 1500
9    read-timeout-ms: 4000
java
1package com.example.config;
2
3import org.springframework.boot.context.properties.ConfigurationProperties;
4
5@ConfigurationProperties(prefix = "clients.billing")
6public class BillingClientProperties {
7    private String baseUrl;
8    private int connectTimeoutMs;
9    private int readTimeoutMs;
10
11    public String getBaseUrl() { return baseUrl; }
12    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
13    public int getConnectTimeoutMs() { return connectTimeoutMs; }
14    public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; }
15    public int getReadTimeoutMs() { return readTimeoutMs; }
16    public void setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; }
17}

Duplicate for shipping with prefix clients.shipping, then enable scanning.

java
1import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
2import org.springframework.boot.autoconfigure.SpringBootApplication;
3
4@SpringBootApplication
5@ConfigurationPropertiesScan
6public class App {
7    public static void main(String[] args) {
8        org.springframework.boot.SpringApplication.run(App.class, args);
9    }
10}

Reuse Shape with Nested Map for Many Similar Entries

If you have many clients, creating one class per client becomes noisy. Model them as a map under one root prefix.

yaml
1clients:
2  configs:
3    billing:
4      base-url: https://billing.internal
5      connect-timeout-ms: 1000
6      read-timeout-ms: 3000
7    shipping:
8      base-url: https://shipping.internal
9      connect-timeout-ms: 1500
10      read-timeout-ms: 4000
11    inventory:
12      base-url: https://inventory.internal
13      connect-timeout-ms: 1200
14      read-timeout-ms: 3500
java
1package com.example.config;
2
3import org.springframework.boot.context.properties.ConfigurationProperties;
4import java.util.HashMap;
5import java.util.Map;
6
7@ConfigurationProperties(prefix = "clients")
8public class ClientsProperties {
9
10    private Map<String, ClientConfig> configs = new HashMap<>();
11
12    public Map<String, ClientConfig> getConfigs() { return configs; }
13    public void setConfigs(Map<String, ClientConfig> configs) { this.configs = configs; }
14
15    public static class ClientConfig {
16        private String baseUrl;
17        private int connectTimeoutMs;
18        private int readTimeoutMs;
19
20        public String getBaseUrl() { return baseUrl; }
21        public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
22        public int getConnectTimeoutMs() { return connectTimeoutMs; }
23        public void setConnectTimeoutMs(int connectTimeoutMs) { this.connectTimeoutMs = connectTimeoutMs; }
24        public int getReadTimeoutMs() { return readTimeoutMs; }
25        public void setReadTimeoutMs(int readTimeoutMs) { this.readTimeoutMs = readTimeoutMs; }
26    }
27}

This pattern scales for many integrations while keeping one validation surface.

Validate Configuration Early

Fail fast when required values are missing by adding bean validation annotations.

java
1import jakarta.validation.constraints.Min;
2import jakarta.validation.constraints.NotBlank;
3import org.springframework.validation.annotation.Validated;
4
5@Validated
6@ConfigurationProperties(prefix = "clients.billing")
7public class BillingClientProperties {
8    @NotBlank
9    private String baseUrl;
10
11    @Min(1)
12    private int connectTimeoutMs;
13
14    @Min(1)
15    private int readTimeoutMs;
16
17    // getters and setters omitted for brevity
18}

When values are invalid, application startup fails with a clear message instead of runtime network failures.

Wiring Client Beans from Properties

Turn property objects into concrete HTTP clients in a config class.

java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.web.reactive.function.client.WebClient;
4
5@Configuration
6public class ClientBeans {
7
8    @Bean
9    WebClient billingWebClient(BillingClientProperties props) {
10        return WebClient.builder()
11            .baseUrl(props.getBaseUrl())
12            .build();
13    }
14}

For map-based configuration, create a factory service that looks up client config by name.

Testing Configuration Binding

Add a focused configuration test so prefix errors are caught before deployment. This is especially useful when teams rename keys or split files by environment.

java
1import org.junit.jupiter.api.Test;
2import org.springframework.boot.context.properties.bind.Bindable;
3import org.springframework.boot.context.properties.bind.Binder;
4import org.springframework.mock.env.MockEnvironment;
5
6class ClientsPropertiesTest {
7    @Test
8    void bindsBillingConfig() {
9        MockEnvironment env = new MockEnvironment()
10            .withProperty("clients.configs.billing.base-url", "https://billing.internal")
11            .withProperty("clients.configs.billing.connect-timeout-ms", "1000")
12            .withProperty("clients.configs.billing.read-timeout-ms", "3000");
13
14        ClientsProperties props = Binder.get(env)
15            .bind("clients", Bindable.of(ClientsProperties.class))
16            .orElseThrow();
17
18        assert props.getConfigs().containsKey("billing");
19    }
20}

This kind of test is quick and prevents silent misbinding when prefixes evolve.

Common Pitfalls

  • Defining @ConfigurationProperties classes but forgetting @ConfigurationPropertiesScan or explicit @EnableConfigurationProperties.
  • Mixing snake case and kebab case keys inconsistently across files.
  • Duplicating classes for dozens of integrations when a map-based model would be simpler.
  • Skipping validation, then discovering misconfiguration only after outbound calls fail.
  • Naming prefixes too generically, making ownership of settings unclear.

Summary

  • Use separate classes per prefix when integration count is small and clarity matters most.
  • Use a map-based root model for many similar external systems.
  • Enable property scanning and validate config on startup.
  • Convert property models into typed client beans in a dedicated config layer.
  • Keep prefix naming consistent so operations and debugging stay straightforward.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.