Spring Framework & the IoC Container

Configuration Styles: Java vs Annotations vs XML

18 min Lesson 5 of 13

Configuration Styles: Java vs Annotations vs XML

Spring gives you three distinct ways to tell the IoC container which beans to create and how to wire them together. Each style has existed for a reason, and all three still appear in production codebases today. Understanding them — and knowing which to reach for — is the difference between a developer who uses Spring and one who understands it.

The Three Styles at a Glance

  • XML configuration — the original (Spring 1.x, circa 2003). Beans and dependencies declared in applicationContext.xml.
  • Annotation-based configuration — introduced in Spring 2.5 (2007). Metadata lives in the class itself via annotations like @Component and @Autowired.
  • Java-based configuration — solidified in Spring 3.0 (2009). A plain Java class annotated with @Configuration acts as the bean factory.

Modern projects almost always combine annotation-based and Java-based configuration, but you will encounter all three when reading legacy code or open-source libraries.

Style 1: XML Configuration

Before annotations existed, developers described every bean and its dependencies in an XML file. Spring read the file, instantiated the beans, and wired them together.

<!-- src/main/resources/applicationContext.xml --> <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="userRepository" class="com.example.UserRepository"/> <bean id="userService" class="com.example.UserService"> <constructor-arg ref="userRepository"/> </bean> </beans>

Loading it at runtime:

import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService svc = ctx.getBean(UserService.class);
XML is fully type-safe at runtime but not at compile time. A typo in a class name or a missing dependency will only blow up when the context loads — not when you compile. Refactoring class names requires careful find-and-replace across XML files, because the IDE cannot always trace the reference automatically.

When you still see XML: legacy enterprise applications (pre-2010 code), Spring Integration / Spring Batch pipelines that were never migrated, and third-party library starters that ship a default configuration file you import.

Style 2: Annotation-Based Configuration

Annotation-based configuration moves the wiring metadata into the classes themselves. You mark a class as a bean with a stereotype annotation (@Component, @Service, @Repository, @Controller) and tell Spring to discover it with component scanning.

package com.example; import org.springframework.stereotype.Repository; @Repository public class UserRepository { public User findById(long id) { /* JDBC or ORM call */ return null; } }
package com.example; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository repo; @Autowired // Spring injects UserRepository here public UserService(UserRepository repo) { this.repo = repo; } public User getUser(long id) { return repo.findById(id); } }

To activate scanning, point Spring at the package:

import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan("com.example") // scan this package and sub-packages public class AppConfig {} // Bootstrap AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); UserService svc = ctx.getBean(UserService.class); ctx.close();
Prefer constructor injection over field injection. Constructor injection makes dependencies explicit, allows final fields (immutability), and makes the class trivially testable without a Spring container — just new UserService(mockRepo). Field injection with @Autowired directly on a private field hides dependencies and requires reflection to test.

Stereotype annotations carry meaning beyond discovery:

  • @Repository — marks a DAO; Spring translates persistence exceptions into its unified DataAccessException hierarchy.
  • @Service — marks business logic; no extra behaviour today but signals intent and is the conventional target for transactional proxies.
  • @Controller / @RestController — marks MVC controllers; Spring MVC maps request-handler methods on these beans.
  • @Component — the generic catch-all for anything that does not fit the three above.

Style 3: Java-Based Configuration

Java configuration replaces the XML file with a Java class. You annotate the class with @Configuration and declare each bean as a method annotated with @Bean. Spring calls those methods and manages the returned objects as beans.

package com.example; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public UserRepository userRepository() { return new UserRepository(); } @Bean public UserService userService() { // Spring intercepts this call; it does NOT create a second UserRepository. // It returns the singleton bean already in the container. return new UserService(userRepository()); } }
import org.springframework.context.annotation.AnnotationConfigApplicationContext; AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); UserService svc = ctx.getBean(UserService.class); ctx.close();
How Spring intercepts @Bean calls: @Configuration classes are subclassed by CGLIB at startup. The generated subclass overrides each @Bean method to check the container first; if the bean already exists it returns it instead of calling your factory code again. This is why calling userRepository() inside userService() gives you the singleton, not a fresh instance. Classes annotated with @Configuration are therefore not final.

Java config gives you full IDE support: rename a class and the compiler immediately flags broken references. You can also use conditionals, loops, or any Java logic inside a @Bean method — something impossible in XML.

@Bean public DataSource dataSource() { // logic driven by environment, profiles, etc. String url = System.getenv("DB_URL"); if (url == null) { // fall back to an in-memory H2 for local dev return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .build(); } HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(url); cfg.setUsername(System.getenv("DB_USER")); cfg.setPassword(System.getenv("DB_PASS")); return new HikariDataSource(cfg); }

Mixing Styles — The Real World

Styles compose. A @Configuration class can import an XML file, and an XML file can enable annotation scanning. A common professional setup:

@Configuration @ComponentScan("com.example") // picks up @Service / @Repository classes @ImportResource("classpath:legacy.xml") // integrates an old XML config public class AppConfig { @Bean // explicit @Bean for infrastructure public DataSource dataSource() { /* ... */ return null; } }
Avoid declaring the same bean in two places. If a class is already discovered by @ComponentScan because it carries @Service, adding a @Bean method for it in your @Configuration class creates a conflict. Spring 6 is stricter about this and may throw an exception rather than silently picking one.

Which Style to Use Today

  • New projects: annotation stereotypes (@Service, @Repository) for your own domain classes, plus @Configuration + @Bean for infrastructure beans (data sources, HTTP clients, third-party adapters). This combination is what Spring Boot uses under the hood.
  • XML: avoid for new code. Read and maintain it in legacy systems, but migrate to Java config when the opportunity arises.
  • Pure Java config (no scanning): useful when you want explicit, auditable control over every bean — common in library code and in teams that dislike magic discovery.

Summary

Spring offers XML, annotation-based, and Java-based configuration — and they are all supported simultaneously. XML is verbose but explicit. Annotations are concise but scatter metadata across classes. Java config gives you the type safety of a compiled language and the expressiveness of real code. Professional Spring applications today lean on annotation stereotypes for domain code and Java config for infrastructure, letting Spring Boot's auto-configuration handle the rest. Understanding all three styles means you can read any Spring codebase and make informed decisions about what to write next.