Skip to content
SparkTime, home
Java From Zero to Job-Ready
Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
12 · Interviews and Readiness
Lesson 32 of 41

Lesson 32 · Web and Spring

Spring Boot Foundations: Starters, Auto-Configuration, Properties and Profiles

Learn what Spring Boot adds to Spring: the project structure, starters, auto-configuration and how it backs off, the embedded server, typed configuration with validation, profiles and the order in which configuration sources win, all run on a real Spring Boot 4.1 application.

On this page

Key takeaways

  • Spring Boot is Spring plus defaults. A starter is one dependency that pulls in a consistent set of libraries, and auto-configuration creates the beans those libraries need when it sees them on the classpath.
  • Auto-configuration backs off: if you define a bean of the same type, yours wins. Running with --debug prints a conditions report that says exactly what matched and why.
  • An application is a runnable jar with an embedded web server, started with java -jar.
  • Configuration comes from many sources with a fixed order of precedence. In the run shown here, a command-line argument beat an environment variable, which beat application.properties, and a profile file beat the default file.
  • Bind settings to a typed record with @ConfigurationProperties and validate them, so a bad value stops the application at start-up with a precise message instead of failing later in production.

In the last article you saw the Spring container in its bare form: you gave it classes and it built a connected set of objects. A real application needs much more: a web server, JSON conversion, a database connection pool, logging, configuration files and a way to package and run it. Each of those could take a day to set up by hand. Spring Boot sets them up for you, with defaults you can override. This article shows how, using a small application that was built and run on Spring Boot 4.1.1.

What you will learn

#
  • What Spring Boot adds on top of Spring, and how a project is laid out.
  • Starters and auto-configuration, and how to see what was configured.
  • How to configure an application: properties, typed settings, profiles and precedence.
  • How to run it, and what to look for in the start-up output.

What Spring Boot is

#

Spring Boot is an opinionated layer on the Spring Framework. It gives you:

Feature

What it does for you

Starters

One dependency per capability (web, data, security), with versions that work together

Auto-configuration

Creates the beans a library needs, from what is on the classpath and in the configuration

Embedded server

Tomcat runs inside your application, so you run a program, not deploy to a server

Externalised configuration

Properties from files, environment variables and the command line, bound to Java objects

Production features

Health checks, metrics and logging setup (Actuator, article 38)

Boot does not change how Spring works. Everything it creates is an ordinary bean in the container from the last article.

Creating a project

#

The usual way is Spring Initializr (start.spring.io, also built into IntelliJ IDEA and VS Code): you choose Maven, the language, the Spring Boot version, the Java version and the dependencies, and it generates a project. (The Initializr website was not used while verifying this course. The project below was created by hand with the same structure, which is all Initializr produces.)

Code
foundations/├── pom.xml└── src/    ├── main/    │   ├── java/com/example/foundations/    │   │   ├── FoundationsApplication.java      the main class    │   │   ├── ExpenseSettings.java             typed configuration    │   │   └── HelloController.java             a web endpoint    │   └── resources/    │       ├── application.properties           default configuration    │       └── application-dev.properties       extra configuration for the "dev" profile    └── test/java/com/example/foundations/        └── ExpenseSettingsTest.java

The POM

#
XML
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  <modelVersion>4.0.0</modelVersion>  <parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>4.1.1</version>    <relativePath/>  </parent>  <groupId>com.example</groupId>  <artifactId>foundations</artifactId>  <version>0.0.1-SNAPSHOT</version>  <name>foundations</name>  <properties>    <java.version>21</java.version>  </properties>  <dependencies>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-webmvc</artifactId>    </dependency>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-validation</artifactId>    </dependency>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-webmvc-test</artifactId>      <scope>test</scope>    </dependency>  </dependencies>  <build>    <plugins>      <plugin>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-maven-plugin</artifactId>      </plugin>    </plugins>  </build></project>

Look at what is not there: no versions on the dependencies, and no list of dependencies for a web server, JSON, validation and logging. Two things do that work:

  • The parent POM spring-boot-starter-parent manages the versions of a very large number of libraries, so those that need to work together always do. It also sets sensible Maven defaults, such as the Java version from java.version.
  • Starters are dependencies whose only job is to pull in a set of others. spring-boot-starter-webmvc brings Spring MVC, an embedded Tomcat, and Jackson for JSON. spring-boot-starter-validation brings Bean Validation. spring-boot-starter-webmvc-test brings the testing libraries and is in test scope (article 23).

