DEV Community

Mayur Bodhare
Mayur Bodhare

Posted on

What Does SpringApplication.run Actually Do?

When I started writing bigger Java programs, I noticed something annoying. Every class created the objects it needed with new. A Dev class created its own Laptop. The Laptop class created its own Battery. And when I wanted to change one small thing, like using a Desktop instead, I had to open and edit class after class.

Then someone told me about Spring, and described it like this: "You don't create the objects. Spring creates them for you when you need them."

That one sentence is the base idea behind both the Spring Framework and Spring Boot. In this post, we'll unpack it step by step: what problem Spring solves, what IoC and DI really mean, how the Spring container works, how configuration worked in the XML days (and what <beans>, <bean>, <property>, <constructor-arg>, autowire and primary do), how annotations like @Component, @Autowired, @Primary and @Qualifier replaced it, and finally what Spring Boot adds on top, including the meaning of that starter code everyone copies without understanding.

I wrote this for Spring Framework 7 and Spring Boot 4.x, which need Java 17 or newer. I've drawn the key pictures in text so you can see how everything connects.

Table of contents

The problem: creating objects yourself

Tight coupling with new

Let's start with plain Java, no Spring. A developer (Dev) builds a project on a laptop:

public class Laptop {
    public void compile() {
        System.out.println("Compiling on Laptop");
    }
}

public class Dev {
    private Laptop laptop = new Laptop();      // Dev creates its own helper

    public void build() {
        System.out.println("Dev is building the project...");
        laptop.compile();
    }
}

