Java Spring Boot

ADVANCED ~8 min read Tutorial

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:

java
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

java
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:

java
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:

java
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:

java
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:

java
# 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:

java
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:

java
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:

java
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:

java
# 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:

java
# 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

  1. Scaffold a Spring Boot project with start.spring.io and start it; visit http://localhost:8080.
  2. Add a GET /api/time endpoint that returns the current time as JSON.
  3. Define a Task entity, a TaskRepository, and a controller that lists, creates, and deletes tasks.
  4. Add a custom query findByStatus on the repository and call it from a controller.
  5. Write a @WebMvcTest for the controller using MockMvc.