By WebNest Studio Engineering Team
Spring Framework Tutorial
Spring Configuration: Java Config vs XML Config
Every Spring application needs a way to tell the container what beans to create and how they relate to each other. Historically, Spring used verbose XML files for this. Modern Spring strongly favors Java-based configuration using the @Configuration and @Bean annotations, which lets you define beans with ordinary, type-checked Java code instead of XML markup.
Both approaches ultimately produce the same result — a populated IoC container — so understanding XML configuration still matters for reading legacy codebases, but nearly all new Spring and Spring Boot projects are written entirely with Java configuration (often combined with component scanning so most beans need no explicit @Bean method at all).
Java Configuration with @Configuration and @Bean
A class annotated with @Configuration is itself treated as a source of bean definitions. Each method inside it annotated with @Bean tells Spring: "call this method, and register whatever it returns as a bean, named after the method by default." Spring processes @Configuration classes specially (via CGLIB proxying) so that calling one @Bean method from another within the same class still returns the same singleton instance rather than creating a duplicate object.
XML-Based Configuration
In XML configuration, beans are declared inside a <beans> root element using <bean id="..." class="..."> tags, with dependencies wired using nested <constructor-arg> or <property> elements. This style predates annotations and is still found in older enterprise codebases, loaded via ClassPathXmlApplicationContext. It has no compile-time type checking — a typo in a class name or property is only caught when the context starts.
- Java config: type-safe, refactor-friendly, supports full IDE navigation and debugging.
- XML config: no recompilation needed to change wiring, but no compile-time safety and more verbose.
- Both can be mixed via @ImportResource to load legacy XML into a Java-configured application.
Choosing Between Explicit @Bean Methods and Component Scanning
Use @Bean methods inside @Configuration classes for objects you don't own or can't annotate directly — third-party library classes, or beans that need conditional/complex construction logic. Use stereotype annotations (@Component, @Service, etc.) with @ComponentScan for your own application classes, since it removes the need for a separate @Bean method per class. Most real Spring applications use a combination of both.
When Two Beans Match: @Primary and @Qualifier
Java configuration often registers several beans of the same type, such as a card gateway and a UPI gateway that both implement PaymentGateway. When a constructor or @Bean method asks for a PaymentGateway, Spring has to pick one. It decides in this order:
- By type. Spring collects every bean that can be assigned to the requested type. If there is exactly one, it is injected.
- @Qualifier. If the injection point carries
@Qualifier("upiGateway"), only the bean with that name or qualifier stays in the running. - @Primary. If several candidates are still left, the one marked
@Primarywins. It acts as the default. - Parameter or field name. As a last resort, Spring looks for a bean whose name equals the parameter name. Since Spring 6.1 this only works when the code is compiled with
-parameters. Spring Boot's Maven and Gradle plugins turn that on, but a plainjavacbuild does not. - If none of these settles it, startup fails with
NoUniqueBeanDefinitionException: expected single matching bean but found 2.
Examples
Java-based configuration with two related @Bean methods
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
class DataSourceConfig {
String url;
DataSourceConfig(String url) { this.url = url; }
}
class UserRepository {
private final DataSourceConfig dataSourceConfig;
UserRepository(DataSourceConfig dataSourceConfig) { this.dataSourceConfig = dataSourceConfig; }
void connect() { System.out.println("Connecting to " + dataSourceConfig.url); }
}
@Configuration
class AppConfig {
@Bean
public DataSourceConfig dataSourceConfig() {
return new DataSourceConfig("jdbc:h2:mem:webnest");
}
@Bean
public UserRepository userRepository() {
// Calling dataSourceConfig() here returns the SAME singleton instance,
// not a new one, because @Configuration classes are CGLIB-proxied.
return new UserRepository(dataSourceConfig());
}
}
public class JavaConfigDemo {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(UserRepository.class).connect();
}
}
Connecting to jdbc:h2:mem:webnest
Two PaymentGateway beans: @Primary picks the default, @Qualifier asks for a specific one
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
public class QualifierDemo {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(PaymentConfig.class);
context.getBean(CheckoutService.class).pay(500);
context.getBean(RefundService.class).refund(200);
context.close();
}
}
interface PaymentGateway {
String name();
}
class CardGateway implements PaymentGateway {
public String name() { return "card"; }
}
class UpiGateway implements PaymentGateway {
public String name() { return "UPI"; }
}
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
void pay(int amount) { System.out.println("Paying " + amount + " by " + gateway.name()); }
}
class RefundService {
private final PaymentGateway gateway;
RefundService(PaymentGateway gateway) { this.gateway = gateway; }
void refund(int amount) { System.out.println("Refunding " + amount + " by " + gateway.name()); }
}
@Configuration
class PaymentConfig {
@Bean
@Primary // the default when a PaymentGateway is needed and nothing else decides
PaymentGateway cardGateway() { return new CardGateway(); }
@Bean
PaymentGateway upiGateway() { return new UpiGateway(); }
@Bean
CheckoutService checkoutService(PaymentGateway gateway) { // gets the @Primary bean
return new CheckoutService(gateway);
}
@Bean
RefundService refundService(@Qualifier("upiGateway") PaymentGateway gateway) { // asks for one by name
return new RefundService(gateway);
}
}
Paying 500 by card
Refunding 200 by UPI
Common Mistakes
- Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead.
- Calling a @Bean method with plain Java semantics in mind and assuming it creates a new object each time — inside an @Configuration class it is intercepted to return the existing singleton.
- Forgetting the class-name/property typos in XML configuration are only caught at context startup, not at compile time, unlike Java @Bean methods.
- Mixing @Component-scanned beans and manually defined @Bean beans of the same type without a qualifier, causing an ambiguous dependency error.
- Annotating a @Configuration class's @Bean methods as private or final, which breaks the CGLIB proxying Spring relies on for singleton correctness.
Exercise
Using the lesson's Java-based configuration with two related @Bean methods example, create a small implementation or design sketch that demonstrates @Configuration + @Bean is the modern, type-safe way to declare beans in Java code.. Add a test case or scenario that would catch Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead., and describe the expected result.
Show hints
- Start with Java Configuration with @Configuration and @Bean and compare your approach with Java-based configuration with two related @Bean methods. Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows @Configuration + @Bean is the modern, type-safe way to declare beans in Java code. and that it would catch the failure described by Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead..
Quick quiz
1. Which choice is a mistake to avoid in Spring Configuration: Java Config vs XML Config?
- A. Calling a @Bean method with plain Java semantics in mind and assuming it creates a new object each time — inside an @Configuration class it is intercepted to return the existing singleton.
- B. @Configuration + @Bean is the modern, type-safe way to declare beans in Java code.
- C. Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead.
Show answer
C. Avoid this approach: Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead. The lesson recommends: @Configuration + @Bean is the modern, type-safe way to declare beans in Java code.
Interview questions
A teammate is about to make this mistake: Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead. How would you diagnose and correct it using the lesson's guidance?
Show answer
@Configuration + @Bean is the modern, type-safe way to declare beans in Java code. Apply this in Java Configuration with @Configuration and @Bean; the example Java-based configuration with two related @Bean methods shows one way to do it. Avoid Relying on the parameter name to choose between two beans of the same type. It breaks when the parameter is renamed, and since Spring 6.1 it does nothing unless the code is compiled with -parameters. Use @Qualifier or @Primary instead. and explain how your implementation or design prevents that failure.
Key Points to Remember
- @Configuration + @Bean is the modern, type-safe way to declare beans in Java code.
- XML configuration (<bean> tags loaded via ClassPathXmlApplicationContext) is the legacy approach, still seen in older codebases.
- @Configuration classes are proxied so that inter-method @Bean calls still return the same singleton instance.
- Use @Bean for third-party or conditionally constructed objects; use stereotype annotations + @ComponentScan for your own classes.
- When several beans match a type, @Qualifier narrows the choice and @Primary sets the default; otherwise startup fails with NoUniqueBeanDefinitionException.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering