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
- What is the Spring Framework?
- IoC and DI
- Spring with XML: the classic way
- Annotation and Java configuration
- What is Spring Boot?
- Spring vs Spring Boot vs Spring Web
- Spring Boot architecture
- Your first Spring Boot application
- The three types of dependency injection
- Resolving ambiguity: @Primary and @Qualifier
- The lifecycle of a bean
- Building a web endpoint with Spring Web
- Configuration with application.properties
- Running, packaging and testing
- Best practices
- Which one should I use?
- Common mistakes
- Quick recap
- Try it yourself
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();
}
}
Output:
Dev is building the project...
Compiling on Laptop
It works, but look at the problems:
-
Devis stuck withLaptop. To use aDesktop, you must edit theDevclass. -
Devmust know how to build its helper. IfLaptopneeds aBatteryand aCharger,Devhas to create those too. - It's hard to test
Devalone, because you can't replace the realLaptopwith a fake one.
TIGHT COUPLING
Dev ---- creates ----> new Laptop() Dev decides AND builds its own helper
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();
}
}
Dev dev = new Dev(new Laptop()); // or new Dev(new Desktop())
dev.build();
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
jakartapackages, 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 ...
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
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(...)
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: extendsBeanFactorywith 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
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>
(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
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();
}
}
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>
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 inrefand ingetBean("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 callsetComp(...). -
value: a plain value (a string, number or boolean). Spring converts"Asha"to aString. -
ref: a reference to another bean, by itsid.
-
-
<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);
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();
}
}
}
Output:
Asha is building the project...
Compiling on Laptop
-
ClassPathXmlApplicationContext("spring.xml")creates the container. It looks forspring.xmlon the classpath (theresourcesfolder ends up there), reads the bean definitions and creates the objects. (FileSystemXmlApplicationContextloads a file from a disk path instead.) -
context.getBean(Dev.class)asks the container for the bean of typeDev. 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>
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"/>
(After adding <bean id="desktop" class="com.example.Desktop"/>.)
Asha is building the project...
Compiling on Desktop
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 writerefyourself. -
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: likebyType, 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>
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>
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
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>
Asha is building the project...
Compiling on Laptop
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>
Asha is building the project...
Compiling on Laptop
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"/>
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"/>
Dev a = context.getBean(Dev.class);
Dev b = context.getBean(Dev.class);
System.out.println(a == b);
- Default (singleton): prints
true. Both variables hold the same object. - With
scope="prototype": printsfalse. EachgetBeancreates 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 {
// ...
}
To turn scanning on in plain Spring, you add a configuration class:
@Configuration
@ComponentScan("com.example")
public class AppConfig { }
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
(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). -
@Controllerand@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)
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"
}
}
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
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) andspring-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 |
+--------------------------------------------------------------------------+
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, fromapplication.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)
(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
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
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
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>
- 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-starteris the core starter: Spring, logging and auto-configuration. -
spring-boot-starter-testbrings 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);
}
}
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();
-
@SpringBootApplicationmarks this as the starting configuration class, and turns on auto-configuration and component scanning (see the earlier section). -
SpringApplicationis 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.classis the main configuration class, where scanning begins. -
argsare the command-line arguments, like--server.port=9090.
-
-
context(theApplicationContext) is the Spring container: the object that holds all your beans.runreturns it. -
context.getBean(Dev.class)asks the container for the bean of typeDev(dependency lookup). - A bean is any object the container created and manages.
-
@Component(next) is what makesDeva 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();
}
@Component
public class Laptop implements Computer {
@Override
public void compile() {
System.out.println("Compiling on Laptop");
}
}
@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();
}
}
(Import org.springframework.stereotype.Component in the last two.) Here's what's going on:
-
@ComponentonLaptopandDevmakes them beans. Spring finds them because they're in the same package as the main class. -
Devneeds aComputer. Spring sees the constructor, finds aComputerbean (Laptop) and injects it automatically. - There's no
@Autowiredon 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
Output (after the Spring banner and a few INFO log lines):
Dev is building the project...
Compiling on Laptop
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.
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;
}
...
}
- It needs no
@Autowiredwhen 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
Laptopfirst, then call theDevconstructor 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;
}
...
}
- It needs
@Autowiredon the setter. Without it, Spring creates the object with the default constructor and never calls the setter, socomputerstaysnull. - Spring creates
Devfirst (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;
...
}
- It needs
@Autowiredon 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
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
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");
}
}
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
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 { ... }
Compiling on Laptop
Fix 2: @Qualifier. Name the exact bean at the place where you inject it:
public Dev(@Qualifier("desktop") Computer computer) {
this.computer = computer;
}
Compiling on Desktop
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;
How they differ:
-
@Primaryis a default for the whole application: "if nobody says otherwise, use this one." -
@Qualifieris an explicit choice at one injection point. If both are present,@Qualifierwins.
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@Qualifieris safer. -
Inject them all: ask for
List<Computer>orMap<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
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");
}
}
(@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
Things to notice:
- Singleton beans are created when the application starts, in dependency order (the
Laptopbefore theDevthat needs it). - Our
maindidn't call any of these methods. The container did. - When the application ends, the container closes and calls
@PreDestroyon 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>
(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 + "!";
}
}
@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);
}
}
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"
Hello, World!
Hello, Asha!
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!" <-------------------------+
-
@RestControllermeans "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 HTTPGET /helloto the method. -
@RequestParamreads thenamefrom the URL. - Dependency injection works the same as before: the controller got its
GreetingServicethrough 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
-
server.portchanges the web server's port. -
app.greetingis 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 + "!";
}
}
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 theargswe passed toSpringApplication.run. Command-line arguments win over the properties file. -
Profiles let you keep different settings per environment. Put them in
application-dev.propertiesand turn them on withspring.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
Package a runnable JAR:
./mvnw clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar
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);
}
}
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
finalfields. -
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
@Qualifierwith meaningful names, and use@Primarysparingly for the one obvious default. -
Use the specific stereotypes (
@Service,@Repository,@RestController) instead of plain@Componenteverywhere. -
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
getBeanonly inmainand 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,@Beanmethods 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.
-
@Primaryor@Qualifier?@Primaryfor a clear default,@Qualifierwhen a specific place needs a specific bean. -
@Componentor@Bean?@Componenton your classes.@Beanfor library classes or special setup. -
BeanFactoryorApplicationContext?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.
-
getBeanor injection? Injection in application code.getBeanonly where you have no other way in, likemain. -
.propertiesor.yml? Either. YAML is neater for nested settings, and properties are simpler. Use the one your team prefers.
Common mistakes
-
Creating beans with
new. If you writenew Dev()yourself, Spring doesn't know about that object, so its@Autowiredfields staynulland you get aNullPointerException. Ask the container for it, or inject it. -
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. -
Two beans of the same type. You get "required a single bean, but 2 were found." Use
@Primaryor@Qualifier, or inject aList. -
Forgetting
@Autowiredon a setter or field. Spring doesn't complain, and the dependency staysnull. (Constructor injection with a single constructor doesn't need it.) - 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.
-
XML slips.
ByTypeinstead ofbyType, a wrong class name, a<property name="comp">with nosetCompmethod, a missing no-argument constructor, or a beanidthat doesn't match the property name when usingbyName.
Quick recap
- Creating objects with
newties 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).
getBeanis 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, andprimary="true"resolves conflicts. You load it withClassPathXmlApplicationContext("spring.xml"). -
Annotations:
@Component(and@Service,@Repository,@RestController) create beans,@Configurationwith@Beanhandles special cases, and@Primaryand@Qualifierresolve ambiguity. -
Three kinds of DI: constructor (no
@Autowiredneeded 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 theApplicationContext, andgetBean(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)