What Spring Boot Adds

Spring Boot sits on top of Spring Framework and eliminates boilerplate:

  • Auto-configuration — sensible defaults based on classpath
  • Embedded servers — Tomcat, Jetty, or Undertow built in
  • Starters — curated dependency bundles
  • Actuator — production-ready health and metrics endpoints
  • Externalized config — properties, YAML, environment variables

You focus on business logic; Boot handles infrastructure.

Creating a Project

Use start.spring.io or CLI:

  spring init --dependencies=web,data-jpa,postgresql,validation my-api
cd my-api
./mvnw spring-boot:run
  

REST API Layer

  @RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService service;

    public ProductController(ProductService service) {
        this.service = service;
    }

    @GetMapping
    public List<ProductDto> list() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ProductDto get(@PathVariable Long id) {
        return service.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ProductDto create(@Valid @RequestBody CreateProductRequest req) {
        return service.create(req);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        service.delete(id);
    }
}
  

Service Layer

  @Service
@Transactional
public class ProductService {

    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public List<ProductDto> findAll() {
        return repository.findAll().stream()
            .map(ProductDto::from)
            .toList();
    }

    public ProductDto create(CreateProductRequest req) {
        Product product = new Product(req.name(), req.price());
        return ProductDto.from(repository.save(product));
    }
}
  

Keep controllers thin — validation, HTTP mapping, and status codes only. Business rules live in services.

Validation

  public record CreateProductRequest(
    @NotBlank @Size(max = 100) String name,
    @NotNull @Positive BigDecimal price
) {}
  

Spring validates @Valid request bodies automatically. Return structured errors:

  @RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
        var errors = ex.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "invalid"
            ));
        return Map.of("errors", errors);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, String> handleNotFound(ResourceNotFoundException ex) {
        return Map.of("error", ex.getMessage());
    }
}
  

Configuration Properties

Type-safe configuration:

  @ConfigurationProperties(prefix = "app.shipping")
public record ShippingProperties(
    BigDecimal freeThreshold,
    int maxDays
) {}
  
  app.shipping.free-threshold=50.00
app.shipping.max-days=7
  
  @Service
public class ShippingService {
    private final ShippingProperties props;

    public ShippingService(ShippingProperties props) {
        this.props = props;
    }

    public boolean isFree(BigDecimal orderTotal) {
        return orderTotal.compareTo(props.freeThreshold()) >= 0;
    }
}
  

Enable with @EnableConfigurationProperties(ShippingProperties.class).

Profiles and Environment

  # application.yml
spring:
  profiles:
    active: dev

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:postgresql://localhost:5432/devdb

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:postgresql://prod-host:5432/proddb
  

Override at runtime: java -jar app.jar --spring.profiles.active=prod

Spring Boot Actuator

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  
  management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=when-authorized
  

Endpoints:

Endpoint Purpose
/actuator/health Application health (UP/DOWN)
/actuator/info Custom app info
/actuator/metrics JVM, HTTP, custom metrics
/actuator/prometheus Prometheus scrape format

Custom health indicator:

  @Component
public class PaymentGatewayHealth implements HealthIndicator {
    @Override
    public Health health() {
        boolean reachable = checkGateway();
        return reachable ? Health.up().build() : Health.down().withDetail("reason", "timeout").build();
    }
}
  

Logging

  logging.level.root=INFO
logging.level.com.example=DEBUG
logging.pattern.console=%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
  

Use SLF4J with Logback (default). Structure logs as JSON in production with logstash-logback-encoder.

Packaging and Deployment

  ./mvnw clean package -DskipTests
java -jar target/my-api-0.0.1-SNAPSHOT.jar
  

Docker

  FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-jar", "app.jar"]
  

Graceful Shutdown

  server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
  

Kubernetes sends SIGTERM — finish in-flight requests before exit.

DevTools (Development Only)

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
  

Automatic restart on classpath changes. Never include in production builds.

Testing

  @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApiIntegrationTest {

    @Autowired
    private TestRestTemplate rest;

    @Test
    void createsProduct() {
        var req = Map.of("name", "Widget", "price", 9.99);
        var response = rest.postForEntity("/api/products", req, ProductDto.class);
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
    }
}
  

Test slices for faster unit tests:

  • @WebMvcTest — controller layer only
  • @DataJpaTest — repository layer with in-memory DB

Production Checklist

  • Externalize all secrets (env vars, vault)
  • Actuator health endpoint configured for orchestrator
  • Structured logging with correlation IDs
  • Graceful shutdown enabled
  • JVM container-aware flags (-XX:+UseContainerSupport)
  • CI runs ./mvnw verify with tests
  • Dockerfile uses JRE, not JDK

Spring Boot is the fastest path from idea to production REST API in the Java ecosystem.