Key Changes in Spring Boot 3
Spring Boot 3 requires Java 17 or higher and has transitioned to the Jakarta EE 10 namespace (javax.* to jakarta.*). It officially supports GraalVM native images, and observability has been unified through a Micrometer-based approach.
The fastest way to get started is by creating a project via Spring Initializr. The core dependency is spring-boot-starter-web, which alone includes embedded Tomcat, Jackson JSON processing, and Spring MVC.
Project Creation and Basic Structure
build.gradle Configuration
// build.gradle — Spring Boot 3 dependency configuration
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '1.0.0'
java {
sourceCompatibility = '17' // Java 17 or higher required
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' // REST API core
implementation 'org.springframework.boot:spring-boot-starter-validation' // Input validation
testImplementation 'org.springframework.boot:spring-boot-starter-test' // Testing
}
Main Application Class
// Application.java — Spring Boot entry point
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // Auto-configuration + component scanning + configuration class
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
// Output:
// Started Application in 1.234 seconds (process running for 1.567)
// Tomcat started on port 8080
}
}
@SpringBootApplication is a combination of three annotations. @EnableAutoConfiguration detects libraries on the classpath and applies auto-configuration. @ComponentScan automatically registers beans in the current package and sub-packages. @Configuration marks this class itself as a configuration class.
REST API Implementation
Domain Model and Controller
// TaskController.java — CRUD REST API implementation
package com.example.demo.controller;
import com.example.demo.dto.TaskRequest;
import com.example.demo.dto.TaskResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
// DTO — concisely defined using records
record TaskRequest(
@jakarta.validation.constraints.NotBlank(message = "Title is required")
String title,
String description
) {}
record TaskResponse(
Long id,
String title,
String description,
boolean completed,
LocalDateTime createdAt
) {}
@RestController // Automatic JSON serialization
@RequestMapping("/api/tasks") // Common path prefix
public class TaskController {
// In-memory store (in practice, use a JPA Repository)
private final Map<Long, TaskResponse> store = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
// GET /api/tasks — List all
@GetMapping
public List<TaskResponse> getAll() {
return new ArrayList<>(store.values());
// Response example: [{"id":1,"title":"Study","completed":false,...}]
}
// GET /api/tasks/{id} — Get single item
@GetMapping("/{id}")
public ResponseEntity<TaskResponse> getById(@PathVariable Long id) {
TaskResponse task = store.get(id);
if (task == null) {
return ResponseEntity.notFound().build(); // Return 404
}
return ResponseEntity.ok(task); // 200 + JSON body
}
// POST /api/tasks — Create
@PostMapping
public ResponseEntity<TaskResponse> create(@Valid @RequestBody TaskRequest req) {
Long id = idGenerator.getAndIncrement();
TaskResponse task = new TaskResponse(
id, req.title(), req.description(), false, LocalDateTime.now()
);
store.put(id, task);
return ResponseEntity.status(HttpStatus.CREATED).body(task);
// 201 Created + return created resource
}
// DELETE /api/tasks/{id} — Delete
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if (store.remove(id) == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build(); // 204 No Content
}
}
Global Exception Handling
// GlobalExceptionHandler.java — Unified error response
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.List;
record ErrorResponse(int status, String message, LocalDateTime timestamp) {}
@RestControllerAdvice // Exception handler applied to all controllers
public class GlobalExceptionHandler {
// Handle validation failures
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
ErrorResponse response = new ErrorResponse(
400,
String.join(", ", errors),
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(response);
// Result: {"status":400,"message":"title: Title is required","timestamp":"..."}
}
}
Understanding Auto-Configuration
Spring Boot’s auto-configuration follows the “Convention over Configuration” principle. When you add spring-boot-starter-web, the following is configured automatically.
| Detection Condition | Auto-Configuration |
|---|---|
| Tomcat on classpath | Starts embedded web server |
| Jackson on classpath | JSON serialization/deserialization |
@RestController present | Spring MVC dispatcher servlet |
application.properties | Custom settings for port, logging, etc. |
You can override key settings in application.properties.
// application.properties — Key configuration items
// server.port=9090 // Change port (default 8080)
// spring.jackson.date-format=yyyy-MM-dd // JSON date format
// logging.level.root=INFO // Log level
// spring.profiles.active=dev // Active profile
// Profile-specific configuration files
// application-dev.properties -> Development environment
// application-prod.properties -> Production environment
Profiles and External Configuration
// AppConfig.java — @ConfigurationProperties pattern
package com.example.demo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app") // Binds app.* properties
public record AppConfig(
String name, // app.name
int maxRetries, // app.max-retries (kebab -> camel auto-conversion)
String apiUrl // app.api-url
) {}
// In application.properties:
// app.name=TaskManager
// app.max-retries=3
// app.api-url=https://api.example.com
// Usage example:
// @Service
// public class MyService {
// private final AppConfig config;
// public MyService(AppConfig config) {
// this.config = config;
// System.out.println(config.name()); // "TaskManager"
// }
// }
Summary
Here are the key points to remember when starting with Spring Boot 3.
- Java 17 or higher is required, and
javax.*tojakarta.*migration is necessary - A single
@SpringBootApplicationintegrates auto-configuration, component scanning, and configuration class - REST APIs are implemented with the
@RestController+@RequestMappingcombination, and usingrecordfor DTOs reduces boilerplate @Validand@RestControllerAdviceseparate input validation from exception handling- Use
@ConfigurationPropertiesfor type-safe configuration binding - Manage environment-specific settings with profiles (
spring.profiles.active)
The strength of Spring Boot is “start fast, scale flexibly.” Start quickly with default settings, and when needed, override auto-configuration for fine-grained control.