Some starters you will use in this course:

Starter

Purpose

spring-boot-starter-webmvc

Web applications and REST APIs with Spring MVC and Tomcat

spring-boot-starter-validation

Bean Validation (@NotBlank, @Min, ...)

spring-boot-starter-data-jpa

JPA and Hibernate with Spring Data repositories

spring-boot-starter-jdbc

JDBC with a connection pool and JdbcClient

spring-boot-starter-security

Spring Security

spring-boot-starter-actuator

Health, metrics and management endpoints

spring-boot-starter-webmvc-test, spring-boot-starter-data-jpa-test, ...

Test support for each area

Older style. Earlier Spring Boot versions named the web starter spring-boot-starter-web. In Spring Boot 4.1.1 that artifact still exists, but its own description says it is deprecated in favor of spring-boot-starter-webmvc. Many tutorials and existing projects still use the old name, so you will see both. Also, Boot 4 uses Jackson 3 (the tools.jackson packages) where Boot 3 used Jackson 2 (com.fasterxml.jackson), so older tutorial code that imports ObjectMapper from com.fasterxml.jackson.databind needs adjusting.

The spring-boot-maven-plugin is what turns the build output into a runnable fat jar, so mvn package produces a jar you start with java -jar.

The main class

#
Java
// File: src/main/java/com/example/foundations/FoundationsApplication.javapackage com.example.foundations;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.context.properties.ConfigurationPropertiesScan;@SpringBootApplication@ConfigurationPropertiesScanpublic class FoundationsApplication {    public static void main(String[] args) {        SpringApplication.run(FoundationsApplication.class, args);    }}

@SpringBootApplication combines three things: @SpringBootConfiguration (this is a configuration class), @EnableAutoConfiguration (turn on auto-configuration), and @ComponentScan (find @Component, @Service, @RestController and so on). The scan starts in the package of this class and goes down, which is why the main class lives in the root package of the application: classes in com.example.foundations and its sub-packages are found, and a class in com.example.other is not. SpringApplication.run creates the container, performs the auto-configuration, starts the web server and returns. @ConfigurationPropertiesScan is explained below.

Auto-configuration

#

When the container starts, Boot looks at what is on the classpath, what beans you have defined and what the configuration says, and creates beans accordingly. Because we added spring-boot-starter-webmvc, Boot configured a web server, the Spring MVC dispatcher and a JSON mapper without a line of configuration from us.

Auto-configuration is conditional, and the conditions are what make it safe to use. The most common:

Condition

Meaning

@ConditionalOnClass

Only if a certain class is on the classpath (the library was added)

@ConditionalOnMissingBean

Only if you have not defined a bean of this type. This is how you override a default: define your own and Boot backs off

@ConditionalOnProperty

Only if a property has a certain value

You can ask Boot to explain itself: run the application with --debug to print a conditions evaluation report of everything it considered and why it did or did not apply. These excerpts are from the real report of the application above (the whole report lists dozens of entries):

Code
Positive matches:-----------------   WebMvcAutoConfiguration matched:      - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'org.springframework.web.servlet.DispatcherServlet', 'org.springframework.web.servlet.config.annotation.WebMvcConfigurer' (OnClassCondition)      - found 'session' scope (OnWebApplicationCondition)      - @ConditionalOnMissingBean (types: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; SearchStrategy: all) did not find any beans (OnBeanCondition)   JacksonAutoConfiguration#jacksonJsonMapper matched:      - @ConditionalOnMissingBean (types: tools.jackson.databind.json.JsonMapper; SearchStrategy: all) did not find any beans (OnBeanCondition)Negative matches:-----------------   AopAutoConfiguration.AspectJAutoProxyingConfiguration:      Did not match:         - @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice' (OnClassCondition)

Read it like this:

  • WebMvcAutoConfiguration matched because three classes were found on the classpath (@ConditionalOnClass found required classes, which exist because of the web starter) and the context is a web application. It also checked @ConditionalOnMissingBean ... did not find any beans: nobody had defined their own, so Boot's configuration was applied.
  • JacksonAutoConfiguration#jacksonJsonMapper creates the JSON mapper only because no JsonMapper bean existed. If you define your own JsonMapper bean, this condition fails, Boot backs off, and yours is used. That is the override mechanism for almost everything in Boot.
  • AopAutoConfiguration.AspectJAutoProxyingConfiguration did not match, because org.aspectj.weaver.Advice is not on the classpath: the library that this feature needs was not added.

