Spring Boot Conventions
Virtual Threads (Java 21+)
- Enable virtual threads with
spring.threads.virtual.enabled=true — eliminates thread-pool sizing for I/O-bound services
- Virtual threads handle thousands of concurrent requests without platform thread exhaustion
- Avoid
synchronized blocks in virtual thread apps — use ReentrantLock instead; synchronized pins the carrier thread
Dependency Injection
- Use constructor injection for all required dependencies — never
@Autowired on fields
- Mark constructors with a single dependency implicitly (no annotation needed)
- Use
@RequiredArgsConstructor (Lombok) to reduce boilerplate for final fields
- Keep the number of constructor parameters under 5 — more signals the class has too many responsibilities
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
// Constructor injection — Spring autowires automatically
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
@Transactional
public Order placeOrder(CreateOrderRequest request) {
Order order = Order.from(request);
paymentGateway.charge(order.totalAmount());
return orderRepository.save(order);
}
}
REST Controllers
- Use
@RestController and return ResponseEntity<T> for typed responses with status codes
- Use
@RequestMapping at the class level for base path, HTTP method annotations on methods
- Validate request bodies with
@Valid and Bean Validation annotations — catches bad input before it reaches business logic
- Keep controllers thin — delegate to service classes for business logic; fat controllers are untestable without HTTP
JPA & Database
- Annotate service methods with
@Transactional — not repository or controller methods
- Use
@Transactional(readOnly = true) for read-only operations to enable optimizations — Hibernate skips dirty checking
- Define entity relationships carefully — prefer
LAZY fetch type and load eagerly only when needed; EAGER causes N+1 by default
- Use Spring Data JPA derived queries or
@Query with JPQL — avoid native SQL unless necessary
Database Migrations
- Use Flyway or Liquibase for all schema changes — never rely on
ddl-auto in production; ddl-auto can drop data
- Name migration files sequentially:
V1__create_users.sql, V2__add_email_index.sql
- Test migrations against a copy of the production schema before deploying
- Never modify an already-applied migration — create a new one instead; Flyway checks checksums and will fail on mismatch
Error Handling
- Use
@ControllerAdvice with @ExceptionHandler for centralized error handling
- Return consistent error response bodies with status, message, and timestamp
- Map domain exceptions to appropriate HTTP status codes
- Log the full exception server-side, return a sanitized message to the client
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
var error = new ErrorResponse(404, ex.getMessage(), Instant.now());
return ResponseEntity.status(404).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
var error = new ErrorResponse(400, message, Instant.now());
return ResponseEntity.badRequest().body(error);
}
}
Security
- Configure security with
SecurityFilterChain bean — the old WebSecurityConfigurerAdapter is deprecated
- Use method-level security (
@PreAuthorize) for fine-grained access control
- Store passwords with
BCryptPasswordEncoder — never store plaintext
- Disable CSRF for stateless APIs using JWT, enable it for session-based apps
Configuration
- Use
application.yml with Spring profiles: dev, staging, prod
- Externalize secrets using environment variables or Spring Cloud Config
- Use
@ConfigurationProperties for type-safe configuration binding
- Validate configuration at startup with
@Validated — fail fast instead of discovering missing config at runtime
Spring Modulith
- Use Spring Modulith to enforce module boundaries in monolithic applications — verified with
ApplicationModules.of(App.class).verify()
- Use application events for inter-module communication — keeps modules decoupled while maintaining transactional guarantees
- Use
@ApplicationModuleTest for isolated module integration tests
Observability
- Use Micrometer for metrics and OpenTelemetry for distributed tracing — Spring Boot auto-configures both
- Add
spring-boot-starter-actuator and configure OTLP export for production observability
- Use structured logging (JSON) with correlation IDs in production
GraalVM Native Images
- Use
mvn -Pnative native:compile for native images — sub-second startup, reduced memory
- Declare reflection hints with
@RegisterReflectionForBinding for classes used via reflection
- Use CDS (Class Data Sharing) as a lighter alternative when native image is too restrictive
HTTP Clients
- Use
RestClient for synchronous HTTP calls — replaces RestTemplate with a fluent, modern API
- Use
WebClient for reactive/async HTTP calls
- Configure timeouts and error handling globally via
RestClient.Builder
Testing
- Use
@SpringBootTest for full integration tests with the application context
- Use
@WebMvcTest for controller-only tests with MockMvc
- Use
@DataJpaTest for repository tests with an embedded database
- Use
@ServiceConnection with Testcontainers for integration tests with real databases — replaces embedded databases
- Mock external dependencies with
@MockBean — do not mock the class under test