Skip to main content
Java · lesson 49 of 66
Open the Java page →
Java intermediate Lesson 49 of 66

Spring Boot — Introduction

Build and run a Spring Boot 3 app: the entry point, a REST controller, application.properties, command-line overrides, profiles and Actuator health.

By SkillByExample · Updated

Spring Boot is the standard way to build Java backend services. It sits on top of Spring Framework and makes the configuration decisions for you based on what is on the classpath: add the web starter and you get an embedded Tomcat, JSON serialisation and an MVC dispatcher without writing any setup. Every example below is one Maven project built with Spring Boot 3.5 on JDK 21, packaged with mvn package and started with java -jar; the curl calls ran against the live app.

The project

start.spring.io generates this skeleton; the only parts that matter are the parent POM, which pins compatible versions of every Spring library, and the starters you depend on.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.16</version>
</parent>
<properties>
    <java.version>21</java.version>
</properties>
<dependencies>
    <dependency>  <!-- Spring MVC + embedded Tomcat + Jackson -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>  <!-- /actuator/health and friends -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>
<!-- build/plugins: spring-boot-maven-plugin repackages the JAR as executable -->
src/main/java/com/example/demo/DemoApplication.java   entry point
src/main/java/com/example/demo/HelloController.java   REST endpoints
src/main/resources/application.properties             configuration
pom.xml

A starter is a curated bundle: spring-boot-starter-web alone brings in Spring MVC, Tomcat and Jackson at versions the parent POM has already tested together, so no <version> tags appear on them.

The entry point and a first endpoint

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);
    }
}
package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class HelloController {

    @Value("${app.greeting:Hello}")      // property, with "Hello" as the fallback
    private String greeting;

    @GetMapping("/hello")
    public String hello() {
        return greeting + ", Spring Boot!";
    }

    @GetMapping("/hello/{name}")
    public String helloName(@PathVariable String name) {
        return greeting + ", " + name + "!";
    }

    record Greeting(String message, int length) {}

    @GetMapping("/greeting")              // a record is serialised to JSON by Jackson
    public Greeting greeting(@RequestParam(defaultValue = "World") String name) {
        return new Greeting(greeting + ", " + name, name.length());
    }
}
$ mvn -q -B package -DskipTests
$ java -jar target/demo-0.0.1-SNAPSHOT.jar
...
 :: Spring Boot ::               (v3.5.16)