This report is the first place to look when something is or is not configured and you do not know why. A typical mystery is a missing bean that "should" exist, and the report will show a failed condition such as a missing class or a missing property.

Configuration

#

Hard-coding values such as ports, limits and URLs in Java is wrong for the reasons in article 24 and article 28: they differ per environment and some are secret. Spring Boot reads configuration from many places and gives your code the result.

Properties files

#

application.properties (or application.yml) in src/main/resources is read automatically. Ours holds the defaults:

Code
expenses.currency=EURexpenses.max-page-size=50expenses.greeting=Hello from the default configuration

Keys use lower-case words separated by dots, with hyphens inside a word (max-page-size). Boot's relaxed binding accepts several spellings of the same key, so maxPageSize, max-page-size and (in an environment variable) MAXPAGESIZE all refer to the same setting.

Typed settings with @ConfigurationProperties

#

You can read one value with @Value("${expenses.currency}"), but for a group of related settings it is better to bind them to a typed object. A record is ideal, because it is immutable (article 9):

Java
// File: src/main/java/com/example/foundations/ExpenseSettings.javapackage com.example.foundations;import jakarta.validation.constraints.Max;import jakarta.validation.constraints.Min;import jakarta.validation.constraints.NotBlank;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.context.properties.bind.DefaultValue;import org.springframework.validation.annotation.Validated;/** Settings read from properties that start with "expenses.", checked when the application starts. */@ConfigurationProperties(prefix = "expenses")@Validatedpublic record ExpenseSettings(        @NotBlank String currency,        @DefaultValue("20") @Min(1) @Max(100) int maxPageSize,        @DefaultValue("Hello") String greeting) {}
  • @ConfigurationProperties(prefix = "expenses") binds every property that starts with expenses. to the record's components.
  • @DefaultValue supplies a value when the property is absent.
  • @Validated with the Bean Validation annotations means the values are checked when the application starts. A maxPageSize of 0 does not produce a mysterious failure hours later. It stops the application at once.
  • @ConfigurationPropertiesScan on the main class finds this record and registers it as a bean, so the controller can receive it through its constructor like any other bean.

The advantages over @Value: one typed object, validation, defaults in one place, and IDE support for the keys.

A controller that uses them

#
Java
// File: src/main/java/com/example/foundations/HelloController.javapackage com.example.foundations;import org.springframework.core.env.Environment;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloController {    public record Greeting(String message) {    }    public record SettingsView(String currency, int maxPageSize, String activeProfiles) {    }    private final ExpenseSettings settings;    private final Environment environment;    public HelloController(ExpenseSettings settings, Environment environment) {        this.settings = settings;        this.environment = environment;    }    @GetMapping("/hello")    public Greeting hello(@RequestParam(defaultValue = "world") String name) {        return new Greeting(settings.greeting() + ", " + name + "!");    }    @GetMapping("/settings")    public SettingsView settings() {        return new SettingsView(settings.currency(), settings.maxPageSize(),                String.join(",", environment.getActiveProfiles()));    }}

@RestController marks a web controller whose return values are written as the response body. Here the records are converted to JSON by Jackson, and @GetMapping maps a URL to a method (article 33 develops all this). The constructor receives the settings and the Spring Environment, plain constructor injection (article 31).

Profiles

#

A profile is a named set of configuration for an environment. application-dev.properties is read in addition to the default file when the dev profile is active, and its values win:

Code
expenses.currency=USDexpenses.greeting=Hello from the dev profile

Activate a profile with spring.profiles.active=dev (a property, an environment variable or a command-line argument). You can use profiles to select configuration for development, testing and production, and to switch beans on and off with @Profile("dev"). Keep secrets out of the files whatever the profile: they belong in environment variables or a secret manager, because the files are committed (article 24).

Running it

#

Build with mvn clean verify and start the jar. The run below used --server.port=18080, which is itself a configuration property given on the command line, so that it would not clash with anything on the machine (the default port is 8080). Spring Boot prints a lot of log lines at start-up. These are two of the important ones, with the time stamp and process id removed:

