Skip to main content

Dynamic Configuration Loading in Spring Boot: Handling Multiple Variants with a Primary Configuration

Shop Christmas Products Now

In this post, we'll discuss how to dynamically load and manage configurations in a Spring Boot application based on various variants or profiles. This approach is especially useful in scenarios like A/B testing, where each variant may have distinct configuration requirements, but there's also a need for a primary or default configuration.

We’ll demonstrate the solution using a generalized example while outlining the key concepts.


Use Case

Imagine you have a Spring Boot application that needs to load different configurations for various feature variants dynamically, while also maintaining a default configuration as the fallback. The system should:

  1. Dynamically load configuration properties from multiple sources.
  2. Register variant-specific configurations as Spring beans.
  3. Ensure the default configuration is marked as primary for injection wherever no variant is specified.
  4. Provide a mechanism to retrieve a specific configuration based on the variant name.

The Architecture

  1. Configuration Loading: Load configurations from external YAML files.
  2. Dynamic Bean Registration: Register each configuration variant as a Spring bean at runtime.
  3. Dynamic Bean Lookup: Provide an interface to fetch a specific variant’s configuration.



Implementation

1. Configuration Properties Model


@ConfigurationProperties(prefix = "feature-config") @Data @NoArgsConstructor @AllArgsConstructor public class FeatureConfigProperties { private Map<String, String> settings; }


This model represents a generic configuration for a feature. Each variant may have different values for the settings map.




2. Loading Configurations from YAML




# default.yml feature-config: settings: key1: "default-value1" key2: "default-value2" # variant1.yml feature-config: settings: key1: "variant1-value1" key2: "variant1-value2"



Assume these YAML files are stored in directories categorized by environment (e.g., dev, prod) and variant.


3. YAML Resource Loader

This component loads YAML properties for each variant.

@Slf4j public class YamlResourceLoader { private static final String CONFIG_BASE_PATH = "classpath:/config"; public Map<String, Properties> loadAllVariantProperties(String environment) { Map<String, Properties> resultMap = new TreeMap<>(); String searchPath = CONFIG_BASE_PATH + "/" + environment + "/**/*.yml"; try { Resource[] resources = new PathMatchingResourcePatternResolver().getResources(searchPath); for (Resource resource : resources) { if (resource.exists() && resource.isReadable()) { String variant = extractVariantFromPath(resource.getURL().getPath(), environment); Properties properties = loadYamlProperties(resource); resultMap.put(variant, properties); } } } catch (IOException e) { throw new RuntimeException("Failed to load variant properties", e); } return resultMap; } private Properties loadYamlProperties(Resource resource) { YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean(); yamlFactory.setResources(resource); return yamlFactory.getObject(); } private String extractVariantFromPath(String path, String environment) { return path.substring(path.indexOf(environment + "/") + (environment + "/").length(), path.indexOf('/')); } }


4. Dynamic Bean Registration

This component dynamically registers beans for each variant.


@Slf4j public class DynamicConfigurationRegistrar { private static final String DEFAULT_VARIANT = "default"; private final ApplicationContext applicationContext; public DynamicConfigurationRegistrar(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void registerConfigurationBeans(Map<String, Properties> configMap, Environment environment) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory(); for (Map.Entry<String, Properties> entry : configMap.entrySet()) { String variant = entry.getKey(); Properties properties = entry.getValue(); registerConfigurationBean(registry, properties, FeatureConfigProperties.class, variant); } } private void registerConfigurationBean(BeanDefinitionRegistry registry, Properties properties, Class<?> clazz, String variant) { Binder binder = new Binder(new MapConfigurationPropertySource(properties)); Object beanInstance = binder.bind("feature-config", clazz).orElseThrow(); GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(clazz); beanDefinition.setInstanceSupplier(() -> beanInstance); if (DEFAULT_VARIANT.equals(variant)) { beanDefinition.setPrimary(true); } String beanName = clazz.getSimpleName() + "_" + variant; registry.registerBeanDefinition(beanName, beanDefinition); log.info("Registered configuration bean: {}", beanName); } }


5. Dynamic Configuration Lookup

A service to fetch beans dynamically by variant name


