Spring Boot
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 verifywith tests - Dockerfile uses JRE, not JDK
Spring Boot is the fastest path from idea to production REST API in the Java ecosystem.