Spring Configuration & Profiles

Importing & Modularizing Configuration

18 min Lesson 9 of 13

Importing & Modularizing Configuration

As a Spring application grows, packing every @Bean definition into a single @Configuration class becomes unmanageable. Spring provides a clean mechanism for splitting configuration across multiple focused classes and then composing them: the @Import annotation family. This lesson covers how to use it, when to use it, and how to design your configuration layer so it stays easy to understand, test, and change.

Why Modularize Configuration?

A monolithic configuration class typically suffers from several problems:

  • Hard to navigate. Security beans, persistence beans, messaging beans and web beans are all tangled in one file.
  • Merge conflicts. When multiple developers touch different features they all edit the same file.
  • Difficult to test in isolation. Loading the entire context to test one slice is slow and fragile.
  • Poor reuse. You cannot bring a focused, self-contained configuration into another project easily.

Modularization solves all of these: each configuration class owns one concern, and they are composed explicitly.

@Import — Composing Configuration Classes

@Import tells Spring to process one or more additional @Configuration classes as if they were declared inline. All beans defined in the imported class become part of the application context.

import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import({ PersistenceConfig.class, SecurityConfig.class, MessagingConfig.class }) public class AppConfig { // Root configuration — composes focused modules }

Each imported class is a plain @Configuration class with its own beans:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; @Configuration public class PersistenceConfig { @Bean public DataSource dataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:postgresql://localhost/mydb"); ds.setUsername("app"); ds.setPassword("secret"); return ds; } }
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } }
@Import vs component scanning: Component scanning (covered in Lesson 3) picks up classes automatically based on the base package. @Import is explicit — you name exactly which class to include. Prefer @Import for library/framework configuration and for any class that lives outside your component-scan base package.

Bean Visibility Across Imported Configurations

Once a class is imported — whether directly with @Import or pulled in transitively — its beans are available to every other configuration class in the same context. You can @Autowired-inject them as constructor parameters on a @Configuration class, or let Spring resolve them as method parameters on a @Bean method.

@Configuration @Import({ PersistenceConfig.class, SecurityConfig.class }) public class ServiceConfig { // Spring injects the DataSource bean defined in PersistenceConfig @Bean public UserRepository userRepository(DataSource dataSource) { return new JdbcUserRepository(dataSource); } // Spring injects the PasswordEncoder bean defined in SecurityConfig @Bean public AuthService authService(UserRepository userRepository, PasswordEncoder passwordEncoder) { return new AuthServiceImpl(userRepository, passwordEncoder); } }

Hierarchical @Import — Transitive Composition

Imported classes can themselves use @Import, creating a tree of configurations. This is how many Spring Boot auto-configuration classes work internally.

// PersistenceConfig already imports a lower-level JPA config @Configuration @Import(JpaConfig.class) public class PersistenceConfig { // DataSource beans, transaction manager, etc. } @Configuration public class JpaConfig { @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource ds) { // JPA-specific setup LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setDataSource(ds); factory.setPackagesToScan("com.example.domain"); return factory; } }

When AppConfig imports PersistenceConfig, Spring automatically processes JpaConfig as well. You do not need to list it again in AppConfig.

@ImportResource — Mixing XML with Java Config

Legacy projects often have existing XML bean definitions. @ImportResource lets you pull those into a Java-based configuration without rewriting them:

@Configuration @ImportResource("classpath:legacy/messaging-beans.xml") public class LegacyBridgeConfig { // Modern Java beans can reference beans defined in the XML file }
Migration strategy: Use @ImportResource as a bridge while migrating. Convert XML beans to @Bean methods one module at a time, removing the XML file once the migration of each module is complete. Do not try to rewrite everything at once.

@ImportSelector — Dynamic, Programmatic Imports

When you need to decide at startup which configuration classes to import based on runtime conditions, implement ImportSelector:

import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; public class StorageConfigSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata metadata) { String storageType = System.getProperty("storage.type", "local"); if ("s3".equals(storageType)) { return new String[]{ "com.example.config.S3StorageConfig" }; } return new String[]{ "com.example.config.LocalStorageConfig" }; } } // Used via @Import: @Configuration @Import(StorageConfigSelector.class) public class AppConfig { }

This is the mechanism behind Spring Boot's @EnableAutoConfiguration — it uses a sophisticated ImportSelector to pick the right auto-configuration classes based on what is on the classpath.

Recommended Module Boundaries

A practical convention that scales well in real applications:

  • PersistenceConfig — DataSource, EntityManagerFactory, TransactionManager
  • SecurityConfig — PasswordEncoder, AuthenticationManager, security filter chain
  • WebConfig — MVC configuration, converters, CORS, view resolvers
  • MessagingConfig — JMS/RabbitMQ/Kafka factories and listeners
  • CacheConfig — CacheManager, key generators
  • AppConfig (root) — imports all of the above; defines only truly cross-cutting beans
Avoid circular @Import. If ConfigA imports ConfigB and ConfigB imports ConfigA, Spring will throw a BeanDefinitionParsingException or behave unpredictably. Resolve circular dependencies by extracting shared beans to a third, lower-level configuration class that both can import.

Testing a Single Configuration Module

One of the biggest payoffs of modularization is testability. You can load exactly one configuration class in a test without the overhead of the full context:

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @SpringJUnitConfig(PersistenceConfig.class) // only loads this one module class PersistenceConfigTest { @Autowired DataSource dataSource; @Test void dataSourceIsConfigured() { assertThat(dataSource).isNotNull(); } }

Summary

@Import is the primary tool for composing focused @Configuration classes into a coherent application context. You have seen how to use it directly, how transitive imports build a configuration tree, how @ImportResource bridges legacy XML, and how ImportSelector enables runtime-conditional imports. Splitting configuration by concern — persistence, security, messaging — makes each piece independently navigable, testable, and reusable. The next lesson closes the tutorial with a hands-on project that puts multi-environment configuration together end to end.