Course topics

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 @Primary wins. 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 plain javac build 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

Java
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();
    }
}
Output
Connecting to jdbc:h2:mem:webnest

Two PaymentGateway beans: @Primary picks the default, @Qualifier asks for a specific one

Java
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);
    }
}
Output
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.

WebNest Studio

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