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
    • Loose coupling with an interface
  • What is the Spring Framework?
    • A short history
    • The main modules
  • IoC and DI
    • Inversion of Control
    • Dependency Injection
    • IoC vs DI
    • The Spring container and beans
  • Spring with XML: the classic way
    • Project setup
    • The Java classes
    • Understanding spring.xml
    • Running the XML configuration
    • Injecting with property and constructor arguments
    • Autowiring in XML: byName and byType
    • Resolving conflicts with primary
    • Scope: singleton and prototype
  • Annotation and Java configuration
    • Stereotype annotations: @Component and friends
    • Java configuration: @Configuration and @Bean
  • What is Spring Boot?
    • The base idea
    • What Spring Boot adds
  • Spring vs Spring Boot vs Spring Web
  • Spring Boot architecture
    • Inside Spring Boot
    • How @SpringBootApplication works
    • The layers of a Spring Boot application
  • Your first Spring Boot application
    • Creating the project with Spring Initializr
    • Understanding the starter code
    • Adding beans: Dev, Computer and Laptop
    • Running the application
  • The three types of dependency injection
    • Constructor injection
    • Setter injection
    • Field injection
    • Which injection to use
  • 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:

  • 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 

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 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 ... 

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: 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 

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 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); 

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 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> 

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 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> 

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": 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 { // ... } 

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).
  • @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) 

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) 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 | +--------------------------------------------------------------------------+ 

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...