public class App {
    public static void main(String[] args) {
        Dev dev = new Dev();
        dev.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Dev is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

It works, but look at the problems:

  • Dev is stuck with Laptop. To use a Desktop, you must edit the Dev class.
  • Dev must know how to build its helper. If Laptop needs a Battery and a Charger, Dev has to create those too.
  • It's hard to test Dev alone, because you can't replace the real Laptop with a fake one.
   TIGHT COUPLING

   Dev  ---- creates ---->  new Laptop()        Dev decides AND builds its own helper
Enter fullscreen mode Exit fullscreen mode

Loose coupling with an interface

The first fix is to depend on an interface, and to receive the helper from outside, instead of creating it:

public interface Computer {
    void compile();
}

public class Laptop implements Computer {
    public void compile() { System.out.println("Compiling on Laptop"); }
}

public class Desktop implements Computer {
    public void compile() { System.out.println("Compiling on Desktop"); }
}

public class Dev {
    private final Computer computer;

    public Dev(Computer computer) {            // the helper is GIVEN to Dev
        this.computer = computer;
    }

    public void build() {
        System.out.println("Dev is building the project...");
        computer.compile();
    }
}
Enter fullscreen mode Exit fullscreen mode
Dev dev = new Dev(new Laptop());      // or new Dev(new Desktop())
dev.build();
Enter fullscreen mode Exit fullscreen mode

Now Dev doesn't care which computer it gets. It only knows the Computer interface. That's called dependency injection, and we just did it by hand.

But there's a new problem: someone has to write all that wiring code. In a real application with hundreds of classes, main would become a giant list of new calls in exactly the right order. This is what Spring automates. It builds the objects and passes them to each other for you.

What is the Spring Framework?

The Spring Framework is an open-source Java framework. Its core is a container that creates your objects, connects them to each other and manages their whole life. On top of that core, Spring offers support for web applications, databases, transactions, security, testing and more.

A short history

  • In the early 2000s, enterprise Java (J2EE) was heavy and complicated, especially EJBs. Rod Johnson described a simpler approach in his 2002 book, and the code that came with it grew into Spring.
  • Spring's idea was that you write plain Java objects (POJOs), and the framework connects them. The name is often explained as a "fresh start" after the heavy J2EE years.
  • Spring Framework 1.0 came out in 2004. It has been updated steadily since. Version 6 (2022) moved to Java 17 and the jakarta packages, and version 7 arrived in late 2025.
  • Plain Spring still needed a lot of setup, so Spring Boot 1.0 was released in 2014 to remove most of it. Spring Boot 4.0 came out in November 2025, built on Spring Framework 7, and 4.1 followed in June 2026.

The main modules

Spring is split into modules, so you only use what you need:

   SPRING FRAMEWORK
   +--------------------------------------------------------------------+
   |  CORE CONTAINER     core, beans, context, expression language      |  <- IoC and DI live here
   +--------------------------------------------------------------------+
   |  AOP                aspect-oriented programming (logging, security)|
   |  DATA ACCESS        JDBC, ORM (Hibernate/JPA), transactions        |
   |  WEB                spring-web, spring-webmvc, spring-webflux      |
   |  TEST               support for JUnit and mock environments        |
   +--------------------------------------------------------------------+

   Related projects built on it: Spring Data, Spring Security, Spring Batch, Spring Cloud ...
Enter fullscreen mode Exit fullscreen mode

Everything else depends on the core container, so that's what we'll focus on first.

IoC and DI

Inversion of Control

Normally, your code is in control: it decides which objects to create, when to create them and how to connect them.

Inversion of Control (IoC) flips that. You describe what you need, and a container takes control of creating and connecting objects. The control is "inverted": from your code to the framework. People sometimes call it the Hollywood principle: "Don't call us, we'll call you."

   WITHOUT IoC                                     WITH IoC (Spring)

   your code:                                      your code:
     laptop = new Laptop()                           "I need a Dev"
     dev    = new Dev(laptop)                              |
     dev.build()                                           v
                                                    SPRING CONTAINER
   YOU create and wire everything                     creates Laptop
                                                      creates Dev, gives it the Laptop
                                                      hands you the finished Dev
Enter fullscreen mode Exit fullscreen mode

Dependency Injection

A dependency is an object that another object needs. Dev depends on a Computer.

Dependency Injection (DI) means the dependency is handed in from outside, instead of being created inside. Think of a company: a developer doesn't build their own laptop. The IT department gives them one. The developer just uses it.

In Spring, the container is the IT department. It builds the Laptop, builds the Dev, and injects the laptop into the dev. There are three ways it can hand things over: through the constructor, through a setter method or directly into a field. We'll look at all three later.

IoC vs DI

These two terms are mixed up constantly:

  • IoC is the big principle: some framework controls object creation and wiring, not your code.
  • DI is the technique used to achieve it: the container gives each object what it needs.

All DI is IoC, but IoC is a bigger idea. There's also a second form of IoC called dependency lookup, where your code asks the container for an object. A call like context.getBean(Dev.class) is a lookup, not an injection.

   IoC   (the principle: the container is in control)
     |
     |-- Dependency Injection   the container GIVES objects what they need   (most common)
     +-- Dependency Lookup      your code ASKS the container:  context.getBean(...)
Enter fullscreen mode Exit fullscreen mode

The Spring container and beans

The Spring container (also called the IoC container) is the part of Spring that creates objects, connects them and manages their entire lifecycle: creating them, injecting their dependencies, initializing them and finally destroying them. The objects it manages are called beans. A bean isn't a special kind of class. It's just an ordinary Java object that the container created and looks after.

To know what to create, the container needs configuration metadata. That can be XML, annotations or Java code, and we'll see all three.

There are two container interfaces:

  • BeanFactory: the basic container, which creates and wires beans.
  • ApplicationContext: extends BeanFactory with more features, like event publishing, reading properties, resource loading, internationalization and easy integration with other Spring features. This is the one you use.

When people say "the context," they mean the ApplicationContext: the box that holds all your beans.

   configuration metadata            your classes
   (XML / annotations / Java)        (Laptop, Dev ...)
              \                         /
               v                       v
          +----------------------------------+
          |   SPRING CONTAINER               |
          |   (ApplicationContext)           |
          |   creates, wires, manages        |
          |   [laptop]  [dev]  [...]         |   <- the beans
          +----------------------------------+
                      |
                      v   getBean(...)  or  injection
                  your application
Enter fullscreen mode Exit fullscreen mode

Spring with XML: the classic way

Before annotations, Spring was configured with XML files. It's still fully supported, you'll meet it in older projects and interviews, and understanding it makes the modern annotation style much clearer.

Project setup

Create a plain Maven project (see my Maven post) and add Spring's core context library. I'll import the Spring BOM so the versions stay in sync:

<properties>
  <maven.compiler.release>17</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-framework-bom</artifactId>
      <version>7.0.10</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
  </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

(Use the newest 7.0.x version you can find.) The XML file goes in the resources folder:

   my-spring-app/
   |-- pom.xml
   +-- src/main/
        |-- java/com/example/   <- Computer, Laptop, Desktop, Dev, App
        +-- resources/spring.xml
Enter fullscreen mode Exit fullscreen mode

The Java classes

We reuse Computer, Laptop and Desktop from before. Dev now has setters and two constructors, so Spring can build it in different ways:

public class Dev {
    private String name;
    private Computer comp;

    public Dev() { }

    public Dev(String name, Computer comp) {
        this.name = name;
        this.comp = comp;
    }

    public void setName(String name) { this.name = name; }
    public void setComp(Computer comp) { this.comp = comp; }

    public void build() {
        System.out.println(name + " is building the project...");
        comp.compile();
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice there's no Spring code at all in these classes. They're plain Java objects. All the Spring knowledge lives in the XML.

Understanding spring.xml

Here is the configuration file, src/main/resources/spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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="laptop" class="com.example.Laptop"/>

    <bean id="dev" class="com.example.Dev">
        <property name="name" value="Asha"/>
        <property name="comp" ref="laptop"/>
    </bean>

</beans>
Enter fullscreen mode Exit fullscreen mode

What each part means:

  • <?xml ...?>: the standard first line of any XML file.
  • <beans>: the root tag. Everything inside is a bean definition. Its attributes (xmlns, xsi:schemaLocation) tell the XML parser which schema (a rule book) defines the valid tags. They also let your IDE autocomplete and check the file.
  • <bean>: defines one object.
    • id: a unique name for this bean. You use it in ref and in getBean("dev").
    • class: the full class name Spring should create. Spring creates it using reflection, with the no-argument constructor unless you provide <constructor-arg>.
  • <property>: setter injection.
    • name: the property name. name="comp" makes Spring call setComp(...).
    • value: a plain value (a string, number or boolean). Spring converts "Asha" to a String.
    • ref: a reference to another bean, by its id.
  • <constructor-arg>: constructor injection (we'll use it in a moment).

You can think of this file as Spring executing the Java code you used to write yourself:

Laptop laptop = new Laptop();
Dev dev = new Dev();
dev.setName("Asha");
dev.setComp(laptop);
Enter fullscreen mode Exit fullscreen mode

Running the XML configuration

package com.example;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) {
        try (ClassPathXmlApplicationContext context =
                     new ClassPathXmlApplicationContext("spring.xml")) {

            Dev dev = context.getBean(Dev.class);
            dev.build();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Asha is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode
  • ClassPathXmlApplicationContext("spring.xml") creates the container. It looks for spring.xml on the classpath (the resources folder ends up there), reads the bean definitions and creates the objects. (FileSystemXmlApplicationContext loads a file from a disk path instead.)
  • context.getBean(Dev.class) asks the container for the bean of type Dev. You can also ask by id: context.getBean("dev"), or by both: context.getBean("dev", Dev.class).
  • The context is closable, so try-with-resources shuts it down (and destroys the beans) at the end.

Notice we never wrote new Dev(). The container made it.

Injecting with property and constructor arguments

Besides <property>, you can inject through the constructor with <constructor-arg>:

<bean id="dev" class="com.example.Dev">
    <constructor-arg value="Asha"/>
    <constructor-arg ref="laptop"/>
</bean>
Enter fullscreen mode Exit fullscreen mode

By default, the arguments are matched in order. You can be explicit with index="0", type="..." or name="comp". The output is the same.

Now the best part. To switch to a desktop, change one word in the XML, with no Java changes at all:

<constructor-arg ref="desktop"/>
Enter fullscreen mode Exit fullscreen mode

(After adding <bean id="desktop" class="com.example.Desktop"/>.)

Asha is building the project...
Compiling on Desktop
Enter fullscreen mode Exit fullscreen mode

A rule of thumb: use <constructor-arg> for things the object must have, and <property> for optional or changeable settings.

Autowiring in XML: byName and byType

Writing ref for every dependency gets tedious. Autowiring lets Spring find the dependencies itself. The autowire attribute has these main values:

  • no: the default. No autowiring. You write ref yourself.
  • byType: Spring looks at the type of each property (Computer) and finds a bean of that type.
  • byName: Spring looks at the name of each property (comp) and finds a bean with the same id.
  • constructor: like byType, but for constructor arguments.

(The values are case-sensitive: write byType, not ByType.)

byType with exactly one Computer bean:

<bean id="laptop" class="com.example.Laptop"/>

<bean id="dev" class="com.example.Dev" autowire="byType">
    <property name="name" value="Asha"/>
</bean>
Enter fullscreen mode Exit fullscreen mode

There's no <property name="comp" ...>. Spring noticed that Dev has a setComp(Computer) method, found the one Computer bean (laptop) and injected it. Autowiring only applies to objects. Simple values like the name string still need <property value="...">.

Now add a second Computer bean, and Spring can't decide:

<bean id="laptop" class="com.example.Laptop"/>
<bean id="desktop" class="com.example.Desktop"/>

<bean id="dev" class="com.example.Dev" autowire="byType">
    <property name="name" value="Asha"/>
</bean>
Enter fullscreen mode Exit fullscreen mode
UnsatisfiedDependencyException: Error creating bean with name 'dev' ...
Unsatisfied dependency expressed through bean property 'comp':
No qualifying bean of type 'com.example.Computer' available:
expected single matching bean but found 2: laptop,desktop
Enter fullscreen mode Exit fullscreen mode

Two beans match the type, so Spring refuses to guess. One fix is byName:

<bean id="comp" class="com.example.Laptop"/>
<bean id="desktop" class="com.example.Desktop"/>

<bean id="dev" class="com.example.Dev" autowire="byName">
    <property name="name" value="Asha"/>
</bean>
Enter fullscreen mode Exit fullscreen mode
Asha is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

It works because Dev has a property named comp, and there's a bean whose id is comp. The desktop bean is ignored. The matching is by name, so the bean id and the property name must agree exactly.

Resolving conflicts with primary

The other fix is to mark one bean as the preferred choice with primary="true":

<bean id="laptop" class="com.example.Laptop" primary="true"/>
<bean id="desktop" class="com.example.Desktop"/>

<bean id="dev" class="com.example.Dev" autowire="byType">
    <property name="name" value="Asha"/>
</bean>
Enter fullscreen mode Exit fullscreen mode
Asha is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

When several beans match, Spring picks the primary one. There's also a related attribute, autowire-candidate="false", which removes a bean from autowiring completely:

<bean id="desktop" class="com.example.Desktop" autowire-candidate="false"/>
Enter fullscreen mode Exit fullscreen mode

Scope: singleton and prototype

By default, Spring creates one instance of each bean and shares it. That's the singleton scope (one per container, not the same as the singleton design pattern). You can ask for a new object every time with scope="prototype":

<bean id="dev" class="com.example.Dev" scope="prototype"/>
Enter fullscreen mode Exit fullscreen mode
Dev a = context.getBean(Dev.class);
Dev b = context.getBean(Dev.class);
System.out.println(a == b);
Enter fullscreen mode Exit fullscreen mode
  • Default (singleton): prints true. Both variables hold the same object.
  • With scope="prototype": prints false. Each getBean creates a new one.

Singleton beans are created when the container starts (eagerly). Add lazy-init="true" to create one only when it's first needed. In web applications there are more scopes too, like request and session.

XML works, but it has downsides: it's wordy, the configuration is far from the code, and typos in class names show up only at run time. That's why Spring moved toward annotations.

Annotation and Java configuration

Stereotype annotations: @Component and friends

Instead of declaring each bean in XML, you put @Component on the class. Spring finds those classes by component scanning:

@Component
public class Laptop implements Computer {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

To turn scanning on in plain Spring, you add a configuration class:

@Configuration
@ComponentScan("com.example")
public class AppConfig { }
Enter fullscreen mode Exit fullscreen mode
AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(AppConfig.class);
Enter fullscreen mode Exit fullscreen mode

(In XML, the same switch is <context:component-scan base-package="com.example"/>.)

@Component has specialized versions that say what the class is for. They're all components underneath:

  • @Service: business logic.
  • @Repository: data access (it also converts database exceptions into Spring's exception types).
  • @Controller and @RestController: web request handlers.

The default bean name is the class name with a lowercase first letter (Laptop becomes laptop). You can set your own with @Component("comp").

Here's how the XML ideas you just learned map to annotations:

   XML                                       ANNOTATIONS
   <bean id="laptop" class="...Laptop"/>     @Component on the class Laptop
   <property ref> / <constructor-arg ref>    constructor parameter, or @Autowired
   autowire="byType"                         @Autowired (matches by type)
   autowire="byName"                         @Autowired + @Qualifier("name")
   primary="true"                            @Primary
   scope="prototype"                         @Scope("prototype")
   lazy-init="true"                          @Lazy
   init-method / destroy-method              @PostConstruct / @PreDestroy
   ClassPathXmlApplicationContext            AnnotationConfigApplicationContext
                                             (or SpringApplication.run in Spring Boot)
Enter fullscreen mode Exit fullscreen mode

Java configuration: @Configuration and @bean

What if you can't put @Component on a class, because it comes from a library? Or the creation needs custom steps? Use a @Bean method in a @Configuration class:

@Configuration
public class AppConfig {

    @Bean
    public Computer laptop() {
        return new Laptop();
    }

    @Bean
    public Dev dev(Computer computer) {
        return new Dev("Asha", computer);      // Spring supplies "computer"
    }
}
Enter fullscreen mode Exit fullscreen mode

The bean's name is the method name (laptop, dev), and method parameters are injected by Spring. A quick rule: use @Component for classes you write, and @Bean for classes you don't own or that need special setup.

What is Spring Boot?

With the Spring Framework alone, you still have a lot to do before your first line of real code. You must pick compatible versions of many libraries, write configuration for the database, the web layer and transactions, install and configure a server like Tomcat, and package the app for deployment.

Spring Boot removes most of that work. It isn't a different framework. It's Spring, with the setup done for you.

The base idea

Remember the sentence at the start? Spring Boot creates the objects for you, when you need them.

You write your own classes and mark them (@Component, @Service, @RestController). Boot's container creates them and wires them together, just like plain Spring. But Boot also creates the infrastructure objects automatically, based on what's in your project:

  • Add the web starter, and Boot creates an embedded Tomcat server, the request dispatcher and the JSON converters.
  • Add a database starter and driver, and Boot creates a connection pool, and with JPA, Hibernate's factory and a transaction manager (the objects we built by hand in my Hibernate post).
   your classes  +  starters (what's on the classpath)  +  application.properties
                              |
                              v
                       SPRING BOOT
              creates and wires the beans for you
                              |
                              v
                   a running application
Enter fullscreen mode Exit fullscreen mode

You describe what you need, and Boot builds it.

What Spring Boot adds

  • Starters: one dependency (like spring-boot-starter-webmvc) that brings a whole matching set of libraries. No more hunting for compatible versions.
  • Auto-configuration: Boot configures beans automatically based on your classpath and properties.
  • An embedded server: Tomcat is built in, so the app runs with java -jar. No separate server and no WAR deployment.
  • Sensible defaults: convention over configuration. You override only what you need.
  • No XML needed.
  • application.properties: one place for settings.
  • Production features: the Actuator module adds health checks and metrics, for example /actuator/health.
  • Developer tools: quick restarts, an initializer website, and good testing support.

Spring vs Spring Boot vs Spring Web

These three names cause the most confusion, so here's how they relate:

  • Spring Framework is the foundation: the IoC container, dependency injection, transactions, data access and more. You configure everything yourself.
  • Spring Web is the part of the framework for web applications and REST APIs. It has two main sets of modules: spring-web (HTTP basics and clients) and spring-webmvc (Spring MVC, the controller-based web framework). There's also Spring WebFlux, a reactive alternative. These are modules inside the Spring Framework.
  • Spring Boot is a layer on top that sets all of this up for you: starters, auto-configuration, an embedded server and packaging.
   +--------------------------------------------------------------------------+
   |  SPRING BOOT                                                              |
   |  starters  ·  auto-configuration  ·  embedded server  ·  Actuator         |
   +--------------------------------------------------------------------------+
   |  SPRING WEB MVC / WEBFLUX      |  SPRING DATA  |  SPRING SECURITY  |  ... |
   +--------------------------------------------------------------------------+
   |  SPRING FRAMEWORK CORE: IoC container, dependency injection, AOP ...      |
   +--------------------------------------------------------------------------+
   |  JAVA / JVM                                                               |
   +--------------------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Here's how they differ in practice:

  • Setup: Spring needs manual setup (dependencies, configuration, server). Boot generates a ready project in seconds.
  • Configuration: Spring uses XML or Java config you write. Boot auto-configures and you override what you need.
  • Server: with Spring you deploy a WAR to a server you installed. With Boot the server is embedded.
  • Versions: with Spring you pick compatible library versions yourself. With Boot, the starters and parent manage them.
  • Packaging: Spring usually produces a WAR. Boot produces an executable JAR.
  • Control: Spring gives you full control, with more work. Boot gives sensible defaults, with less work.

About names: when you pick "Spring Web" on the Spring Initializr website, you get Spring MVC and an embedded Tomcat. In Spring Boot 4, that starter is called spring-boot-starter-webmvc. Before Boot 4, it was spring-boot-starter-web. The old name is deprecated in the newest versions, but you'll still see it in older tutorials.

Also remember: Boot doesn't replace Spring. Everything you learned about the container, beans and injection still applies. That's why we learn those first.

Spring Boot architecture

Inside Spring Boot

Spring Boot is built from a few key parts:

  • SpringApplication: the class that starts your application.
  • Starters: dependency bundles.
  • Auto-configuration: the logic that creates beans for you.
  • The Environment: your settings, from application.properties, environment variables and command-line arguments.
  • The embedded server: Tomcat by default.
  • Actuator: optional monitoring.

Here's what happens when you call SpringApplication.run(...):

   SpringApplication.run(DemoApplication.class, args)
        |
        1. create the ApplicationContext   (the container)
        |
        2. load the environment            (application.properties, env variables, command line)
        |
        3. component scan                  find @Component, @Service, @RestController ... classes
        |
        4. auto-configuration              add beans based on the classpath and properties
        |                                  (web server, DataSource, JSON converters ...)
        5. create all singleton beans, inject their dependencies
        |
        6. start the embedded web server   (only if a web starter is present)
        |
        7. the application is running      (the context is closed when the app shuts down)
Enter fullscreen mode Exit fullscreen mode

(Spring Boot 4 also split its internals into smaller, focused modules, but the process above stays the same.)

How @SpringBootApplication works

One annotation on your main class does a lot. @SpringBootApplication is a shortcut for three annotations:

@SpringBootApplication
// ... is the same as combining:
@SpringBootConfiguration     // this class is a configuration class (like @Configuration)
@EnableAutoConfiguration     // turn on auto-configuration
@ComponentScan               // scan this package and its sub-packages for @Component classes
Enter fullscreen mode Exit fullscreen mode

Auto-configuration works like this. Spring Boot's jars contain a list of auto-configuration classes (in a file named META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports). Each one is guarded by conditions, such as:

  • @ConditionalOnClass: only apply if a certain class is on the classpath.
  • @ConditionalOnMissingBean: only apply if you haven't defined your own bean.
  • @ConditionalOnProperty: only apply if a property is set.

For example, if a database driver is on the classpath and you haven't defined a DataSource yourself, Boot creates one from your spring.datasource.* properties. If you define your own DataSource, Boot backs off and uses yours. If you ever wonder why a bean exists (or doesn't), start the app with --debug to print a report of every condition that was checked.

An important consequence: component scanning starts at the package of your main class. Classes outside that package and its sub-packages are not found. That's why the main class sits in the root package of the project.

The layers of a Spring Boot application

A typical Spring Boot web application is organized in layers, and each layer is a bean that gets the next layer injected:

   Browser / mobile app / Postman
            |   HTTP request
            v
   EMBEDDED TOMCAT  -->  DispatcherServlet  (Spring MVC's front controller)
                              |
                              v
        +-------------------------------+
        |  CONTROLLER  (@RestController)|   handles HTTP: URLs, parameters, JSON
        +-------------------------------+
                      |  calls
                      v
        +-------------------------------+
        |  SERVICE     (@Service)       |   business logic, transactions
        +-------------------------------+
                      |  calls
                      v
        +-------------------------------+
        |  REPOSITORY  (@Repository)    |   talks to the database (JPA / JDBC)
        +-------------------------------+
                      |
                      v
                  DATABASE
Enter fullscreen mode Exit fullscreen mode

The rule: each layer talks only to the layer below it. Controllers don't touch the database, and repositories hold no business rules. Spring wires the layers together with dependency injection.

Your first Spring Boot application

Creating the project with Spring Initializr

The easiest way to start is the Spring Initializr website, start.spring.io. Choose:

  • Project: Maven
  • Language: Java
  • Spring Boot: the newest stable 4.x version (not a SNAPSHOT or release candidate)
  • Group: com.example
  • Artifact: demo
  • Packaging: Jar
  • Java: 17 or newer (21 is a good choice)
  • Dependencies: none for the first example. Later, we'll add Spring Web.

Click Generate, unzip the file and open the folder in your IDE. (IDEs like IntelliJ IDEA also have this built in.) You get this project:

   demo/
   |-- pom.xml
   |-- mvnw, mvnw.cmd, .mvn/            <- the Maven Wrapper (run Maven without installing it)
   +-- src/
        |-- main/
        |    |-- java/com/example/demo/DemoApplication.java
        |    +-- resources/application.properties
        +-- test/java/com/example/demo/DemoApplicationTests.java
Enter fullscreen mode Exit fullscreen mode

The generated pom.xml (trimmed) looks like this:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.0</version>
    <relativePath/>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
Enter fullscreen mode Exit fullscreen mode
  • The parent (spring-boot-starter-parent) manages the versions of hundreds of libraries, and sets up sensible build defaults. That's why the dependencies below have no version numbers.
  • spring-boot-starter is the core starter: Spring, logging and auto-configuration.
  • spring-boot-starter-test brings testing tools (test scope only).
  • The Spring Boot Maven plugin builds the runnable JAR.

(The version 4.1.0 is just an example. Use whatever Initializr generates for you.)

Understanding the starter code

Open DemoApplication.java. This is the code everyone copies without understanding:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's decode it word by word. First, a version of main that actually uses the result:

ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
Dev dev = context.getBean(Dev.class);
dev.build();
Enter fullscreen mode Exit fullscreen mode
  • @SpringBootApplication marks this as the starting configuration class, and turns on auto-configuration and component scanning (see the earlier section).
  • SpringApplication is the Boot class that starts everything.
  • SpringApplication.run(DemoApplication.class, args): this one call creates the container, loads the settings, scans for components, applies auto-configuration, creates all the beans and (for web apps) starts the server.
    • DemoApplication.class is the main configuration class, where scanning begins.
    • args are the command-line arguments, like --server.port=9090.
  • context (the ApplicationContext) is the Spring container: the object that holds all your beans. run returns it.
  • context.getBean(Dev.class) asks the container for the bean of type Dev (dependency lookup).
  • A bean is any object the container created and manages.
  • @Component (next) is what makes Dev a bean in the first place.

And notice the most important thing again: we never wrote new Dev().

Adding beans: Dev, Computer and Laptop

Create these three files in com.example.demo, next to DemoApplication:

public interface Computer {
    void compile();
}
Enter fullscreen mode Exit fullscreen mode
@Component
public class Laptop implements Computer {

    @Override
    public void compile() {
        System.out.println("Compiling on Laptop");
    }
}
Enter fullscreen mode Exit fullscreen mode
@Component
public class Dev {

    private final Computer computer;

    public Dev(Computer computer) {
        this.computer = computer;
    }

    public void build() {
        System.out.println("Dev is building the project...");
        computer.compile();
    }
}
Enter fullscreen mode Exit fullscreen mode

(Import org.springframework.stereotype.Component in the last two.) Here's what's going on:

  • @Component on Laptop and Dev makes them beans. Spring finds them because they're in the same package as the main class.
  • Dev needs a Computer. Spring sees the constructor, finds a Computer bean (Laptop) and injects it automatically.
  • There's no @Autowired on the constructor, because a class with a single constructor doesn't need it.

Running the application

Use the version of main that calls getBean, then run:

./mvnw spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Output (after the Spring banner and a few INFO log lines):

Dev is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

What if you forget @Component on Laptop? Spring can't find a Computer, and the app fails to start with a clear message:

APPLICATION FAILED TO START

Description:

Parameter 0 of constructor in com.example.demo.Dev required a bean of type
'com.example.demo.Computer' that could not be found.

Action:

Consider defining a bean of type 'com.example.demo.Computer' in your configuration.
Enter fullscreen mode Exit fullscreen mode

The three types of dependency injection

There are three ways the container can hand a dependency to your object. Let's write Dev all three ways.

Constructor injection

The dependency comes in through the constructor:

@Component
public class Dev {

    private final Computer computer;

    public Dev(Computer computer) {       // no @Autowired needed
        this.computer = computer;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode
  • It needs no @Autowired when the class has one constructor (this has been true since Spring 4.3). If a class has several constructors, mark the one Spring should use with @Autowired.
  • Spring must create the Laptop first, then call the Dev constructor with it.

Setter injection

The dependency comes in through a setter method:

@Component
public class Dev {

    private Computer computer;

    @Autowired
    public void setComputer(Computer computer) {
        this.computer = computer;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode
  • It needs @Autowired on the setter. Without it, Spring creates the object with the default constructor and never calls the setter, so computer stays null.
  • Spring creates Dev first (with the no-argument constructor), then calls the setter.

Field injection

The dependency is placed directly into the field:

@Component
public class Dev {

    @Autowired
    private Computer computer;
    ...
}
Enter fullscreen mode Exit fullscreen mode
  • It needs @Autowired on the field.
  • Spring creates Dev, then uses reflection to set the private field. There's no constructor or setter involved.

All three give the same output:

Dev is building the project...
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

Which injection to use

                         NEEDS @Autowired?        BEST FOR
   Constructor           No (one constructor)     required dependencies  <-- recommended
   Setter                Yes                      optional or changeable dependencies
   Field                 Yes                      quick demos and test classes
Enter fullscreen mode Exit fullscreen mode

Use constructor injection by default. The reasons:

  • The dependency can be final, so the object can't be left half-built.
  • Required dependencies are obvious from the constructor.
  • It's easy to test: new Dev(new FakeComputer()), with no Spring needed.
  • Missing beans and circular dependencies are caught at startup, not later. (Spring Boot refuses circular references by default.)

Field injection looks shorter, but it hides the dependencies, can't use final, can't be tested without Spring or reflection, and the field stays null if someone creates the object with a plain new. Setter injection is a good choice for optional dependencies, and you can use @Autowired(required = false) for those.

Resolving ambiguity: @primary and @Qualifier

What happens when two beans match? Add a Desktop next to Laptop:

@Component
public class Desktop implements Computer {

    @Override
    public void compile() {
        System.out.println("Compiling on Desktop");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Dev asks for a Computer and gets two candidates. Spring Boot refuses to start:

APPLICATION FAILED TO START

Description:

Parameter 0 of constructor in com.example.demo.Dev required a single bean, but 2 were found:
    - desktop: defined in file [.../Desktop.class]
    - laptop: defined in file [.../Laptop.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans,
or using @Qualifier to identify the bean that should be consumed
Enter fullscreen mode Exit fullscreen mode

The error message itself tells you the fixes.

Fix 1: @Primary. Mark one bean as the default choice:

@Component
@Primary
public class Laptop implements Computer { ... }
Enter fullscreen mode Exit fullscreen mode
Compiling on Laptop
Enter fullscreen mode Exit fullscreen mode

Fix 2: @Qualifier. Name the exact bean at the place where you inject it:

public Dev(@Qualifier("desktop") Computer computer) {
    this.computer = computer;
}
Enter fullscreen mode Exit fullscreen mode
Compiling on Desktop
Enter fullscreen mode Exit fullscreen mode

The name "desktop" is the bean's default name (the class name with a lowercase first letter). You can set your own with @Component("fast") and then use @Qualifier("fast"). With field injection, you write both annotations together:

@Autowired
@Qualifier("desktop")
private Computer computer;
Enter fullscreen mode Exit fullscreen mode

How they differ:

  • @Primary is a default for the whole application: "if nobody says otherwise, use this one."
  • @Qualifier is an explicit choice at one injection point. If both are present, @Qualifier wins.

These map to the XML ideas you saw earlier: @Primary is like primary="true", and @Qualifier is like autowire="byName".

There are two more ways to deal with several beans:

  • Match by name: if the parameter is named desktop (public Dev(Computer desktop)), Spring falls back to matching the name. It works, but renaming the parameter silently changes the behavior, so @Qualifier is safer.
  • Inject them all: ask for List<Computer> or Map<String, Computer>, and Spring gives you every matching bean.

The lifecycle of a bean

Here's what "Spring manages the lifecycle of all your objects" means. Every bean goes through the same stages:

   1. INSTANTIATE        the constructor is called
          |
          v
   2. INJECT             dependencies are set (constructor, setter or field)
          |
          v
   3. INITIALIZE         @PostConstruct methods run
          |
          v
   4. READY              the bean is used by your application
          |
          v
   5. DESTROY            @PreDestroy methods run when the container shuts down
Enter fullscreen mode Exit fullscreen mode

Let's watch it happen by adding messages to Laptop:

@Component
public class Laptop implements Computer {

    public Laptop() {
        System.out.println("Laptop: constructor called");
    }

    @PostConstruct
    public void init() {
        System.out.println("Laptop: @PostConstruct, ready to use");
    }

    @Override
    public void compile() {
        System.out.println("Compiling on Laptop");
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("Laptop: @PreDestroy, shutting down");
    }
}
Enter fullscreen mode Exit fullscreen mode

(@PostConstruct and @PreDestroy come from jakarta.annotation.) Run the app. Output (between the Spring log lines):

Laptop: constructor called
Laptop: @PostConstruct, ready to use
Dev is building the project...
Compiling on Laptop
Laptop: @PreDestroy, shutting down
Enter fullscreen mode Exit fullscreen mode

Things to notice:

  • Singleton beans are created when the application starts, in dependency order (the Laptop before the Dev that needs it).
  • Our main didn't call any of these methods. The container did.
  • When the application ends, the container closes and calls @PreDestroy on each singleton. This is the place to close resources like files or connections.

A few related facts: the default scope is singleton (one shared bean). You can use @Scope("prototype") to get a new instance each time, but then Spring doesn't call destroy methods for it. @Lazy delays creation until first use. And because singletons are shared by all threads, keep them stateless: don't store per-request data in their fields.

Building a web endpoint with Spring Web

Now let's use Spring Web. Add the dependency (on Initializr, pick Spring Web, or add this to pom.xml):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

(In Spring Boot 3.x, this starter is called spring-boot-starter-web.) Then add a service and a controller:

@Service
public class GreetingService {

    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}
Enter fullscreen mode Exit fullscreen mode
@RestController
public class HelloController {

    private final GreetingService greetingService;

    public HelloController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(name = "name", defaultValue = "World") String name) {
        return greetingService.greet(name);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the app, and the log shows that Tomcat started (on port 8080 by default). The application keeps running, until you stop it with Ctrl+C. Now call it:

curl http://localhost:8080/hello
curl "http://localhost:8080/hello?name=Asha"
Enter fullscreen mode Exit fullscreen mode
Hello, World!
Hello, Asha!
Enter fullscreen mode Exit fullscreen mode

Here's what happened:

   curl  -->  embedded Tomcat  -->  DispatcherServlet  -->  HelloController.hello("Asha")
                                                                   |  (it was given a GreetingService
                                                                   v   by constructor injection)
                                                            GreetingService.greet("Asha")
                                                                   |
   curl  <--  response: "Hello, Asha!"  <-------------------------+
Enter fullscreen mode Exit fullscreen mode
  • @RestController means "this class handles web requests, and returns the data directly as the response body." (If you return an object instead of a string, Boot converts it to JSON for you.)
  • @GetMapping("/hello") maps HTTP GET /hello to the method.
  • @RequestParam reads the name from the URL.
  • Dependency injection works the same as before: the controller got its GreetingService through its constructor.

Configuration with application.properties

Spring Boot reads settings from src/main/resources/application.properties (or application.yml). Some examples:

server.port=9090
spring.application.name=demo
app.greeting=Welcome
Enter fullscreen mode Exit fullscreen mode
  • server.port changes the web server's port.
  • app.greeting is your own setting. Read it into a bean with @Value:
@Service
public class GreetingService {

    private final String greeting;

    public GreetingService(@Value("${app.greeting:Hello}") String greeting) {
        this.greeting = greeting;
    }

    public String greet(String name) {
        return greeting + ", " + name + "!";
    }
}
Enter fullscreen mode Exit fullscreen mode

The :Hello part is a default used if the property isn't set. With app.greeting=Welcome, the endpoint now returns Welcome, Asha!.

A few more useful facts:

  • Settings can be overridden by environment variables (SERVER_PORT=9090) and by command-line arguments (--server.port=9090). Those are the args we passed to SpringApplication.run. Command-line arguments win over the properties file.
  • Profiles let you keep different settings per environment. Put them in application-dev.properties and turn them on with spring.profiles.active=dev.
  • For groups of related settings, look at @ConfigurationProperties, which binds them to a Java class.
  • Don't put real passwords in the file. Use environment variables or a secrets manager.

Running, packaging and testing

Run during development:

./mvnw spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Package a runnable JAR:

./mvnw clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar
Enter fullscreen mode Exit fullscreen mode

In the Maven post, a plain JAR failed with "no main manifest attribute." A Spring Boot JAR works, because the Boot Maven plugin turns it into an executable "fat JAR": it contains your classes, all the libraries, the embedded server and the information about the main class. You can override settings at launch, like java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port=9090.

Check the application's health. Add the spring-boot-starter-actuator dependency, and http://localhost:8080/actuator/health returns {"status":"UP"}.

Test it. @SpringBootTest starts the whole container for a test, and you can inject beans straight into the test class:

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private Dev dev;

    @Test
    void devBeanIsCreated() {
        assertNotNull(dev);
    }
}
Enter fullscreen mode Exit fullscreen mode

Field injection is perfectly fine in test classes. For testing just the web layer, there are focused test annotations like @WebMvcTest. The test starter brings in JUnit, so everything in my JUnit post applies here.

Best practices

  • Use constructor injection for required dependencies, with final fields.
  • Never create beans with new. Let the container do it, otherwise injection and the lifecycle are skipped.
  • Depend on interfaces (Computer), not concrete classes, so implementations can be swapped.
  • Keep beans stateless. Singletons are shared between threads.
  • Keep the layers clean: controller, service, repository. Put business logic in services.
  • Put the main class in the root package, so component scanning finds everything below it.
  • Use @Qualifier with meaningful names, and use @Primary sparingly for the one obvious default.
  • Use the specific stereotypes (@Service, @Repository, @RestController) instead of plain @Component everywhere.
  • Externalize configuration in application.properties, profiles and environment variables, and keep secrets out of code.
  • Avoid circular dependencies. Redesign instead of fighting the container.
  • Use getBean only in main and tests. In application code, use injection.
  • Let the Boot parent or BOM manage versions, and don't override library versions unless you must.
  • Read the startup error. Spring Boot's failure messages usually tell you exactly what to fix.

Which one should I use?

  • Spring Framework alone or Spring Boot? Spring Boot for almost every new project. Plain Spring for learning the core, and for older applications that already use it.
  • XML, annotations or Java configuration? Annotations (@Component) for your own classes, @Bean methods for classes you don't own, and XML only in legacy code.
  • Constructor, setter or field injection? Constructor by default, setter for optional dependencies, and field injection only in tests or quick demos.
  • @Primary or @Qualifier? @Primary for a clear default, @Qualifier when a specific place needs a specific bean.
  • @Component or @Bean? @Component on your classes. @Bean for library classes or special setup.
  • BeanFactory or ApplicationContext? ApplicationContext, always.
  • Spring MVC or WebFlux? Spring MVC (spring-boot-starter-webmvc) for most applications. WebFlux for reactive, non-blocking designs.
  • Singleton or prototype? Singleton (the default) for stateless beans. Prototype when each user of the bean needs its own instance.
  • getBean or injection? Injection in application code. getBean only where you have no other way in, like main.
  • .properties or .yml? Either. YAML is neater for nested settings, and properties are simpler. Use the one your team prefers.

Common mistakes

  1. Creating beans with new. If you write new Dev() yourself, Spring doesn't know about that object, so its @Autowired fields stay null and you get a NullPointerException. Ask the container for it, or inject it.
  2. Missing @Component, or a class outside the scan. The error says "required a bean of type ... that could not be found." Check the annotation, and check that the class is in the main class's package or one of its sub-packages.
  3. Two beans of the same type. You get "required a single bean, but 2 were found." Use @Primary or @Qualifier, or inject a List.
  4. Forgetting @Autowired on a setter or field. Spring doesn't complain, and the dependency stays null. (Constructor injection with a single constructor doesn't need it.)
  5. Keeping state in singleton beans. A singleton is shared by every request and thread. Fields that change per user lead to confusing bugs. Keep beans stateless.
  6. XML slips. ByType instead of byType, a wrong class name, a <property name="comp"> with no setComp method, a missing no-argument constructor, or a bean id that doesn't match the property name when using byName.

Quick recap

  • Creating objects with new ties classes together. Dependency injection gives an object its dependencies from outside, and Spring automates the wiring.
  • IoC is the principle: the container, not your code, controls object creation. DI is the technique (the container gives objects what they need). getBean is the other form, called dependency lookup.
  • The Spring container (ApplicationContext) creates beans, injects their dependencies and manages their lifecycle: instantiate, inject, @PostConstruct, ready, @PreDestroy.
  • XML configuration: <beans> is the root, <bean id class> defines an object, <property> is setter injection, <constructor-arg> is constructor injection, autowire="byName" or "byType" lets Spring find dependencies, and primary="true" resolves conflicts. You load it with ClassPathXmlApplicationContext("spring.xml").
  • Annotations: @Component (and @Service, @Repository, @RestController) create beans, @Configuration with @Bean handles special cases, and @Primary and @Qualifier resolve ambiguity.
  • Three kinds of DI: constructor (no @Autowired needed for a single constructor, and the best choice), setter (needs @Autowired) and field (needs @Autowired, and is discouraged).
  • Spring Boot is Spring with the setup done for you: starters, auto-configuration, an embedded server and an executable JAR. It creates the objects you need.
  • Spring is the foundation, Spring Web (Spring MVC) is its web module, and Spring Boot is the layer that sets it all up.
  • SpringApplication.run(DemoApplication.class, args) creates the container, scans, auto-configures, creates the beans and starts the server. It returns the ApplicationContext, and getBean(Dev.class) fetches a bean from it.
  • A Boot application is layered: controller, service, repository, each one injected into the layer above.

Try it yourself

Here's a small practice idea: build the Dev, Computer, Laptop and Desktop example twice. First use plain Spring with spring.xml: wire it with <property>, then with <constructor-arg>, then with autowire="byType", and make the two-bean error happen on purpose before fixing it with primary="true" and again with byName. Then rebuild the same thing in Spring Boot with @Component, and break it with a second Computer bean before fixing it with @Primary and @Qualifier.

For a bonus experiment, add @PostConstruct and @PreDestroy messages to a bean and watch the order. Then add Spring Web, write a /hello endpoint that uses a service with constructor injection, change the port with --server.port=9090, and package everything into a JAR you can start with java -jar.

If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.

Top comments (0)