Spring Basics

Table Of Contents

At its core, Spring is a Dependency Injection framework. Although it now offers a variety of other features that simplify developers' lives, most of these are built on top of the Dependency Injection framework.

Dependency Injection (DI) is often considered the same as Inversion of Control (IoC). Let’s briefly explain and classify the two terms in the context of the Spring Framework.

Inversion of Control

The concept of Inversion of Control is to give control over the execution of program code to a framework. This can be done, for example, through a function that we program ourselves and then pass to a framework, which then calls it at the right time. We call this function a “callback.”

An example of a callback is a function that should be executed in a server application when a specific URL is called. We program the function, but we do not call it ourselves. Instead, we pass the function to a framework that listens for HTTP requests on a specific port, analyzes the request, and then forwards it to one of the registered callback functions based on specific parameters. The Spring WebMVC project is based on this exact mechanism.

Dependency Injection

Dependency Injection is a specific form of Inversion of Control. As the name suggests, Dependency Injection is about dependencies. Class A is dependent on another class B if class A calls a method of B. In program code, this dependency is often expressed as an attribute of type B in class A:

class GreetingService {
  UserDatabase userDatabase = new UserDatabase();

  String greet(Integer userId){
    User user = userDatabase.findUser(userId);
    return "Hello " + user.getName();

In this example, the GreetingService requires an object of type UserDatabase to do its work. When we instantiate an object of type GreetingService, it automatically creates an object of type UserDatabase.

The GreetingService class is responsible for resolving the dependency on UserDatabase. This is problematic due to several reasons.

First, this solution creates a very strong coupling between the two classes. GreetingService must know how to create a UserDatabase object. What if creating a UserDatabase object is not that simple? To open a database connection, we usually require a few parameters:

class GreetingService {

  UserDatabase userDatabase;

  public GreetingService(
    String dbUrl,  
    Integer dbPort
      this.userDatabase = new UserDatabase(dbUrl, dbPort);

  String greet(Integer userId){
    User user = userDatabase.findUser(userId);
    return "Hello " + user.getName();

The GreetingService still creates its own instance of type UserDatabase, but now it needs to know which parameters are required for a database connection. The coupling between GreetingService and UserDatabase has just become even stronger. We don’t want to see these details in the GreetingService!

What if other classes in our application also need a UserDatabase object? We don’t want every class to know how to create a UserDatabase object!

Due to the strong coupling, details of the UserDatabase class are spread throughout the codebase. A change to UserDatabase would therefore lead to many changes in other parts of the code.

This not only makes it difficult to develop application code but also to write tests. If we want to test the GreetingService class, we need the URL and port of a real database in this example. If we pass invalid connection parameters, the greet() method no longer works!

To break the strong coupling between classes, we modify the code so that we can “inject” the dependency into the constructor:

class GreetingService {

  final UserDatabase userDatabase;

  public GreetingService(UserDatabase userDatabase){
    this.userDatabase = userDatabase;

  String greet(Integer userId){
    User user = userDatabase.findUser(userId);
    return "Hello " + user.getName();

There is still a coupling between GreetingService and UserDatabase, but it is much looser than before because GreetingService no longer needs to know how a UserDatabase object is created. The coupling is reduced to the necessary minimum. This pattern is called “constructor injection” since we pass the dependencies of a class in the form of constructor parameters.

In a test, we can now create a mock object of type UserDatabase (for example, using a mock library like Mockito) and pass it to the GreetingService. Since we control the behavior of the mock, we no longer need a real database connection to test the GreetingService class.

In the application code, we instantiate the UserDatabase class only once and pass this instance to all the classes that need it. In other words, we “inject” the dependency to UserDatabase into the constructors of other classes.

This “injection” of dependencies can become cumbersome in a real application with hundreds of classes because we need to instantiate all classes in the correct order and explicitly program their dependencies. The result is a lot of “boilerplate” code that often changes and distracts us from the actual development.

This is where Dependency Injection comes into play. A Dependency Injection framework like Spring takes on the task of instantiating most of the classes in our application so that we don’t have to worry about it anymore. It becomes clear that Dependency Injection is a form of Inversion of Control because we hand over control of object instantiation to the Dependency Injection framework.

The division of tasks between Spring and us as developers looks something like this:

  1. We program the classes GreetingService and UserDatabase.
  2. We express the dependency between the two classes through a parameter of type UserDatabase in the constructor of GreetingService.
  3. We instruct Spring to take control over the classes GreetingService and UserDatabase.
  4. Spring instantiates the classes in the correct order to resolve dependencies and creates an object network with an object for each passed class.
  5. When we need an object of type GreetingService or UserDatabase, we ask Spring for that object.

In a real application, Spring manages not just two objects but a complex network of hundreds or thousands of objects. This network is referred to as the “application context” in Spring, as it forms the core of our application.

In the next section, we’ll discuss how Spring’s application context works.

The Spring Application Context

The application context is the heart of an application that is based on the Spring Framework. It contains all the objects whose control we have delegated to Spring. For this reason, it is sometimes also referred to as the “IoC container” (IoC = “Inversion of Control”) or the “Spring container”.

The objects within the application context are called “beans.” If you are not familiar with the Java world, the term “bean” might be somewhat confusing. We can derive the word like this:

  • Spring is a framework for the Java programming language.
  • Java is the name of an island in Indonesia where coffee is grown, and the type of coffee produced there is also called “Java.”
  • Coffee is made from coffee beans.
  • In the Java community, it was decided to call certain objects in Java (the programming language) “Beans.”

It’s a bit far-fetched, but the term has become widely adopted, like it or not.

So, the application context is essentially a network of Java objects known as “beans.” Spring instantiates these beans for us and resolves the dependencies between the beans through constructor injection.

But how does Spring know which beans it should create and manage within its application context? This is where the term “configuration” comes into play.

A configuration in the context of Spring is a definition of the beans required for our application. In the simplest case, this is just a list of classes. Spring takes these classes, instantiates them, and includes the resulting objects (beans) in the application context.

If the instantiation of the classes is not possible (for example, if a bean constructor expects another bean that is not part of the configuration), Spring halts the creation of the application context with an exception.

This is one of the advantages that Spring offers: a faulty configuration usually prevents the application from starting at all, thus avoiding potential runtime issues.

There are several ways to create a Spring configuration. In most use cases, it is convenient and practical to program the configuration in Java. However, in cases where the source code should be completely free from dependencies on the Spring Framework, configuring with XML can also be useful.

Configuring an Application Context with XML

In the early days of Spring, the application context had to be configured with XML. XML configuration allows for complete separation of configuration from the code. The code doesn’t need to be aware that it is managed by Spring.

An example XML configuration looks like this:

<?xml version="1.0" encoding="UTF-8"?> 
  <bean id="userDatabase" class="de.springboot3.xml.UserDatabase"/> 
  <bean id="greetingService" class="de.springboot3.xml.GreetingService"> 
      <constructor-arg ref="userDatabase"/> 

In this configuration, the beans userDatabase and greetingService are defined. Each bean declaration provides instructions to Spring on how to instantiate that bean.

The class UserDatabase has a default constructor without parameters, so it is sufficient to provide Spring with the class name. The class GreetingService has a constructor parameter of type UserDatabase, so we refer to the previously declared userDatabase bean using the constructor-ref element.

With this XML declaration, we can now create an ApplicationContext object:

public class XmlConfigMain { 
  public static void main(String[] args) { 
    ApplicationContext applicationContext = 
      new ClassPathXmlApplicationContext( 
    GreetingService greetingService =  

We pass the XML configuration to the constructor of ClassPathXmlApplicationContext, and Spring creates an ApplicationContext object for us.

This ApplicationContext now serves as our IoC container, and, via the method getBean() we can, for example, inquire about a bean of type GreetingService from it.

While the XML configuration in this example appears quite manageable, it can become more extensive in larger applications. For us as Java developers, it would be more convenient to manage such a comprehensive configuration in Java itself and take advantage of the Java compiler and IDE features.

Java Configuration in Detail

XML configuration is now mostly used in exceptional cases and legacy applications, and configuration with Java has become the standard. Therefore, let’s take a closer look at this approach, also known as “Java config”.

@Configuration and @Bean

The core of a Java configuration is a Java class annotated with the Spring annotation @Configuration:

public class GreetingConfiguration {
	UserDatabase userDatabase() {
	    return new UserDatabase();
	GreetingService greetingService(UserDatabase userDatabase) {
	    return new GreetingService(userDatabase);

This configuration is equivalent to the XML configuration from the previous section. With the @Configuration annotation, we inform Spring that this class contributes to the application context. Without this annotation, Spring remains inactive.

A configuration class can declare factory methods like userDatabase() and greetingService(), each creating an object. With the @Bean annotation, we mark such factory methods. Spring finds these methods and calls them to create an ApplicationContext.

Dependencies between beans, such as the dependency of GreetingService on UserDatabase, are resolved through parameters of the factory methods. In our case, Spring first calls the method userDatabase() to create a UserDatabase bean and then passes it to the method greetingService() to create a GreetingService bean.

Using the AnnotationConfigApplicationContext class, we can then create an ApplicationContext:

public class JavaConfigMain { 
  public static void main(String[] args) {  
    ApplicationContext applicationContext 
      = new AnnotationConfigApplicationContext(GreetingConfiguration.class);
    GreetingService greetingService 
      = applicationContext.getBean(GreetingService.class); 

The constructor of AnnotationConfigApplicationContext allows us to pass multiple configuration classes instead of just one. This is helpful for larger applications because we can split the configuration of many beans into multiple configuration classes to maintain clarity.

@Component and @ComponentScan

Configuring hundreds or even thousands of beans via Java for a large application can become tedious. To simplify this, Spring offers the ability to “scan” for beans in the Java classpath.

This scanning is activated using the @ComponentScan annotation:

public class GreetingScanConfiguration {

As before, we create a configuration class (annotated with @Configuration). However, instead of defining the beans ourselves as factory methods annotated with the @Bean annotation, we add the new @ComponentScan annotation.

With this annotation, we instruct Spring to scan the de.springboot3 package for beans. If the scan finds a class annotated with @Component, it will create a bean from that class (i.e., the class will be instantiated and added to the Spring application context).

Therefore, we simply annotate all classes for which Spring should create a bean with the @Component annotation:

public class GreetingService {
	private final UserDatabase userDatabase;
	public GreetingService(UserDatabase userDatabase) {
	    this.userDatabase = userDatabase;

public class UserDatabase { 
  // ... 

As before, dependencies between beans are expressed through constructor parameters, and Spring resolves these automatically.

@Bean vs. @Component

The annotations @Bean and @Component express a similar concept: both mark a contribution to the Spring application context. This similarity can be confusing, especially at the beginning.

The Java compiler helps here a bit, as the @Bean annotation is only allowed on methods, and the @Component annotation is only allowed on classes. So, we cannot confuse them. However, we can still annotate methods and classes that Spring doesn’t recognize!

Spring evaluates the @Bean annotation only within a @Configuration class, and the @Component annotation only on classes found by a component scan.

Combining @Configuration and @ComponentScan

Spring does not dictate how we should configure the beans of our application. We can configure them using XML or Java config or even combine both approaches. We can also combine explicit bean definitions using @Bean methods with a scan using @ComponentScan:

class MixedConfiguration {
	GreetingService greetingService(UserDatabase userDatabase) {
	    return new GreetingService(userDatabase);

// no @Component annotation!
class GreetingService {...}

class UserDatabase {...}

In this configuration, Spring creates a bean of type UserDatabase because the class is annotated with @Component, and a @ComponentScan is configured. On the other hand, the bean of type GreetingService is defined through the explicit @Bean-annotated factory method.

Modular Configuration

Configuring a larger application with hundreds of beans can quickly become confusing.

The explicit configuration using @Bean annotations has the advantage that the configuration of beans is bundled in a few @Configuration classes and is easy to understand.

The implicit configuration using @ComponentScan and @Component has the advantage that we don’t need to define each bean ourselves, but it can be spread over many @Component annotations and, therefore, over the entire codebase, making it more challenging to grasp.

A proven principle is to design the Spring configuration along the architecture of the application. Each module of the application should reside in its own package and have its own @Configuration class. In this @Configuration class, we can either configure a @ComponentScan for the module package or use explicit configuration using @Bean methods. To bring the modules together into a complete application, we create a parent @Configuration class that defines a @ComponentScan for the main package. This scan will pick up all @Configuration classes in this and the sub-packages.

What benefits does the Spring Container give us?

Now we know that Spring offers us an IoC container that instantiates and manages objects (beans) for us. This saves us from the burden of managing the lifecycle of these objects ourselves.

But that’s just the beginning. Since Spring has control over all beans, it can perform many other tasks for us. For example, Spring can intercept calls to bean methods to start or commit a database transaction. We can also use Spring as an event bus. We send an event to the Spring container, and Spring forwards the event to all interested beans.

We will delve into these and many other features throughout the rest of the book. The foundation of all these features is the Spring programming model, whose core we have already learned in this chapter. This programming model is a combination of annotations, conventions, and interfaces that we can assemble into a complete application.


Since Spring manages all beans defined by us, the framework can send messages to these beans. We can use this to develop an event mechanism that loosely couples our components. This is just one of the benefits that the Spring container gives us.

Loose Coupling

When a software component requires the functionality of another component, the two components are “coupled” together.

This coupling can vary. For example, a class may call a method of another class to access its functionality. This couples the two classes at compile time. We refer to this as “strong coupling.”

The strength of the coupling also depends on how extensive and complicated the signature of the called method is – if it’s a simple method without parameters, the coupling is not as strong as if the method expects a series of complex parameters. Once we make a change to the types of these parameters, we have to modify the code of both the calling and the called class.

Coupling is not inherently bad. Sometimes, two classes need to work very closely together to fulfill a function.

Most of the time, we want to design our code modularly. When working on a module, we don’t want to have to think about all the other modules of the application. This is only possible if the dependencies between the modules are limited.

Loosely coupled modules allow for parallel development. Each module can be developed by a different developer or even team. This is not possible if the modules are heavily coupled because then the teams would step on each other’s feet.

Let’s take the example of a banking application that implements use cases from two different domains. The first module implements the “User” domain. It manages user data and serves as the single source of truth for this data. Another module implements the “Transactions” domain. This module implements functions related to money transfers.

Every time the “Transactions” module initiates a transfer, it needs to check whether the user initiating the transfer is locked or not. The transfer is only executed if the user is not locked.

The “Transactions” module could always directly call the “User” module before performing a transfer and ask if the user is locked or not. However, this would strongly couple the “Transactions” module to the “User” module. Whenever we make a change to the “User” module, we may also have to adjust the code in the “Transactions” module. Additionally, in the future, we may want to extract the “Transactions” module into its own (micro)service so that it can be released independently of the “User” module.

Events provide a solution to loosely couple both modules. Every time a new user is registered, locked, or unlocked, the “User” module sends an event. The “Transactions” module listens to these events and updates its own database with the current status of each respective user. Before performing a transfer, the “Transactions” module can now check its own database to see if the user is locked or not, instead of having to make a request to the user module. The data storage is completely decoupled from the “User” module.

Using this example, we want to explore how we can implement such an event mechanism with Spring and Spring Boot.

Sending Events

The prerequisite for sending and receiving events with Spring is that both the sender and the receiver must be registered as beans in the ApplicationContext.

We can define the events themselves as simple Java classes or records. For example, we can write our user events as follows:

public record UserCreatedEvent(User user) {}

public record UserLockedEvent(int userId) {}

public record UserUnlockedEvent(int userId) {}

To send such an event, we can use the ApplicationEventPublisher interface. Conveniently, Spring automatically provides a bean that implements this interface in the ApplicationContext. So we can simply inject it into our UserService:

public class UserService {

    private final ApplicationEventPublisher applicationEventPublisher;

    public UserService(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;

    public void createUser(User user) {
        // ... business logic omitted
        this.applicationEventPublisher.publishEvent(new UserCreatedEvent(user));

    public void lockUser(int userId) {
        // ... business logic omitted
        this.applicationEventPublisher.publishEvent(new UserLockedEvent(userId));

    public void unlockUser(int userId) {
        // ... business logic omitted
        this.applicationEventPublisher.publishEvent(new UserUnlockedEvent(userId));

We simply call the publishEvent() method. That’s all we need to do to send an event.

Receiving Events

There are several ways to respond to events in Spring. We can implement the ApplicationListener interface or use the @EventListener annotation.


The conventional way to respond to an event in a Spring application is by implementing the ApplicationListener interface:

public class UserCreatedEventListener implements ApplicationListener<UserCreatedApplicationEvent> {

    private static final Logger logger 
      = LoggerFactory.getLogger(UserCreatedEventListener.class);

    private final TransactionDatabase database;

    public UserCreatedEventListener(TransactionDatabase database) {
        this.database = database;

    public void onApplicationEvent(UserCreatedApplicationEvent event) {
        this.database.saveUser(new User(event.getUser().id(), User.UNLOCKED));
        logger.info("received event: {}", event);

The UserCreatedEventListener class is part of the “Transactions” module. It has access to an object of type TransactionDatabase, which simulates the module’s database.

We implement the onApplicationEvent() method, which, in our case, takes an object of type UserCreatedApplicationEvent. The event contains a User object. The listener takes the user’s data that the “Transactions” module needs and saves it in the database. Users are unlocked by default, so we pass the UNLOCKED status.

Why do we use the event class UserCreatedApplicationEvent instead of the record UserCreatedEvent we learned about earlier?

Because Spring’s ApplicationListener can only handle events of type ApplicationEvent. This means that every event we send must inherit from this class:

public class UserCreatedApplicationEvent extends ApplicationEvent {

    private final User user;

    public UserCreatedApplicationEvent(Object source, User user) {
        this.user = user;

    public User getUser() {
        return user;

As we can see, it is somewhat cumbersome to receive events with an ApplicationListener. On the one hand, our event must inherit from the ApplicationEvent class, and on the other hand, we must implement the ApplicationListener interface, which can only respond to a single type of events. To receive other event types, we would have to write additional ApplicationListeners or implement a large if/else block in the onApplicationEvent() method.

The Spring team recognized this and added an annotation-based event mechanism to Spring.


To respond to an event, we can also use the @EventListener annotation. If Spring finds this annotation on a method of a bean registered in the ApplicationContext, Spring automatically sends all events of the corresponding type to this method.

public class UserEventListener {

    private final TransactionDatabase database;

    public UserEventListener(TransactionDatabase database) {
        this.database = database;

    public void onUserCreated(UserCreatedEvent event) {
        this.database.saveUser(new User(event.user().id(), User.UNLOCKED));

The UserEventListener class is added to the Spring ApplicationContext using the @Component annotation. The onUserCreated() method is annotated with @EventListener and accepts an object of our simple record type UserCreatedEvent.

The event does not have to inherit from the ApplicationEvent type, as in the example with an ApplicationListener! Spring internally wraps our event in an ApplicationEvent, but we don’t notice it. We can receive any type as an event, but immutable records are best suited for transporting events.

Synchronous or asynchronous?

If we use events as shown in the examples, our “Users” and “Transactions” modules are decoupled at compile time. The only coupling between the modules are the event objects, which both modules must know.

But how are our modules coupled at runtime? What happens if our EventListener in the “Transactions” module produces an exception when receiving an event or takes a long time to process an event? Can and should we react to this in our “Users” module?

By default, the Spring Event mechanism works synchronously, as shown in the following diagram:

The UserService sends an event to the ApplicationEventPublisher. This knows which event listeners are interested in the event, and calls them one after the other (in our case, it calls only the UserEventListener). Only when the onUserCreated() method has completed its processing does the control flow return to the UserService. The processing takes place synchronously.

This means that if the UserEventListener.onUserCreated() method throws an exception, it arrives in the UserService and interrupts the processing there. Or if the method takes a long time, the control flow in the UserService is interrupted for that long.

So our modules are not as decoupled as we had hoped! We have to adapt our exception handling and make our code more robust so that it can handle long waiting times if necessary.

Can we also process events asynchronously to reduce this coupling, as shown in the following sequence diagram?

The short answer is “yes.” We can simply annotate the listener method with the @Async annotation:

public class UserEventListener {

    public void onUserCreated(UserCreatedEvent event) {
        // …

This prompts Spring to return control flow directly to the caller and execute the method in a separate thread in the background.

For the @Async annotation to take effect, we must first activate it. We do this using the @EnableAsync annotation on one of our @Configuration classes.

Since events are now processed asynchronously, exceptions in the event listener are no longer forwarded to the sender of the event. Depending on the use case, this may be desired or undesired.

We should be aware that decoupling using the @Async annotation only scales to a limited extent. If we process a large number of events, they will accumulate in Spring’s internal thread pool and be processed one after the other. In synchronous event processing, scaling problems become apparent more quickly, as the entire processing chain slows down instead of just the event processing.

Spring Boot’s Application Events

During the lifecycle of an application, Spring Boot sends some events that we can respond to. Most of these events are very technical and are rarely relevant to us as application developers. The following table briefly lists these events in chronological order:

Event Description
ApplicationStartingEvent The application is currently starting, but the Environment and ApplicationContext are not yet available.
ApplicationEnvironmentPreparedEvent The application is currently starting, and the Spring Environment is available.
ApplicationContextInitializedEvent The application is currently starting, and the Spring ApplicationContext is available.
ApplicationPreparedEvent A combination of the previous two events. The ApplicationContext and Environment are available.
ContextRefreshedEvent The ApplicationContext has been updated. This happens when the ApplicationContext starts and when it is reloaded (for example, after a configuration change).
WebServerInitializedEvent The embedded web server (Tomcat by default) has started.
ApplicationStartedEvent The application has started, but no ApplicationRunners and CommandLineRunners have been executed yet.
ApplicationReadyEvent The application is fully started.
ApplicationFailedEvent The application did not start due to an error.

The most relevant event for us is probably the ApplicationReadyEvent, as it is fired when the application is ready to work. We can use it, for example, to activate certain components in our application.

If our application processes messages from a queue, for example, we want to make sure that our application is also ready to process these messages. This is the case as soon as we have received the ApplicationReadyEvent.

We cannot react to some of the earlier events from the table in the “normal” way, as they are fired very early. If we want to react to these events, we must manually register an ApplicationListener:

public class EventsApplication {

    public static void main(String[] args) {
        SpringApplication springApplication =
                new SpringApplication(EventsApplication.class);
        springApplication.addListeners(new MyApplicationListener());
Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Merge Sort in Kotlin

Sorting is a fundamental operation that plays a crucial role in various applications. Among the many sorting algorithms, merge sort stands out for its efficiency and simplicity.

Read more

Extension Functions in Kotlin

One of Kotlin’s standout features is extension functions, a mechanism that empowers developers to enhance existing classes without modifying their source code.

Read more

Use Cases for Java Records

Java Records introduce a simple syntax for creating data-centric classes, making our code more concise, expressive, and maintainable. In this guide, we’ll explore the key concepts and practical applications of Java Records, providing a step-by-step guide to creating records and sharing best practices for using them effectively in projects.

Read more