โณ
Loading cheatsheet...
Spring Boot 3.4+, Java 21+, starters, architecture, modern HTTP clients, observability, native image, and deployment patterns.
| Feature | Description |
|---|---|
| Virtual Threads (Java 21+) | Enabled by default via spring.threads.virtual.enabled=true |
| Structured Logging | Built-in JSON structured logging with EcsFormatter & LogstashEncoder |
| RestClient | Synchronous HTTP client (fluently typed, replaces RestTemplate) |
| RestClient SNI | Server Name Indication support for TLS |
| JdbcClient | Modern JDBC wrapper with fluent API & optional query params |
| Base64 Resource Loading | spring-boot-starter-web supports classpath: base64 resources |
| Improved Docker Compose | Auto-detection of services, service connections API |
| CDS (Class Data Sharing) | Application class data sharing for faster startup |
| Starter | What It Includes |
|---|---|
| spring-boot-starter | Core starter: auto-config, logging, YAML |
| spring-boot-starter-web | Spring MVC + embedded Tomcat (Servlet stack) |
| spring-boot-starter-webflux | Spring WebFlux + Netty (Reactive stack) |
| spring-boot-starter-data-jpa | Spring Data JPA + Hibernate + HikariCP |
| spring-boot-starter-data-mongodb | Spring Data MongoDB + MongoDB driver |
| spring-boot-starter-security | Spring Security (auto-config + default form login) |
| spring-boot-starter-test | JUnit 5, Mockito, AssertJ, Spring Test |
| spring-boot-starter-actuator | Production-ready monitoring endpoints |
| spring-boot-starter-validation | Jakarta Bean Validation (Hibernate Validator) |
| spring-boot-starter-cache | Spring Framework caching abstractions |
| Aspect | Details |
|---|---|
| Baseline Java | Java 17+ minimum (Java 21 LTS recommended) |
| Jakarta EE | Jakarta EE 10+ namespace migration |
| Repository | Moved to Spring Repositories (OSSRH) |
| Deprecations | javax.* fully removed โ jakarta.* only |
| GraalVM | First-class native image support (no Spring AOT plugin) |
javax.* packages are replaced with jakarta.* (e.g., jakarta.servlet, jakarta.persistence). If migrating from Boot 2.x, use the OpenRewrite migration tool to automate package renames.// Spring Boot 3.4.x with Gradle (Kotlin DSL)
plugins {
java
id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java.sourceCompatibility = JavaVersion.VERSION_21
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("com.h2database:h2") // Dev DB
runtimeOnly("org.postgresql:postgresql") // Prod DB
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:junit-jupiter")
}spring:
application:
name: my-app
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 100
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
server:
port: 8080
tomcat:
threads:
max: 200
min-spare: 10
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
logging:
level:
root: info
com.example: debug
structured:
format:
enabled: true| Priority | Source |
|---|---|
| 1 (highest) | Command-line arguments (--server.port=8080) |
| 2 | SPRING_APPLICATION_JSON environment variable |
| 3 | ServletConfig / ServletContext parameters |
| 4 | JNDI attributes |
| 5 | Java System properties (System.getProperties()) |
| 6 | OS environment variables |
| 7 | RandomValuePropertySource |
| 8 | Profile-specific YAML/properties outside JAR |
| 9 | Profile-specific YAML/properties inside JAR |
| 10 | Application YAML/properties outside JAR |
| 11 | Application YAML/properties inside JAR |
| 12 (lowest) | @PropertySource annotations on your @Configuration |
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record AppProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@Email String from,
boolean sslEnabled
) {}
// In main class or @Configuration:
@EnableConfigurationProperties(AppProperties.class)
// Usage (constructor injection):
@Service
public class EmailService {
private final AppProperties props;
public EmailService(AppProperties props) {
this.props = props;
}
}@ConfigurationProperties provides type-safe binding, relaxed binding (kebab-case โ camelCase), validation via JSR-380, and IDE auto-completion with the spring-boot-configuration-processor. Use @Value only for simple one-off values.// โโ Stereotype Annotations โโ
@Component // Generic Spring-managed bean
@Service // Business logic layer (extends @Component)
@Repository // Data access layer (auto-translates exceptions)
@Controller // Web controller (returns view name)
@RestController // = @Controller + @ResponseBody (returns JSON)
// โโ Constructor Injection (PREFERRED) โโ
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentService paymentService;
private final AppProperties props;
// Single constructor โ @Autowired is optional
public OrderService(OrderRepository orderRepo,
PaymentService paymentService,
AppProperties props) {
this.orderRepo = orderRepo;
this.paymentService = paymentService;
this.props = props;
}
}
// โโ @Bean Configuration โโ
@Configuration
public class AppConfig {
@Bean
@Primary
public RestClient restClient(RestClient.Builder builder) {
return builder.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
}
@Bean
@Profile("dev")
public DataSource devDataSource() { /* H2 in-memory */ }
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() { return new PrototypeBean(); }
}| Scope | Description | Typical Use |
|---|---|---|
| singleton (default) | One instance per Spring IoC container | Services, repositories, utilities |
| prototype | New instance every time requested | Stateful objects, short-lived tasks |
| request | One per HTTP request (web only) | Request-scoped data holders |
| session | One per HTTP session (web only) | User session data, shopping carts |
| application | One per ServletContext | Shared app-level resources |
| Annotation | Purpose |
|---|---|
| @Primary | Preferred bean when multiple candidates exist |
| @Qualifier("beanName") | Select specific bean by name |
| @ConditionalOnClass | Bean only if class is on classpath |
| @ConditionalOnProperty | Bean only if property matches |
| @ConditionalOnMissingBean | Bean only if no other bean of same type |
| @Profile("prod") | Bean only active in specific profile |
| @PostConstruct | Called after dependency injection |
| @PreDestroy | Called before bean destruction |
final fields), simplifies testing (no reflection needed for mocks), and guarantees the bean is in a valid state after construction. Field injection with @Autowired should be avoided.@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public Page<ProductDto> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String category) {
return service.findAll(category, PageRequest.of(page, size));
}
@GetMapping("/{id}")
public ProductDto get(@PathVariable UUID id) {
return service.findById(id)
.orElseThrow(() -> new NotFoundException("Product not found"));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductDto create(@Valid @RequestBody CreateProductRequest req) {
return service.create(req);
}
@PutMapping("/{id}")
public ProductDto update(@PathVariable UUID id,
@Valid @RequestBody UpdateProductRequest req) {
return service.update(id, req);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable UUID id) {
service.delete(id);
}
@GetMapping("/search")
public List<ProductDto> search(
@RequestParam String q,
@RequestHeader(value = "X-Correlation-ID", required = false) String traceId) {
return service.search(q);
}
}@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(NotFoundException ex) {
return new ErrorResponse(HttpStatus.NOT_FOUND.value(),
ex.getMessage(), Instant.now());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.toList();
return new ErrorResponse(400, "Validation failed", Instant.now(), errors);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneral(Exception ex) {
return new ErrorResponse(500, "Internal server error", Instant.now());
}
}
public record ErrorResponse(int status, String message,
Instant timestamp, List<String> details) {
public ErrorResponse(int s, String m, Instant t) { this(s, m, t, List.of()); }
}| Annotation | Purpose |
|---|---|
| @RestController | @Controller + @ResponseBody โ all methods return JSON |
| @RequestMapping | Maps HTTP methods to handler methods (class/method level) |
| @GetMapping / @PostMapping | Shorthand for @RequestMapping(method = GET/POST) |
| @PathVariable | Binds URI template variable to method parameter |
| @RequestParam | Binds query parameter (supports defaultValue, required) |
| @RequestBody | Binds HTTP request body to Java object (deserializes JSON) |
| @RequestHeader | Binds HTTP header to method parameter |
| @ResponseStatus | Sets HTTP status code for the response |
| @Valid | Triggers Jakarta Bean Validation on @RequestBody / @RequestParam |
| @ControllerAdvice | Global exception handler + shared @InitBinder / @ModelAttribute |
@Mapper(componentModel = "spring")
public interface ProductMapper {
ProductDto toDto(Product entity);
Product toEntity(CreateProductRequest req);
@BeanMapping(nullValuePropertyMappingStrategy
= NullValuePropertyMappingStrategy.IGNORE)
void update(UpdateProductRequest req, @MappingTarget Product entity);
}
// Usage:
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository repo;
private final ProductMapper mapper; // injected by Spring
public ProductDto create(CreateProductRequest req) {
Product entity = mapper.toEntity(req);
return mapper.toDto(repo.save(entity));
}
}MapStruct for type-safe mapping between entities and DTOs. Always validate input with @Valid + Jakarta Validation annotations (@NotBlank, @Size, @Email, etc.).@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = "email")
})
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Enumerated(EnumType.STRING)
private UserRole role;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL,
orphanRemoval = true, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
@CreatedDate @Column(updatable = false)
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
// Getters/setters or use records (requires @ConstructorProperties)
}
// โโ Repository โโ
public interface UserRepository extends
JpaRepository<User, UUID>,
JpaSpecificationExecutor<User>,
CustomUserRepository {
Optional<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.role = :role AND u.createdAt > :since")
List<User> findByRoleAndCreatedAfter(@Param("role") UserRole role,
@Param("since") Instant since);
@Query(value = "SELECT * FROM users WHERE email LIKE %:keyword%",
nativeQuery = true)
List<User> searchNative(@Param("keyword") String keyword);
boolean existsByEmail(String email);
@Modifying
@Query("DELETE FROM User u WHERE u.lastLogin < :cutoff")
int deleteInactiveUsers(@Param("cutoff") Instant cutoff);
}| Relationship | Annotation | Owner Side | Fetch Default |
|---|---|---|---|
| One-to-Many | @OneToMany | @ManyToOne side | LAZY |
| Many-to-One | @ManyToOne | This side (has FK) | EAGER โ ๏ธ |
| Many-to-Many | @ManyToMany | Either side (use mappedBy) | LAZY |
| One-to-One | @OneToOne | @JoinColumn side | EAGER โ ๏ธ |
| Interface | Methods Provided |
|---|---|
| CrudRepository | save, findById, findAll, deleteById, count, existsById |
| ListCrudRepository | Same as above but returns List<T> (no Iterable) |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) |
| JpaRepository | All above + flushInBatch, saveAndFlush, deleteInBatch |
| JpaSpecificationExecutor | findAll(Specification), findOne(Specification) |
// โโ Dynamic queries with Specification API โโ
public class UserSpecs {
public static Specification<User> hasRole(UserRole role) {
return (root, query, cb) -> cb.equal(root.get("role"), role);
}
public static Specification<User> nameContains(String name) {
return (root, query, cb) ->
cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
public static Specification<User> createdAfter(Instant date) {
return (root, query, cb) -> cb.greaterThan(root.get("createdAt"), date);
}
}
// Usage:
Specification<User> spec = Specification
.where(UserSpecs.hasRole(UserRole.ADMIN))
.and(UserSpecs.nameContains("John"));
Page<User> result = userRepository.findAll(spec, PageRequest.of(0, 20));spring.jpa.open-in-view=true keeps the Hibernate session open for the entire HTTP request, causing N+1 issues and masking lazy loading bugs. Disable it and use @EntityGraph or @Query("JOIN FETCH ...") to explicitly control fetching.@Configuration
@EnableWebSecurity
@EnableMethodSecurity // enables @PreAuthorize, @Secured, @RolesAllowed
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
private final UserDetailsService userDetailsService;
// โโ SecurityFilterChain (new style โ no WebSecurityConfigurerAdapter) โโ
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
)
.headers(h -> h.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'")))
.build();
}
@Bean
@Order(2)
public SecurityFilterChain formFilterChain(HttpSecurity http) throws Exception {
return http
.formLogin(form -> form.loginPage("/login").permitAll())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register").permitAll()
.anyRequest().authenticated()
)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength 4-31
}
}| Annotation | Expression | Requires |
|---|---|---|
| @PreAuthorize("hasRole('ADMIN')") | SpEL expression, most flexible | @EnableMethodSecurity |
| @Secured("ROLE_ADMIN") | Simple role check, no SpEL | @EnableMethodSecurity |
| @RolesAllowed("ADMIN") | JSR-250 standard | @EnableMethodSecurity(jsr250Enabled=true) |
| @PreAuthorize("@acl.canRead(#id, authentication)") | Custom bean method call | Bean named "acl" in context |
| @PreAuthorize("#userId == authentication.principal.id") | Parameter-based check | Method param named userId |
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain)
throws ServletException, IOException {
String token = resolveToken(req);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsername(token);
UserDetails user = userDetailsService
.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(req));
SecurityContextHolder.getContext()
.setAuthentication(auth);
}
chain.doFilter(req, res);
}
}SecurityFilterChain bean approach (Lambdas). CORS must be configured before Spring Security โ use CorsConfigurationSource bean or http.cors(Customizer.withDefaults()) with a CorsFilter bean.@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private ProductService productService;
@Test
void shouldReturnProductById() throws Exception {
ProductDto dto = new ProductDto(UUID.randomUUID(),
"Widget", BigDecimal.valueOf(29.99), "ELECTRONICS");
when(productService.findById(any())).thenReturn(Optional.of(dto));
mockMvc.perform(get("/api/v1/products/{id}", dto.id()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Widget"))
.andExpect(jsonPath("$.price").value(29.99));
}
@Test
void shouldCreateProduct() throws Exception {
CreateProductRequest req = new CreateProductRequest(
"Widget", BigDecimal.valueOf(29.99), "ELECTRONICS");
ProductDto dto = new ProductDto(UUID.randomUUID(),
"Widget", BigDecimal.valueOf(29.99), "ELECTRONICS");
when(productService.create(any())).thenReturn(dto);
mockMvc.perform(post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists());
}
@Test
void shouldReturn404WhenNotFound() throws Exception {
when(productService.findById(any())).thenReturn(Optional.empty());
mockMvc.perform(get("/api/v1/products/{id}", UUID.randomUUID()))
.andExpect(status().isNotFound());
}
}| Annotation | Scope | Loads |
|---|---|---|
| @SpringBootTest | Full application context | Entire app (webEnvironment) |
| @WebMvcTest | Spring MVC layer only | Controllers, @ControllerAdvice, filters |
| @DataJpaTest | JPA layer only | Entities, repositories, JPA config |
| @JsonTest | JSON serialization only | Jackson, Gson auto-config |
| @WebFluxTest | WebFlux layer only | @RestController, WebFlux config |
| @RestClientTest | RestClient layer | RestTemplate/RestClient support |
| @MockBean | Replace bean in context | Mockito mock |
| @SpyBean | Partial mock bean | Mockito spy (wraps real) |
@Testcontainers
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:tc:postgresql:16-alpine:///testdb",
"spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver"
})
abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void postgresProperties(DynamicPropertyRegistry reg) {
reg.add("spring.datasource.url", postgres::getJdbcUrl);
reg.add("spring.datasource.username", postgres::getUsername);
reg.add("spring.datasource.password", postgres::getPassword);
}
}@WebMvcTest loads only the web layer (fast), @DataJpaTest loads only JPA (uses embedded DB), @SpringBootTest loads everything (slowest). Use @Testcontainers for realistic integration tests with real databases.@RestController
@RequestMapping("/api/v2/products")
public class ReactiveProductController {
private final ReactiveProductRepository repo;
public ReactiveProductController(ReactiveProductRepository repo) {
this.repo = repo;
}
@GetMapping("/{id}")
public Mono<ProductDto> getById(@PathVariable String id) {
return repo.findById(id)
.map(this::toDto)
.switchIfEmpty(Mono.error(new NotFoundException("Not found")));
}
@GetMapping
public Flux<ProductDto> list() {
return repo.findAll().map(this::toDto);
}
@GetMapping("/stream")
public Flux<ServerSentEvent<ProductDto>> streamProducts() {
return repo.findAll()
.map(dto -> ServerSentEvent.<ProductDto>builder()
.data(dto)
.event("product-update")
.build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ProductDto> create(@Valid @RequestBody CreateProductRequest req) {
return repo.save(toEntity(req)).map(this::toDto);
}
}| Type | Cardinality | Description |
|---|---|---|
| Mono<T> | 0 or 1 | Asynchronous single result (like CompletableFuture) |
| Flux<T> | 0..N | Stream of 0+ elements (like a reactive Stream) |
| Sinks.Many<T> | Programmatic | Manually emit signals to Flux/Mono |
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeaders(h -> {
h.setBearerAuth(token);
h.setContentType(MediaType.APPLICATION_JSON);
})
.build();
}
}
// Usage:
Mono<UserDto> user = webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(UserDto.class)
.timeout(Duration.ofSeconds(5)
.onErrorResume(WebClientResponseException.NotFound.class,
ex -> Mono.empty());Flux for streaming (SSE, WebSocket, Server-Sent Events), Mono for single async results. Never call .block() on a Netty thread โ use Schedulers.boundedElastic() for blocking I/O. R2DBC replaces JDBC for reactive database access.| Component | Purpose | Alternatives |
|---|---|---|
| Config Server | Centralized configuration management | Consul, Vault, AWS Parameter Store |
| Eureka / Consul | Service discovery & registration | Kubernetes DNS, ZooKeeper |
| Spring Cloud Gateway | API Gateway (routing, filters) | Kong, NGINX, Envoy |
| Resilience4j | Circuit breaker, rate limiter, retry | Hystrix (deprecated), Sentinel |
| Micrometer + Zipkin | Distributed tracing | OpenTelemetry, Jaeger |
| Cloud LoadBalancer | Client-side load balancing | Ribbon (deprecated), Spring Cloud |
| Sleuth (deprecated) | Distributed tracing (use Micrometer Tracing) | Micrometer Observation API |
| Cloud Stream | Message-driven microservices | Direct Kafka/RabbitMQ clients |
spring:
cloud:
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: productCircuit
fallbackUri: forward:/fallback/products
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=1
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT@Configuration
public class ResilienceConfig {
@Bean
public CircuitBreaker circuitBreaker(
CircuitBreakerRegistry registry) {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // open at 50% failure
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10) // last 10 calls
.permittedNumberOfCallsInHalfOpenState(3)
.build();
return registry.circuitBreaker("default", config);
}
@Bean
public Retry retry(RetryRegistry registry) {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryOnException(e -> e instanceof WebClientResponseException
&& ((WebClientResponseException) e)
.getStatusCode().is5xxServerError())
.build();
return registry.retry("default", config);
}
}| Concept | Description |
|---|---|
| Job | Top-level batch process โ contains one or more Steps |
| Step | Unit of work โ Tasklet-based or Chunk-oriented |
| Tasklet | Single execution (e.g., delete temp files, send email) |
| Chunk | Read โ Process โ Write in configurable commit intervals |
| ItemReader | Reads data (flat file, DB, JMS, custom) |
| ItemProcessor | Transforms/validates items (optional) |
| ItemWriter | Writes data (DB, file, email, custom) |
| JobRepository | Stores job/step execution metadata (DB required) |
| JobLauncher | Starts a job with JobParameters |
| SkipListener | Handles skipped items (logging, error handling) |
| Strategy | How It Works | Complexity |
|---|---|---|
| Multi-threaded Step | TaskExecutor with thread pool per step | Low |
| Parallel Steps | Split Step (multiple steps run concurrently) | Medium |
| Remote Chunking | Master reads โ sends chunks to workers via messaging | High |
| Partitioning | Master partitions data โ workers process partitions | High |
@Configuration
@EnableBatchProcessing
public class UserImportBatchConfig {
@Bean
public Job importUserJob(JobRepository jobRepo,
Step importStep,
JobListener listener) {
return new JobBuilder("importUsers", jobRepo)
.listener(listener)
.start(importStep)
.build();
}
@Bean
public Step importStep(JobRepository jobRepo,
PlatformTransactionManager txMgr,
ItemReader<UserRecord> reader,
ItemProcessor<UserRecord, User> processor,
ItemWriter<User> writer) {
return new StepBuilder("importUsersStep", jobRepo)
.<UserRecord, User>chunk(500, txMgr)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.skipLimit(50)
.skip(ValidationException.class)
.listener(skipListener())
.build();
}
@Bean
public FlatFileItemReader<UserRecord> flatReader() {
return new FlatFileItemReaderBuilder<UserRecord>()
.name("userReader")
.resource(new ClassPathResource("users.csv"))
.delimited().delimiter(",")
.names("name", "email", "role")
.fieldSetMapper(fs -> new UserRecord(
fs.readString("name"), fs.readString("email"),
UserRole.valueOf(fs.readString("role"))))
.build();
}
}spring.batch.jdbc.initialize-schema=always for development and Flyway/Liquibase for production schema management.@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
public ProducerFactory<String, OrderEvent> producerFactory() {
Map<String, Object> config = Map.of(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092",
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
StringSerializer.class,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
JsonSerializer.class,
ProducerConfig.ACKS_CONFIG, "all"
);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public ConsumerFactory<String, OrderEvent> consumerFactory() {
Map<String, Object> config = Map.of(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092",
ConsumerConfig.GROUP_ID_CONFIG, "order-group",
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
JsonDeserializer.class,
JsonDeserializer.TRUSTED_PACKAGES, "com.example.*"
);
return new DefaultKafkaConsumerFactory<>(config);
}
}
@KafkaListener(topics = "orders",
groupId = "order-group",
containerFactory = "kafkaListenerContainerFactory")
public void handleOrder(OrderEvent event) {
orderService.process(event);
}| Feature | How To |
|---|---|
| Retry | @RetryableTopic(attempts = 3, backoff = @Backoff(delay = 1000)) |
| DLT (Dead Letter Topic) | Auto-created: topic.DLT after retries exhausted |
| Manual ACK | Acknowledgment.acknowledge() (enable: ack-mode = MANUAL) |
| Batch listener | @KafkaListener(batch = "true") returns List<ConsumerRecord> |
| Concurrency | @KafkaListener(concurrency = "3") โ 3 consumer threads |
| Error handler | CommonErrorHandler bean (DefaultErrorHandler with BackOff) |
| Partition assignment | Use Kafka Streams or manually assign via ConsumerRebalanceListener |
spring.kafka.consumer.auto-offset-reset=earliest in development to avoid missing messages.@Configuration
public class SpringAiConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("""
You are a helpful customer support assistant
for an e-commerce platform. Be concise and friendly.
Today's date: ${java.time.LocalDate.now()}
""")
.defaultAdvisors(new MessageChatMemoryAdvisor(
new InMemoryChatMemory()))
.build();
}
}@RestController
@RequestMapping("/api/v1/ai")
public class AiController {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
// โโ Simple Chat โโ
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
// โโ Structured Output (returns Java object) โโ
@GetMapping("/analyze")
public SentimentResult analyze(@RequestParam String text) {
return chatClient.prompt()
.user("Analyze sentiment: " + text)
.call()
.entity(SentimentResult.class); // Auto-parsed
}
// โโ Tool Calling / Function Calling โโ
@GetMapping("/order-status")
public String orderStatus(@RequestParam String orderId) {
return chatClient.prompt()
.user("What is the status of order " + orderId + "?")
.functions("getOrderStatus") // registered tool
.call()
.content();
}
}
// โโ Tool Registration โโ
@Bean
@Description("Get the current status of an order by ID")
public Function<OrderStatusRequest, OrderStatusResponse> getOrderStatus() {
return request -> orderService
.getStatus(UUID.fromString(request.orderId()));
}// โโ Ingest Documents โโ
List<Document> docs = new PdfReader(
new FileSystemResource("knowledge.pdf")).get();
vectorStore.add(docs);
// โโ Similarity Search โโ
List<Document> relevant = vectorStore
.similaritySearch(
SearchRequest.builder()
.query("refund policy?")
.topK(5)
.similarityThreshold(0.7)
.build());
// โโ RAG with Advisor โโ
String answer = chatClient.prompt()
.user(u -> u.text("Answer: {question}")
.param("question", userQuestion))
.advisors(new QuestionAnswerAdvisor(vectorStore))
.call()
.content();| Category | Providers |
|---|---|
| Chat Models | OpenAI, Azure OpenAI, Anthropic, Google Vertex AI, Ollama, Mistral, Amazon Bedrock |
| Embedding Models | OpenAI, Azure, Ollama, Google, ONNX, Transformers (local) |
| Vector Stores | PGVector, Chroma, Pinecone, Redis, Milvus, Elasticsearch, Simple (in-memory) |
| Image Models | OpenAI DALL-E, Stability AI, Amazon Bedrock |
ChatClient for fluent API, VectorStore for RAG, Function Calling for tool use, and Advisors for cross-cutting concerns (memory, logging, guardrails). Add spring-ai-openai-spring-boot-starter to get started.@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCacheSpecification(
"expireAfterWrite=10m, maximumSize=1000");
manager.setCacheNames(List.of(
"users", "products", "categories"));
return manager;
}
}
// โโ Usage in Service Layer โโ
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id",
unless = "#result == null")
public ProductDto findById(UUID id) {
return productMapper.toDto(repository.findById(id)
.orElseThrow());
}
@CachePut(value = "products", key = "#result.id")
public ProductDto update(UUID id, UpdateProductRequest req) {
Product entity = repository.findById(id).orElseThrow();
productMapper.update(req, entity);
return productMapper.toDto(repository.save(entity));
}
@CacheEvict(value = "products", key = "#id")
public void delete(UUID id) {
repository.deleteById(id);
}
@Caching(evict = {
@CacheEvict(value = "products", allEntries = true),
@CacheEvict(value = "categories", key = "#req.category()")
})
public ProductDto create(CreateProductRequest req) { /*...*/ }
}| Annotation | Purpose | Key Behavior |
|---|---|---|
| @Cacheable | Return cached value or compute & cache | Does NOT execute method if cached |
| @CachePut | Always execute method & update cache | Always runs, updates the cache entry |
| @CacheEvict | Remove entry(es) from cache | Supports allEntries=true, beforeInvocation |
| @Caching | Group multiple cache operations | Nest @Cacheable, @CachePut, @CacheEvict |
| Provider | Type | Best For |
|---|---|---|
| Caffeine | In-process (local) | Single-instance, high-performance, TTL/size eviction |
| Redis | Distributed | Multi-instance, persistent, pub/sub, TTL |
| EhCache 3 | In-process (local) | JSR-107 compliant, disk overflow |
| Hazelcast | Distributed | In-memory data grid, multi-instance, queryable |
| GemFire | Distributed | High-scale, consistent, sub-ms latency |
spring-boot-starter-cache + caffeine. Always set unless = "#result == null" to avoid caching null values (cache penetration).| Endpoint | Path | Description |
|---|---|---|
| health | /actuator/health | Application health (DB, disk, etc.) |
| info | /actuator/info | Application info (build, git, env) |
| metrics | /actuator/metrics | Application metrics (CPU, memory, HTTP) |
| prometheus | /actuator/prometheus | Scrape format for Prometheus |
| env | /actuator/env | Environment properties (mask secrets!) |
| beans | /actuator/beans | All registered beans |
| mappings | /actuator/mappings | All request mappings |
| loggers | /actuator/loggers | View/modify log levels at runtime |
| threaddump | /actuator/threaddump | JVM thread dump |
| heapdump | /actuator/heapdump | JVM heap dump (HPROF) |
| startup | /actuator/startup | Startup timing events (Boot 2.4+) |
@Component
public class ExternalApiHealthIndicator
implements HealthIndicator {
private final ExternalApiClient apiClient;
@Override
public Health health() {
try {
apiClient.ping();
return Health.up()
.withDetail("responseTime", "45ms")
.withDetail("version", "2.1.0")
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.withDetail("error", e.getMessage())
.build();
}
}
}
// Custom info contributor
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("app",
Map.of("version", "1.0.0",
"features", List.of("v1", "v2")));
}
}management.endpoint.health.show-details=when_authorized and limit management.endpoints.web.exposure.include to only health,info,metrics,prometheus. Always secure actuator endpoints behind authentication.@AutoConfiguration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(prefix = "my.service",
name = "enabled", havingValue = "true",
matchIfMissing = true)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyServiceProperties props) {
return new MyService(props.getUrl(), props.getTimeout());
}
}
// Register in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
// com.example.MyServiceAutoConfiguration| Step | Command / Config |
|---|---|
| 1. Add plugin | org.graalvm.buildtools:native-maven-plugin (or Gradle equivalent) |
| 2. Build native | ./mvnw native:compile -Pnative or ./gradlew nativeCompile |
| 3. Output | Executable binary (no JVM needed) โ starts in ~milliseconds |
| 4. Reflection hints | @RegisterReflectionForBinding or reflect-config.json |
| 5. Test | @NativeBootTest instead of @SpringBootTest |
| 6. AOT | spring-boot-starter-aot (auto-configured for native) |
// โโ Virtual Threads (Java 21+, enabled by default in Boot 3.4+) โโ
// application.yml
// spring.threads.virtual.enabled: true
// โโ RestClient (Spring Boot 3.4+) โโ
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer {token}")
.requestFactory(new JdkClientHttpRequestFactory()) // uses virtual threads
.build();
}
@Bean
@Primary
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
// โโ CRaC (Coordinated Restore at Checkpoint) โโ
// Enables snapshotting running JVM state for instant startup
// Add: org.crac:crac dependency
// Build: -XX:CRaCCheckpointTo=checkpoint.jfr
// Restore: -XX:CRaCRestoreFrom=checkpoint.jfrExecutorService with platform threads.| Database | Starter | Use Case |
|---|---|---|
| PostgreSQL | spring-boot-starter-data-jpa + postgresql | Production RDBMS (ACID, JSON, full-text) |
| MySQL | spring-boot-starter-data-jpa + mysql | Production RDBMS (wide adoption) |
| H2 | spring-boot-starter-data-jpa + h2 | Dev/test embedded database (in-memory/file) |
| MongoDB | spring-boot-starter-data-mongodb | Document store (flexible schema) |
| Redis | spring-boot-starter-data-redis | Cache, sessions, pub/sub, rate limiting |
| Elasticsearch | spring-boot-starter-data-elasticsearch | Full-text search, log analytics |
@Configuration
public class DataSourceConfig {
@Bean @Primary
@ConfigurationProperties("app.datasource.primary")
public DataSourceProperties primaryProps() {
return new DataSourceProperties();
}
@Bean @Primary
@ConfigurationProperties("app.datasource.primary.hikari")
public DataSource primaryDataSource() {
return primaryProps()
.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.readonly")
public DataSourceProperties readonlyProps() {
return new DataSourceProperties();
}
@Bean("readOnlyDataSource")
@ConfigurationProperties("app.datasource.readonly.hikari")
public DataSource readOnlyDataSource() {
return readonlyProps()
.initializeDataSourceBuilder()
.type(HikariDataSource.class).build();
}
}-- โโ Flyway Versioned Migrations โโ
-- Location: src/main/resources/db/migration/
-- Naming: V{version}__{description}.sql
-- V1__create_users_table.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(30) NOT NULL DEFAULT 'USER',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- V2__add_users_indexes.sql
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
-- V3__add_audit_columns.sql
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ;
-- R__refresh_materialized_view.sql (Repeatable: runs if checksum changes)
REFRESH MATERIALIZED VIEW CONCURRENTLY analytics_view;
-- U3__undo_add_audit_columns.sql (Undo: flyway undo (requires Pro license))
-- ALTER TABLE users DROP COLUMN last_login;| Strategy | Example | Pros / Cons |
|---|---|---|
| URI Path | /api/v1/users | Simple, visible, cacheable |
| Header | Accept: application/vnd.myapp.v2+json | Clean URLs, client complexity |
| Query Param | /api/users?version=2 | Simple but mixes concerns |
// โโ Java Record (immutable DTO) โโ
public record UserDto(UUID id, String name, String email) {}
// โโ Lombok Entity (mutable JPA entity) โโ
@Entity
@Table(name = "users")
@Getter @Setter @NoArgsConstructor
@AllArgsConstructor @Builder
public class User {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String name;
private String email;
@CreatedDate
private Instant createdAt;
}
// โโ SLF4J Logging โโ
@Slf4j // Lombok annotation โ generates private static final Logger
@Service
public class OrderService {
public void process(Order order) {
log.info("Processing order {} for user {}",
order.getId(), order.getUserId());
log.debug("Order details: {}", order);
log.error("Failed to process order {}", order.getId(),
exception); // exception included with stack trace
}
}| Area | Check |
|---|---|
| Logging | Structured JSON logging, correlation IDs, no PII in logs |
| Config | Externalize ALL config (env vars), no secrets in code/repos |
| Security | HTTPS everywhere, CSRF disabled for REST API, rate limiting |
| Database | Flyway migrations, HikariCP tuned, connection pool monitoring |
| Errors | Global @ControllerAdvice, consistent error response format |
| Testing | Unit + integration tests, TestContainers, coverage > 80% |
| Monitoring | Actuator health/metrics, Prometheus + Grafana dashboards |
| Performance | Virtual threads enabled, caching (Caffeine/Redis), pagination |
| API Docs | SpringDoc OpenAPI (Swagger UI) at /swagger-ui.html |
| Containers | Multi-stage Docker build, non-root user, .dockerignore |
server:
servlet:
context-path: /api
shutdown: graceful # graceful shutdown
tomcat:
connection-timeout: 5s
max-connections: 8192
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
data:
jpa:
repositories:
bootstrap-mode: deferred # lazy init for faster startup
management:
endpoints:
health:
show-details: when_authorized
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
http.server.requests: trueserver.shutdown=graceful; (2) Use structured JSON logging โ logging.structured.format.enabled=true; (3) Externalize secrets โ never commit application-prod.yml; (4) Add spring-boot-starter-actuator + Prometheus; (5) Use springdoc-openapi-starter-webmvc-ui for Swagger docs.