DEV Community

Ankit Verma
Ankit Verma

Posted on

Configuration styles: XML annotations Java config

Every Spring application starts with the same quiet question: how does the container know which objects to build? The first articles in this module described the container as a factory that reads recipe cards — bean definitions — and builds your objects from them. But they skipped over where those recipe cards actually come from.

That source is a configuration style: the way you tell Spring what beans exist and how they wire together. Spring has had three of them over its life — XML files, annotations, and Java config — and they arrived in that order, each fixing a pain in the one before.

You meet all three whether you like it or not. A legacy module still carries an XML file. Your own code is dotted with @Service. And the framework's own internals, plus most modern setup, are written in Java config. This article walks the three in the order they appeared, because each one only makes sense as an answer to the problem the previous one left behind.

The recipe cards have to come from somewhere

Recall the one rule from the first article: Spring's powers only apply to beans the container built. So before the container can build anything, something has to hand it the list of beans — their classes, their dependencies, their scopes. That list is the configuration.

Here is the part that ties everything together. The format of that list is separate from what the container does with it. However the definitions arrive, they all become the same internal bean definition objects, and the same factory builds from them.

So the three styles are not three different containers. They are three different ways of writing down the exact same thing. Keep that in mind, because it is why you can freely mix them in one application.

The XML era: configuration as a separate file

In the beginning, Spring kept all configuration in a separate XML file, completely outside your Java code. You listed each bean by its class name and spelled out its wiring by hand:

<beans>
    <bean id="httpClient" class="com.shop.HttpClient"/>

    <bean id="gateway" class="com.shop.PaymentGateway">
        <constructor-arg ref="httpClient"/>
    </bean>

    <bean id="orderService" class="com.shop.OrderService">
        <constructor-arg ref="gateway"/>
    </bean>
</beans>
Enter fullscreen mode Exit fullscreen mode

Read it top to bottom and it is just the recipe cards, written out longhand. Each <bean> names a class. Each <constructor-arg ref="..."> says "pass this other bean into the constructor." You point the container at the file, and it builds everything listed.

The appeal was real. All wiring lived in one place, visible at a glance, and you could change which class got wired where without recompiling a line of Java. Configuration was data, not code.

But the costs piled up fast. The file is verbose — three lines of XML for what a constructor call says in one. The class names are plain strings, so a typo or a renamed class fails only at startup, never at compile time. And your IDE cannot help: rename PaymentGateway in Java and the XML still says the old name, silently. As applications grew to hundreds of beans, the XML grew with them, and it became its own burden to maintain.

Moving the marks into the code

The next step was to stop describing beans in a far-off file and instead mark the class itself as a bean, right where it is defined. That is what @Component and its specialized cousins — @Service, @Repository, @Controller — do:

@Service
class OrderService {
    OrderService(PaymentGateway gateway) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

One annotation replaces a whole <bean> block. The container finds these marked classes by component scanning — walking your packages at startup and registering every annotated class as a bean. (Scanning is its own topic, the very next article; for now just take it that the container can find these marks.)

This fixed the worst of XML. The bean definition now sits next to the code it describes, so the two can never drift apart. There are no class-name strings to mistype — the annotation rides on the class, so a rename carries it along. And the dependencies are simply the constructor parameters, resolved by type, exactly as the dependency-injection article described.

So annotations won for your own classes, and that is where they remain the default today. But they have one hard limit, and it is the reason a third style had to exist.

The wall annotations hit

@Component only works if you can put it on the class. For your own code, fine. But a large share of the beans in a real application are not your classes at all — they come from libraries. A DataSource from a connection-pool library, an ObjectMapper from Jackson, a RestTemplate from Spring itself.

You cannot annotate those. You do not own the source, and editing a library's jar to add @Component is not an option. Annotation-based configuration simply has nothing to say about a class you did not write.

// You want this as a bean, but you can't add @Component to it —
// HikariDataSource lives in someone else's jar.
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://localhost/shop");
Enter fullscreen mode Exit fullscreen mode

So you need a way to register a bean without touching its class — and ideally one that is still plain, type-safe Java, not a string-typed file off to the side. That is exactly what Java config provides.

Java config: methods that build beans

The third style writes configuration as ordinary Java, in a class marked @Configuration. Inside it, each method marked @Bean builds and returns one bean. The method body is the recipe — you write the construction yourself:

@Configuration
class InfrastructureConfig {