2026-10-07T21:56:12.814Z  INFO 1942 --- [demo] [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.12.1 with PID 1942 (/tmp/sb/intro/target/demo-0.0.1-SNAPSHOT.jar started by root in /tmp/sb/intro)
2026-10-07T21:56:12.816Z  INFO 1942 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2026-10-07T21:56:13.708Z  INFO 1942 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
...
2026-10-07T21:56:14.234Z  INFO 1942 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2026-10-07T21:56:14.245Z  INFO 1942 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.796 seconds (process running for 2.152)
$ curl -s -w '\n' localhost:8080/api/hello
Hello, Spring Boot!
$ curl -s -w '\n' localhost:8080/api/hello/Alice
Hello, Alice!
$ curl -s -w '\n' 'localhost:8080/api/greeting?name=Ada'
{"message":"Hello, Ada","length":3}
$ curl -s -w '\n' localhost:8080/api/greeting
{"message":"Hello, World","length":5}
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/api/nope
404

Nothing configured Tomcat, the port or JSON — @SpringBootApplication turned on auto-configuration, which saw spring-boot-starter-web on the classpath and set all three up. Component scanning found HelloController because it lives in the same package as DemoApplication; a controller in a sibling package such as com.example.other would silently never be registered. A String return value is written as plain text, while the record becomes JSON with its components as keys, in declaration order.

application.properties and overriding it

Configuration lives in src/main/resources/application.properties, which is packaged inside the JAR.

spring.application.name=demo
app.greeting=Hello
management.endpoints.web.exposure.include=health,info
management.info.env.enabled=true
info.app.name=demo
info.app.version=0.0.1
$ java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port=9090 --app.greeting=Howdy
...
2026-10-07T21:54:06.071Z  INFO 1118 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 9090 (http) with context path '/'
2026-10-07T21:54:06.086Z  INFO 1118 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.925 seconds (process running for 2.252)
$ curl -s -w '\n' localhost:9090/api/hello/Bob
Howdy, Bob!
$ APP_GREETING=Bonjour java -jar target/demo-0.0.1-SNAPSHOT.jar
...
2026-10-07T21:54:11.813Z  INFO 1175 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.987 seconds (process running for 2.338)
$ curl -s -w '\n' localhost:8080/api/hello/Bob
Bonjour, Bob!

The same JAR answered with three different greetings. Spring Boot layers property sources: command-line --key=value arguments beat environment variables, which beat the packaged application.properties. The environment variable name is the property upper-cased with dots turned into underscores — APP_GREETING for app.greeting, SERVER_PORT for server.port — which is how containers and CI inject configuration without rebuilding.

If a property has no value anywhere and no :default in the placeholder, the app refuses to start. With ${app.greeting} (no fallback) and the key removed from the file:

$ java -jar target/demo-0.0.1-SNAPSHOT.jar 2>&1 | grep -m1 'Could not resolve'
Caused by: org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'app.greeting' in value "${app.greeting}"

Failing at startup is deliberate: a missing database password should stop the deploy, not surface as a null on the first request.

Profiles

A profile file holds only the keys that differ for one environment.

# src/main/resources/application-dev.properties
server.port=8081
app.greeting=Hi from dev
$ java -jar target/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
2026-10-07T21:54:15.894Z  INFO 1233 --- [demo] [           main] com.example.demo.DemoApplication         : The following 1 profile is active: "dev"
...
2026-10-07T21:54:17.531Z  INFO 1233 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8081 (http) with context path '/'
2026-10-07T21:54:17.543Z  INFO 1233 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.052 seconds (process running for 2.421)
$ curl -s -w '\n' localhost:8081/api/hello
Hi from dev, Spring Boot!

The startup line now names the active profile instead of falling back to 1 default profile. application.properties still loaded — spring.application.name=demo shows up as [demo] in the log — and application-dev.properties was applied on top, so its two keys won. Set SPRING_PROFILES_ACTIVE=dev to do the same from the environment.

Actuator

spring-boot-starter-actuator adds operational endpoints. Only health is exposed over HTTP by default; the management.endpoints.web.exposure.include=health,info line above added info, and the info.* keys supply its content.

$ curl -s -w '\n' localhost:8080/actuator/health
{"status":"UP"}
$ curl -s -w '\n' localhost:8080/actuator/info
{"app":{"name":"demo","version":"0.0.1"}}

Load balancers and Kubernetes probes poll /actuator/health to decide whether an instance should receive traffic. When a database is configured, its connection check is folded into that UP automatically. Keep the exposure list short — endpoints such as env and heapdump leak secrets.

Practice

  1. Add a GET /api/add?a=2&b=40 endpoint that returns {"a":2,"b":40,"sum":42}. What status does it return when b is missing?
Solution
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MathController {

    record Sum(int a, int b, int sum) {}

    @GetMapping("/api/add")
    public Sum add(@RequestParam int a, @RequestParam int b) {
        return new Sum(a, b, a + b);
    }
}
$ curl -s -w '\n' 'localhost:8080/api/add?a=2&b=40'
{"a":2,"b":40,"sum":42}
$ curl -s -o /dev/null -w '%{http_code}\n' 'localhost:8080/api/add?a=2'
400
$ curl -s -o /dev/null -w '%{http_code}\n' 'localhost:8080/api/add?a=2&b=x'
400

@RequestParam is required by default, and x cannot be converted to int, so both bad requests are rejected with 400 before the method runs.

  1. Add a prod profile that serves on port 9000 and greets with Welcome.
Solution
# src/main/resources/application-prod.properties
server.port=9000
app.greeting=Welcome
$ java -jar target/demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
2026-10-07T21:54:43.245Z  INFO 1427 --- [demo] [           main] com.example.demo.DemoApplication         : The following 1 profile is active: "prod"
2026-10-07T21:54:44.722Z  INFO 1427 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 9000 (http) with context path '/'
$ curl -s -w '\n' localhost:9000/api/hello/Ada
Welcome, Ada!
  1. Delete the app.greeting line from application.properties. Predict what /api/hello returns with @Value("${app.greeting:Hello}"), and what happens with @Value("${app.greeting}").
Solution
$ curl -s -w '\n' localhost:8080/api/hello
Hello, Spring Boot!

With the :Hello fallback the app starts and uses the default. Without it, startup fails with the Could not resolve placeholder 'app.greeting' error shown in the properties section.

Frequently Asked Questions

What is the difference between Spring and Spring Boot?
Spring Framework provides dependency injection, web MVC, data access and more, but a plain Spring application needs explicit configuration for each piece. Spring Boot sits on top of it and auto-configures beans based on the libraries on the classpath, embeds a web server such as Tomcat, and packages the app as one executable JAR you start with java -jar.
What does @SpringBootApplication do?
@SpringBootApplication combines three annotations. @Configuration marks the class as a source of bean definitions, @EnableAutoConfiguration turns on Spring Boot's classpath-driven auto-configuration, and @ComponentScan finds @Component, @Service, @Repository and @RestController classes in the same package and its sub-packages.
How do I change a Spring Boot property without rebuilding the JAR?
Spring Boot reads properties from several sources with a fixed precedence. Command-line arguments such as --server.port=9090 win over environment variables such as SERVER_PORT, which win over application.properties packaged inside the JAR. Environment variable names are the property name in upper case with dots replaced by underscores.
What are Spring Boot profiles?
A profile is a named set of configuration. Properties in application-dev.properties are loaded only when the dev profile is active, on top of the shared application.properties, so they override matching keys. Activate a profile with --spring.profiles.active=dev or the SPRING_PROFILES_ACTIVE environment variable.
What is Spring Boot Actuator used for?
Actuator adds operational HTTP endpoints under /actuator, such as health for liveness checks, info for build details and metrics for JVM and HTTP statistics. Only health is exposed over HTTP by default; you list others in management.endpoints.web.exposure.include, and you should expose only what your monitoring actually needs.