Java Spring Boot
Spring Boot is the most popular framework for production Java web applications and microservices. It builds on the Spring Framework's dependency injection and adds an opinionated auto-configuration, an embedded HTTP server (Tomcat by default), and a starter-dependency model that gets a runnable application going in minutes.
This tutorial covers scaffolding a Spring Boot app, writing a REST controller, defining a service, persisting data with Spring Data JPA, and running the application. We assume you are comfortable with Java basics, OOP, exceptions, and (ideally) JDBC.
1. Scaffolding a Project
The fastest way to start is start.spring.io. Pick Maven, Java 21, and add the dependencies: Spring Web, Spring Data JPA, and an H2 database for testing. Download, unzip, and open in your IDE.
The generated project has this shape:
src/main/java/com/example/demo/
├── DemoApplication.java # main class
├── controller/
│ └── UserController.java # REST endpoints
├── service/
│ └── UserService.java # business logic
├── model/
│ ├── User.java # JPA entity
│ └── dto/
│ └── UserResponse.java # response shape
└── repository/
└── UserRepository.java # Spring Data repo
src/main/resources/
└── application.properties # config
The @SpringBootApplication annotation on the main class enables auto-configuration and component scanning. The main method calls SpringApplication.run, which starts the embedded Tomcat server and the Spring context.
2. Running the Application
class=class="tok-str">"tok-cmt">// DemoApplication.java
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);
}
}
class=class="tok-str">"tok-cmt">// run with Maven
class=class="tok-str">"tok-cmt">// $ ./mvnw spring-boot:run
class=class="tok-str">"tok-cmt">// or build and run the JAR
class=class="tok-str">"tok-cmt">// $ ./mvnw package
class=class="tok-str">"tok-cmt">// $ java -jar target/demo-class="tok-num">0.0.class="tok-num">1-SNAPSHOT.jar
By default the app listens on port 8080. Open http://localhost:8080 — you will see a 404 page (no routes yet) but the server is running.
3. Writing a REST Controller
A controller maps HTTP requests to Java methods:
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController class=class="tok-str">"tok-cmt">// = @Controller + @ResponseBody
@RequestMapping("/api/users")
public class UserController {
private final UserService service;
class=class="tok-str">"tok-cmt">// constructor injection - preferred over @Autowired on fields
public UserController(UserService service) {
this.service = service;
}
@GetMapping class=class="tok-str">"tok-cmt">// GET /api/users
public List<User> list() {
return service.findAll();
}
@GetMapping("/{id}") class=class="tok-str">"tok-cmt">// GET /api/users/class="tok-num">42
public User get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping class=class="tok-str">"tok-cmt">// POST /api/users
public User create(@RequestBody User user) {
return service.save(user);
}
@DeleteMapping("/{id}") class=class="tok-str">"tok-cmt">// DELETE /api/users/class="tok-num">42
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
Spring picks up JSON conversion automatically thanks to Jackson on the classpath. Sending a request to GET /api/users/42 returns a JSON body.
4. Service Layer and Dependency Injection
Controllers should stay thin — put business logic in a service. Spring's dependency injection wires the service into the controller:
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service class=class="tok-str">"tok-cmt">// Spring registers this as a bean
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { class=class="tok-str">"tok-cmt">// constructor injection
this.repo = repo;
}
public List<User> findAll() { return repo.findAll(); }
public User findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new IllegalArgumentException("no user " + id));
}
@Transactional class=class="tok-str">"tok-cmt">// wraps the method in a DB transaction
public User save(User user) { return repo.save(user); }
@Transactional
public void delete(Long id) { repo.deleteById(id); }
}
@Autowired is optional when there is a single constructor — Spring 4.3+ auto-wires single-constructor classes. Prefer constructor injection (your field can be final).
5. Persistence with Spring Data JPA
Define an entity and a repository interface — Spring implements the queries for you:
class=class="tok-str">"tok-cmt">// entity - a JPA-mapped table row
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
class=class="tok-str">"tok-cmt">// getters and setters (or use Lombok @Data)
}
class=class="tok-str">"tok-cmt">// repository - Spring implements this at runtime
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
class=class="tok-str">"tok-cmt">// derived query - Spring generates the SQL from the method name
Optional<User> findByEmail(String email);
class=class="tok-str">"tok-cmt">// custom query with JPQL
@org.springframework.data.jpa.repository.Query(
"SELECT u FROM User u WHERE u.name LIKE ?class="tok-num">1%")
java.util.List<User> findByNamePrefix(String prefix);
}
The repository interface has zero implementation. Spring Data reads the method name (findByEmail), derives the SQL (SELECT ... WHERE email = ?), and provides the implementation at runtime.
6. Configuration
Spring Boot reads application.properties (or application.yml) on startup:
# src/main/resources/application.properties
# server
server.port=class="tok-num">8080
# database
spring.datasource.url=jdbc:postgresql:class=class="tok-str">"tok-cmt">//localhost:class="tok-num">5432/mydb
spring.datasource.username=app
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA - auto-create schema in dev, never use in prod
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# logging
logging.level.com.example=DEBUG
logging.level.org.hibernate.SQL=DEBUG
# YAML equivalent: application.yml
server:
port: class="tok-num">8080
spring:
datasource:
url: jdbc:postgresql:class=class="tok-str">"tok-cmt">//localhost:class="tok-num">5432/mydb
username: app
password: secret
Profile-specific files (application-dev.properties) override when you run with --spring.profiles.active=dev.
7. Spring Security (Overview)
Adding the spring-boot-starter-security dependency turns on authentication for every endpoint by default. Configure it with a SecurityFilterChain bean:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()); class=class="tok-str">"tok-cmt">// disable for stateless APIs
return http.build();
}
}
For stateless REST APIs, pair Spring Security with JWT. For server-rendered apps, use sessions. Both are well-supported; pick the one that matches your architecture.
8. Error Handling
Throw an exception from any controller or service, and Spring converts it to an HTTP response:
class=class="tok-str">"tok-cmt">// custom exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User not found: " + id);
}
}
class=class="tok-str">"tok-cmt">// global handler - turns exceptions into HTTP responses
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@org.springframework.http.HttpStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(UserNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
public record ErrorResponse(String code, String message) {}
}
class=class="tok-str">"tok-cmt">// usage in a controller
public User get(@PathVariable Long id) {
return repo.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
Spring Boot also provides a default error page at /error and a JSON error body for REST clients. Override both by implementing ErrorController.
9. Testing
Spring Boot's test support is excellent. @SpringBootTest boots the full context; @WebMvcTest slices just the web layer; @DataJpaTest slices just the JPA layer:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired MockMvc mvc;
@Test
void listUsers_returns200() throws Exception {
mvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray());
}
}
10. Deployment
Spring Boot packages an executable JAR with an embedded Tomcat. You deploy one file — no separate servlet container install:
# build the executable JAR
$ ./mvnw clean package
# the result: target/demo-class="tok-num">0.0.class="tok-num">1-SNAPSHOT.jar
# contains all dependencies plus an embedded Tomcat
# run anywhere with Java class="tok-num">17+
$ java -jar target/demo-class="tok-num">0.0.class="tok-num">1-SNAPSHOT.jar
# override config at runtime
$ java -jar demo.jar --server.port=class="tok-num">9090
$ java -jar demo.jar --spring.profiles.active=prod
For container deployment, write a minimal Dockerfile:
# Dockerfile
FROM eclipse-temurin:class="tok-num">21-jre AS runtime
WORKDIR /app
COPY target/demo-class="tok-num">0.0.class="tok-num">1-SNAPSHOT.jar app.jar
EXPOSE class="tok-num">8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# build and run
# $ docker build -t demo:latest .
# $ docker run -p class="tok-num">8080:class="tok-num">8080 demo:latest
Spring Boot's official getting-started guides cover dozens of common integrations: Kafka, Redis, OAuth, GraphQL, gRPC, and more.
Exercises
- Scaffold a Spring Boot project with start.spring.io and start it; visit
http://localhost:8080. - Add a
GET /api/timeendpoint that returns the current time as JSON. - Define a
Taskentity, aTaskRepository, and a controller that lists, creates, and deletes tasks. - Add a custom query
findByStatuson the repository and call it from a controller. - Write a
@WebMvcTestfor the controller usingMockMvc.