@Service public class ConfigurationLookupService { private final ApplicationContext applicationContext; @Autowired public ConfigurationLookupService(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public <T> T getConfigurationBean(String variant, Class<T> configClass) { String beanName = configClass.getSimpleName() + "_" + variant; if (applicationContext.containsBean(beanName)) { return applicationContext.getBean(beanName, configClass); } else { throw new RuntimeException("Configuration bean for variant '" + variant + "' not found."); } } }




6. Putting It All Together

A BeanFactoryPostProcessor to integrate the loader and registrar:


@Slf4j @Configuration public class ConfigurationLoader { @Bean public YamlResourceLoader yamlResourceLoader() { return new YamlResourceLoader(); } @Bean public DynamicConfigurationRegistrar dynamicConfigurationRegistrar(ApplicationContext context) { return new DynamicConfigurationRegistrar(context); } @Bean public BeanFactoryPostProcessor configurationPostProcessor( YamlResourceLoader loader, DynamicConfigurationRegistrar registrar) { return beanFactory -> { String environment = "prod"; // You can fetch this dynamically Map<String, Properties> configMap = loader.loadAllVariantProperties(environment); StandardEnvironment env = beanFactory.getBean(StandardEnvironment.class); registrar.registerConfigurationBeans(configMap, env); log.info("Dynamic configurations loaded successfully."); }; } }



Usage

Fetch configuration for a specific variant dynamically:


@Autowired private ConfigurationLookupService lookupService; public void useConfig() { FeatureConfigProperties defaultConfig = lookupService.getConfigurationBean("default", FeatureConfigProperties.class); FeatureConfigProperties variant1Config = lookupService.getConfigurationBean("variant1", FeatureConfigProperties.class); log.info("Default config: {}", defaultConfig.getSettings()); log.info("Variant1 config: {}", variant1Config.getSettings()); }




Conclusion

This approach provides a clean, extensible way to manage configuration variants dynamically. Key benefits include:

  1. Scalability: Easily add new variants without code changes.
  2. Separation of Concerns: Configuration and business logic are decoupled.
  3. Primary Fallback: Ensures a default configuration is always available.

You can adapt this design to any multi-variant scenario, making it ideal for dynamic and modular application architectures.





Comments

Popular posts from this blog

Learning How to Map One-to-Many Relationships in JPA Spring Boot with PostgreSQL

  Introduction In this blog post, we explore how to effectively map one-to-many relationships using Spring Boot and PostgreSQL. This relationship type is common in database design, where one entity (e.g., a post) can have multiple related entities (e.g., comments). We'll dive into the implementation details with code snippets and provide insights into best practices. Understanding One-to-Many Relationships A one-to-many relationship signifies that one entity instance can be associated with multiple instances of another entity. In our case: Post Entity : Represents a blog post with fields such as id , title , content , and a collection of comments . Comment Entity : Represents comments on posts, including fields like id , content , and a reference to the post it belongs to. Mapping with Spring Boot and PostgreSQL Let's examine how we define and manage this relationship in our Spring Boot application: Post Entity  @Entity @Getter @Setter @Builder @AllArgsConstructor @NoArgsCon...

Understanding the Advertisement Domain: A Comprehensive Overview Part 2

 The advertisement domain is a complex and dynamic ecosystem that involves various technologies and platforms working together to deliver ads to users in a targeted and efficient manner. The primary goal is to connect advertisers with their target audience, increasing brand visibility, user engagement, and revenue generation. In this blog, we will delve into the different components of the advertisement ecosystem, key concepts like programmatic advertising and real-time bidding (RTB), and provide a practical example to illustrate how it all works. Key Components of the Advertisement Domain The advertisement domain broadly consists of the following components: Advertisers : These are brands or companies that want to promote their products or services through advertisements. They set up ad campaigns targeting specific user segments. Publishers : These are websites, mobile apps, or digital platforms that display ads to users. Publishers monetize their content by selling ad space to ad...

Tree Based Common problems and patterns

  Find the height of the tree. public class BinaryTreeHeight { public static int heightOfBinaryTree (TreeNode root) { if (root == null ) { return - 1 ; // Height of an empty tree is -1 } int leftHeight = heightOfBinaryTree(root.left); int rightHeight = heightOfBinaryTree(root.right); // Height of the tree is the maximum of left and right subtree heights plus 1 for the root return Math.max(leftHeight, rightHeight) + 1 ; } Find the Level of the Node. private static int findLevel (TreeNode root, TreeNode node, int level) { if (root == null ) { return - 1 ; // Node not found, return -1 } if (root == node) { return level; // Node found, return current level } // Check left subtree int leftLevel = findLevel(root.left, node, level + 1 ); if (leftLevel != - 1 ) { return leftLevel; // Node found ...