    @Bean
    DataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://localhost/shop");
        return ds;
    }
}
Enter fullscreen mode Exit fullscreen mode

The container calls dataSource() once at startup, takes the returned object, and registers it as a bean named dataSource — the method name. The third-party problem is gone: you never needed to annotate HikariDataSource, you just called its constructor inside a method you do own.

Notice what this keeps from the annotation style and what it wins back from XML. It is plain Java, so it is type-safe and your IDE refactors it like any other code — no magic strings. But like XML, it can register beans for classes you cannot annotate, and it hands you a real method body where you can run logic — read a property, pick an implementation, tune the object — before returning it.

How Java-config beans find each other

A bean usually needs other beans. In Java config you wire them by writing a method that takes the dependency as a parameter, or by calling another @Bean method directly:

@Configuration
class InfrastructureConfig {

    @Bean
    PaymentGateway gateway(DataSource dataSource) {   // asks for the DataSource bean
        return new PaymentGateway(dataSource);
    }

    @Bean
    OrderService orderService() {
        return new OrderService(gateway());   // calls the other @Bean method
    }
}
Enter fullscreen mode Exit fullscreen mode

The parameter form is the clean one: ask for DataSource as a method argument and the container injects the existing bean, exactly as constructor injection does for your @Components.

The second form is where people get nervous. orderService() calls gateway() directly — and a plain Java method call would run the method again, building a second PaymentGateway. That would shatter the singleton guarantee: two different gateways floating around, when the whole point was one shared instance.

The @Configuration proxy that makes it safe

Here the proxy from the first article comes back to do real work. Spring does not use your @Configuration class directly. At startup it wraps it in a proxy — a same-shaped subclass — that intercepts every call to a @Bean method.

So when orderService() calls gateway(), the proxy steps in front of that call. Instead of running the method body again, it checks the container: if the gateway bean already exists, it hands back that cached instance. The body runs once, no matter how many other @Bean methods call it.

@Bean
OrderService orderService() {
    return new OrderService(gateway());   // proxy returns the SAME gateway bean, not a new one
}
Enter fullscreen mode Exit fullscreen mode

That is why inter-bean method calls are safe inside a @Configuration class — and only inside one. The same call from a plain class, or from a class marked @Component instead of @Configuration, gets no proxy and really does run the method twice. (Spring lets you switch the proxy off with @Configuration(proxyBeanMethods = false) when you know no @Bean method calls another — it trims a little startup work, at the cost of this guarantee.)

Which style, when

These three styles all feed the same container, and a single application happily uses more than one at once. The modern division of labor is settled:

  • Annotations (@Component and friends) for your own application classes — the default, because the mark lives right on the code it describes.
  • Java config (@Configuration + @Bean) for everything you cannot annotate: third-party objects, and any bean whose creation needs real logic.
  • XML essentially never in new code. You learn to read it only because older codebases — and a lot of the internet — still speak it.

The thing to hold onto is that the choice is about where the recipe is written, not about what you get out. Whichever style registers a bean, it lands in the container as the same kind of bean definition, builds the same way, and can be wired into beans declared by any other style.

Putting it together

Configuring Spring means handing the container its recipe cards, and the configuration style is just the form those cards take. Spring has offered three, each answering the last one's weakness. XML put all wiring in one external file — central and recompile-free, but verbose, string-typed, and blind to refactoring. Annotations moved the mark onto the class itself, killing the drift and the typos, but they only work on classes you own. Java config writes each bean as a @Bean method in a @Configuration class — type-safe Java that can register anything, including third-party objects, and run logic while it builds.

The one mechanism worth remembering is the @Configuration proxy: it intercepts calls between @Bean methods so a shared bean is built once, not every time it is referenced. That single trick is what lets Java config read like ordinary method calls while still honoring the container's singleton promise.

All three styles end in the same place — bean definitions in one container. Which leaves the question we kept deferring: when you use the annotation style, how does the container actually find your @Component classes scattered across dozens of packages? That is component scanning, and it is where we go next.

Top comments (0)