Code
c.e.foundations.FoundationsApplication   : No active profile set, falling back to 1 default profile: "default"o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 18080 (http) with context path '/'

No active profile set tells you which configuration was used, and Tomcat started on port 18080 (http) tells you where to send requests. The application then runs until stopped. Here are the requests and responses, from real runs on JDK 21 and JDK 25 (identical):

Code
$ curl -s http://localhost:18080/hello{"message":"Hello from the default configuration, world!"}$ curl -s http://localhost:18080/hello?name=Ada{"message":"Hello from the default configuration, Ada!"}$ curl -s http://localhost:18080/settings{"currency":"EUR","maxPageSize":50,"activeProfiles":""}$ curl -s -o /dev/null -w "%{http_code}" http://localhost:18080/missing404$ curl -s -i http://localhost:18080/settings   (first line and content type only)HTTP/1.1 200 Content-Type: application/json

The JSON in the first three responses was produced from Java records, and the 404 for /missing is Boot's default handling of an unknown URL. The Content-Type: application/json header was added automatically.

Which source wins

#

When the same setting appears in several places, one has to win, and Boot defines a fixed order of precedence. These runs show the part of it you will use most. Each started the same jar with a different configuration source for expenses.max-page-size (default file: 50):

How it was set

Result in /settings

Only application.properties (50)

maxPageSize: 50

Environment variable EXPENSES_MAXPAGESIZE=75

75

The same environment variable and also --expenses.max-page-size=30 on the command line

30

Code
$ curl -s http://localhost:18080/settings{"currency":"EUR","maxPageSize":75,"activeProfiles":""}
Code
$ curl -s http://localhost:18080/settings{"currency":"EUR","maxPageSize":30,"activeProfiles":""}

So the command line beats an environment variable, which beats the properties file. That ordering is deliberate: the more specific and the more temporary the source, the higher it ranks. It lets one jar run unchanged in every environment, with the differences supplied from outside (twelve-factor style), which is how containers and cloud platforms configure applications (article 38). Boot's documentation lists the complete order, which has many levels, so check it if a value is not what you expect.

The dev profile shows the other layering, where a profile file overrides the default file, while the keys it does not mention (max-page-size) keep their default:

Code
$ curl -s http://localhost:18080/hello{"message":"Hello from the dev profile, world!"}$ curl -s http://localhost:18080/settings{"currency":"USD","maxPageSize":50,"activeProfiles":"dev"}

Fail fast on bad configuration

#

Now the validation. The same jar started with --expenses.max-page-size=0 (the record requires at least 1) refuses to start, and Boot prints a failure analysis instead of a stack trace:

Code
APPLICATION FAILED TO START***************************Description:Binding to target com.example.foundations.ExpenseSettings failed:    Property: expenses.maxPageSize    Value: "0"    Origin: "expenses.max-page-size" from property source "commandLineArgs"    Reason: must be greater than or equal to 1Action:Update your application's configuration

The message names the property, the value, where the value came from (the command line), and the rule it broke. That is an ideal failure: immediate, at deployment, with the cause spelled out. Compare it with an application that starts happily and breaks when the first request needs the value.

Testing the configuration

#

@SpringBootTest starts the whole application context inside a test (article 37 covers faster, narrower tests). This one checks that the properties are bound as expected:

Java
// File: src/test/java/com/example/foundations/ExpenseSettingsTest.javapackage com.example.foundations;import static org.junit.jupiter.api.Assertions.assertEquals;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclass ExpenseSettingsTest {    @Autowired    ExpenseSettings settings;    @Test    void settingsAreBoundFromApplicationProperties() {        assertEquals("EUR", settings.currency());        assertEquals(50, settings.maxPageSize());    }}

mvn clean verify ran it with Tests run: 1, Failures: 0, Errors: 0, Skipped: 0. Test classes may use @Autowired on a field, since the test framework instantiates them. In production classes, use constructor injection.

Other things Boot gives you

