Spring Configuration & Profiles

Configuration Styles Overview

18 min Lesson 1 of 13

Configuration Styles Overview

Spring has supported three distinct ways to configure an application container since its early days: XML configuration, annotation-driven configuration, and Java-based configuration. As a working developer you will encounter all three in existing codebases, and you need to understand what each style looks like, where it shines, and why the community has largely converged on Java-based configuration for new projects running Spring 6 and Spring Boot 3.

Style 1 — XML Configuration

Spring was born with XML. Every bean, every dependency, every property value was declared in one or more .xml files that Spring read at startup through a ClassPathXmlApplicationContext or similar loader. The approach made Spring's wiring completely externalised from application code, which was considered a virtue in the early 2000s when Inversion of Control was a new idea.

<!-- beans.xml (Spring 2.x era) --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:postgresql://localhost/shop"/> <property name="username" value="${db.user}"/> <property name="password" value="${db.pass}"/> </bean> <bean id="orderRepository" class="com.example.OrderRepositoryImpl"> <constructor-arg ref="dataSource"/> </bean> </beans>

What you lose with XML: The compiler cannot validate bean names or class references. A typo in class="com.example.OrederRepositoryImpl" is not caught until the application starts. Refactoring a class name requires a find-and-replace across XML, not a rename in the IDE. The verbosity scales badly — large enterprise applications built this way could accumulate thousands of lines of XML spread across dozens of files.

Style 2 — Annotation-Driven Configuration

Spring 2.5 introduced @Component, @Service, @Repository, @Controller, and @Autowired. With component scanning enabled, Spring discovers and wires beans automatically just by finding annotated classes on the classpath. This cut XML dramatically.

// Spring scans this package and registers the class as a bean @Repository public class OrderRepositoryImpl implements OrderRepository { private final DataSource dataSource; @Autowired // Spring injects the DataSource bean here public OrderRepositoryImpl(DataSource dataSource) { this.dataSource = dataSource; } }

An XML file still had to enable scanning and declare infrastructure beans (like DataSource) that Spring cannot discover by itself:

<!-- A thinner XML file — but it still exists --> <context:component-scan base-package="com.example"/> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:postgresql://localhost/shop"/> </bean>

Annotations got the logic closer to the code but introduced a different problem: configuration was now scattered. To understand how a component was wired you had to jump between the class file, the XML, and potentially a properties file. There was no single place to read the full picture.

Style 3 — Java-Based Configuration

Spring 3.0 introduced @Configuration and @Bean, eliminating XML entirely. A configuration class is a plain Java class annotated with @Configuration; each method annotated with @Bean produces a bean managed by the Spring container.

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; @Configuration public class DataConfig { @Bean public DataSource dataSource() { HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl("jdbc:postgresql://localhost/shop"); cfg.setUsername(System.getenv("DB_USER")); cfg.setPassword(System.getenv("DB_PASS")); return new HikariDataSource(cfg); } @Bean public OrderRepository orderRepository(DataSource dataSource) { // Spring injects the dataSource bean declared above return new OrderRepositoryImpl(dataSource); } }

To bootstrap this context outside of Spring Boot:

import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class App { public static void main(String[] args) { var ctx = new AnnotationConfigApplicationContext(DataConfig.class); OrderRepository repo = ctx.getBean(OrderRepository.class); // use repo ... ctx.close(); } }
In Spring Boot 3 you almost never call AnnotationConfigApplicationContext directly. SpringApplication.run() creates and manages the application context for you, scanning your @SpringBootApplication class and all @Configuration classes it finds. The mechanism is identical — you just do not wire the bootstrap code yourself.

Why Java Configuration Is Preferred Today

The shift to Java configuration was not just stylistic. It delivers concrete, practical advantages:

  • Compile-time safety. Class names, method names, and parameter types are checked by the compiler. A bad refactor that leaves a stale reference fails the build, not the deployment.
  • IDE support. Navigate-to-definition, find-usages, and rename-refactoring work across configuration classes just as they do in regular Java code. With XML, IDEs could only provide best-effort support.
  • Full Java expressiveness. You can use if statements, loops, factory methods, builders, environment checks, and any other Java construct inside a @Bean method. XML could not express conditional logic without Spring-specific XML extensions.
  • Easier testing. A configuration class is a Java class. You can instantiate it in a unit test, call its @Bean methods directly, and verify the produced objects without starting a full Spring context.
  • Cohesion. Related bean definitions live together in one class, making it straightforward to understand a subsystem's wiring at a glance.
Prefer constructor injection over field injection. Even with Java config, you may be tempted to use @Autowired on a field inside a service class. Prefer constructor injection instead: it makes dependencies explicit, keeps classes testable without a Spring context, and plays well with final fields. Lombok's @RequiredArgsConstructor eliminates the boilerplate entirely.

Can the Three Styles Be Mixed?

Yes — Spring supports mixing all three in one application, and you will encounter this in real projects migrating legacy code. A Java config class can import an XML file with @ImportResource("classpath:legacy-beans.xml"). An XML file can trigger Java config scanning with <context:annotation-config/>. Annotations on component classes work regardless of whether the rest of the application uses XML or Java config.

Mixing styles adds cognitive overhead. When you inherit a codebase that uses all three, map it out first: identify which beans come from XML, which from component scan, and which from @Configuration classes. Attempting to trace a dependency without this map leads to confusion about why a bean exists and where it was registered.

Annotation Config vs Java Config — The Remaining Distinction

Even within modern Spring Boot 3 projects, there is a practical division of labour between the two non-XML styles:

  • Component scanning (@Component, @Service, @Repository, @Controller) is used for application classes you own — business services, repositories, controllers. Spring discovers them automatically.
  • Java config (@Configuration + @Bean) is used for third-party classes you cannot annotate yourself (e.g. HikariDataSource, Jackson ObjectMapper, AWS SDK clients) and for beans that require non-trivial construction logic.

Understanding this division is the foundation for everything in this tutorial: when to let component scanning do the work, when to write an explicit @Bean method, how to externalise configuration values, how to switch behaviour between environments, and how to make beans conditional. The next lesson dives deep into Java-based configuration — the style you will use most.