#
  • Logging: Spring Boot logs through SLF4J, with Logback by default, and its documentation lists the levels ERROR, WARN, INFO (the default), DEBUG and TRACE. Set levels in properties, for example logging.level.root=warn or logging.level.org.hibernate=error (article 22).
  • DevTools (spring-boot-devtools): automatic restart when code changes, during development only.
  • Actuator (spring-boot-starter-actuator): /actuator/health and metrics, used by monitoring and by container platforms (article 38).
  • Running from Maven: mvn spring-boot:run starts the application without building a jar. (This article's runs used java -jar, which is how production runs it. The Maven goal was not run here.)

Common mistakes

#
  • Putting the main class in a sub-package, or beans in a package outside it, so component scanning misses them.
  • Defining a second bean of a type that Boot already provides and being surprised by the result. Know whether you are replacing a default (Boot backs off) or adding a second candidate (ambiguity, article 31).
  • Committing secrets in application.properties, or in a profile file.
  • Using @Value strings scattered across the code instead of one validated settings object.
  • Not looking at the conditions report when auto-configuration surprises you.
  • Copying old tutorials without checking versions: spring-boot-starter-web, javax.* imports and Jackson 2 packages are all from earlier generations.
  • Treating application.properties as the only source, then being confused when an environment variable or a command-line argument changes the result.
  • Running with a different profile in production than in tests, so the configuration that matters was never exercised.

Exercises

#

Exercise 1

Predict the winner

#

application.properties sets expenses.currency=EUR, the prod profile file sets it to GBP, and the environment variable EXPENSES_CURRENCY=CHF is set. The application runs with --spring.profiles.active=prod. Which value is used, using what you saw above?

Exercise 2: Read the report

#

You added a class named JsonMapperConfig that defines your own JsonMapper bean. What happens to Boot's jacksonJsonMapper configuration, and why?

Exercise 3: Add a setting

#

You need a new setting expenses.export-folder, which must not be blank, with the default exports. Which two changes do you make in ExpenseSettings, and what is the property key?

Exercise 4: Why not hard-code?

#

A teammate puts the database password in application.properties "just for now". Give two concrete problems and a better approach.

Exercise 5: Missing bean

#

Your @Service class is in the package com.example.shared and the main class is in com.example.app. The application fails with a missing bean. Why, and what are two fixes?

Show solutionHide solution

The profile file overrides the default file, which gives GBP, but an environment variable beats the properties files (the run above showed the environment variable beating application.properties), so with EXPENSES_CURRENCY=CHF set, the value is CHF. If the application were also started with --expenses.currency=USD, the command line would win and the value would be USD.

Exercise 2

Show solutionHide solution

Boot's JacksonAutoConfiguration#jacksonJsonMapper is conditional on @ConditionalOnMissingBean (types: tools.jackson.databind.json.JsonMapper). Once your own JsonMapper bean exists, that condition no longer holds, so Boot backs off and does not create its mapper. Your bean is the one that the application uses, including in the web layer. Run with --debug to confirm in the conditions report, where the entry moves to the negative matches with the reason.

Exercise 3

Show solutionHide solution

Add a record component with its annotations, @DefaultValue("exports") @NotBlank String exportFolder, and the property key is expenses.export-folder (or expenses.exportFolder, thanks to relaxed binding). @NotBlank makes an empty value fail at start-up, and @DefaultValue provides exports when nothing is configured. In an environment variable it would be EXPENSES_EXPORTFOLDER.

Exercise 4

Show solutionHide solution

(1) The file is committed to Git and copied into jars, so the password is in the repository history and in every build artifact, and anyone with read access to either has it. (2) It is the same password in every environment, so a developer's laptop and production share a secret, and it cannot be changed without rebuilding. A better approach is an environment variable (such as DB_PASSWORD) or a secret manager, read through configuration (for example spring.datasource.password=${DB_PASSWORD}), with the properties file holding only non-secret defaults. If a password was committed, treat it as compromised and rotate it (article 24).

Exercise 5

Show solutionHide solution

Component scanning starts at the package of the main class, com.example.app, and only scans that package and its sub-packages. com.example.shared is a sibling, so it is never scanned and the @Service is not a bean. Fixes: move the class (or the main class) so the service is under the main class's package, or widen the scan explicitly, for example @SpringBootApplication(scanBasePackages = {"com.example.app", "com.example.shared"}), or move the main class up to com.example.

Where to go next

#

Sources & further reading

  1. docs.spring.io
  2. docs.spring.io
  3. docs.spring.io
  4. docs.spring.io
  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Technology

    Dynamic Programming Practice: From 1D to 2D

  • Course · Technology

    DSA From Zero to Job-Ready: A Practical Guide with Java