Spring Boot · cheat sheet

Spring Boot

Spring Boot 3.x for interviews: DI and bean lifecycle, auto-configuration, config precedence, REST, JPA and N+1, @Transactional rules, JWT and testing.

Spring Boot 3.x (Spring Framework 6, Java 17+, jakarta.* packages) on one page, with notes where Boot 4 differs.

IoC, DI & beans

  • IoC: the container creates and wires objects; DI hands a bean its dependencies instead of it calling new.
  • Prefer constructor injection: final fields, required dependencies visible, testable without Spring. A single constructor needs no @Autowired.
  • Resolution is by type; break ties with @Primary or @Qualifier("name"). Inject List<PaymentGateway> to get every implementation.
  • Circular references are rejected by default since Boot 2.6. Redesign rather than set spring.main.allow-circular-references=true.
  • ApplicationContext extends BeanFactory with eager singletons, events, i18n, AOP and the Environment.
Scope One instance per Note
singleton (default) container must be stateless or thread-safe
prototype injection or getBean() no destroy callbacks
request / session HTTP request / session web only; injected through a proxy
application / websocket ServletContext / WebSocket session web only
  • Prototype into singleton is injected once. Use ObjectProvider<T>.getObject(), @Lookup, or a scoped proxy.
  • Lifecycle: constructor, dependency injection, *Aware callbacks, BeanPostProcessor before-init, @PostConstruct, afterPropertiesSet(), custom init method, BeanPostProcessor after-init (AOP proxies are created here), ready. On shutdown: @PreDestroy, destroy(), custom destroy method.

Core annotations

Annotation Purpose
@SpringBootApplication @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan of its package and below
@Component, @Service generic and service-layer beans found by scanning
@Repository DAO bean; translates persistence exceptions to DataAccessException
@Controller, @RestController MVC controller; @RestController adds @ResponseBody
@Configuration + @Bean factory methods, often for third-party classes; calls between @Bean methods return the same singleton (CGLIB proxy)
@Configuration(proxyBeanMethods = false) “lite” mode, no proxy, faster startup
@Value("${app.name:demo}") inject one property with a default
@ConfigurationProperties("app") bind a typed, validatable group of properties
@Profile("!prod") register the bean only for matching profiles
@Lazy, @DependsOn, @Order, @Import defer creation, force order, sort lists, pull in config

Auto-configuration & starters

  • @EnableAutoConfiguration loads classes listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (Boot 3 no longer reads them from spring.factories).
  • Each is guarded by conditions: @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, @ConditionalOnBean, @ConditionalOnWebApplication.
  • It backs off: define your own DataSource or ObjectMapper bean and Boot’s is skipped.
  • See why with --debug (condition evaluation report) or /actuator/conditions. Exclude with @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) or spring.autoconfigure.exclude.
  • A custom starter is an @AutoConfiguration class, listed in that imports file, plus a starter POM.
Starter Brings
spring-boot-starter-web Spring MVC, embedded Tomcat, Jackson (renamed -webmvc in Boot 4)
spring-boot-starter-webflux reactive stack on Reactor Netty
spring-boot-starter-data-jpa Hibernate, Spring Data JPA, HikariCP
spring-boot-starter-validation Hibernate Validator (not included in -web)
spring-boot-starter-security Spring Security, secured by default
spring-boot-starter-actuator health, metrics, management endpoints
spring-boot-starter-test JUnit 5, Mockito, AssertJ, Spring Test, JsonPath

Configuration & profiles

spring:
  application.name: orders
  profiles.active: dev
app:
  payment-timeout: 2s
---
spring.config.activate.on-profile: prod
server.port: 8080
yaml
@ConfigurationProperties(prefix = "app")      // enable with @ConfigurationPropertiesScan
record AppProps(Duration paymentTimeout, @DefaultValue("3") int retries) {}
java
  • Precedence, highest first: command-line args (--server.port=9000), SPRING_APPLICATION_JSON, Java system properties (-D), OS environment variables, profile-specific files outside the jar, application.yml outside the jar, profile-specific files inside it, application.yml inside it, @PropertySource, defaults. Test annotations such as @TestPropertySource beat all of them.
  • .properties wins over .yml in the same location.
  • Relaxed binding: app.payment-timeout, app.paymentTimeout and env var APP_PAYMENTTIMEOUT are the same key. spring.datasource.url becomes SPRING_DATASOURCE_URL.
  • Profiles: application-{profile}.yml, spring.profiles.active, groups (spring.profiles.group.prod=db,mq). Secrets come from the environment or a vault, never the jar.
  • Config Server: spring.config.import=optional:configserver:http://config:8888.

REST controllers

@RestController
@RequestMapping("/api/users")
class UserController {
  private final UserService users;
  UserController(UserService users) { this.users = users; }

  @GetMapping("/{id}")
  UserDto get(@PathVariable Long id) { return users.find(id); }

  @PostMapping @ResponseStatus(HttpStatus.CREATED)
  UserDto create(@Valid @RequestBody CreateUser req) { return users.create(req); }
}
java
Annotation Binds or does
@GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping shortcuts for @RequestMapping(method = ...)
@PathVariable URI template segment, /users/{id}
@RequestParam query or form parameter; required by default; defaultValue = "0"
@RequestBody JSON body via HttpMessageConverter (Jackson)
@RequestHeader, @CookieValue header or cookie value
@ResponseStatus fixed status for the handler or an exception class
ResponseEntity<T> full control: ResponseEntity.created(uri).body(dto)
@CrossOrigin CORS for one controller (use global config with Security)
  • Request flow: DispatcherServlet, HandlerMapping, HandlerAdapter, controller, then HttpMessageConverter writes the body.
  • Return DTOs, not entities: avoids lazy-loading errors, JSON cycles and leaking columns.
  • Blocking HTTP clients: RestClient (6.1) or RestTemplate; reactive: WebClient.

Validation & error handling

record CreateUser(@NotBlank String name, @NotNull @Email String email, @Min(18) int age) {}
java
  • @NotNull rejects null; @NotEmpty also rejects "" and empty collections; @NotBlank also rejects whitespace-only strings.
  • @Valid @RequestBody failures throw MethodArgumentNotValidException (400). Constraints placed directly on @RequestParam or @PathVariable raise HandlerMethodValidationException (built in since 6.1); a class-level @Validated switches to AOP validation and ConstraintViolationException.
  • Custom rule: a @Constraint(validatedBy = ...) annotation plus a ConstraintValidator. @Valid on a field cascades.
@RestControllerAdvice
class ApiErrors {
  @ExceptionHandler                                       // type comes from the parameter
  ProblemDetail notFound(NotFoundException ex) {          // RFC 9457 JSON body
    return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
  }
  @ExceptionHandler ProblemDetail invalid(MethodArgumentNotValidException ex) {
    var pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
    ex.getFieldErrors().forEach(e -> pd.setProperty(e.getField(), e.getDefaultMessage()));
    return pd;
  }
}
java
  • spring.mvc.problemdetails.enabled=true makes Spring MVC’s own exceptions answer as ProblemDetail; extending ResponseEntityExceptionHandler does the same inside your advice.
  • Throw ResponseStatusException(HttpStatus.CONFLICT, "...") for one-offs. Unhandled errors fall through to Boot’s /error (BasicErrorController).

Spring Data JPA

public interface OrderRepository extends JpaRepository<Order, Long> {
  List<Order> findByStatusAndCreatedAtAfter(Status status, Instant since);
  Page<Order> findByCustomerId(Long customerId, Pageable pageable);

  @Query("select o from Order o join fetch o.items where o.status = :s")
  List<Order> findWithItems(@Param("s") Status status);         // one query, no N+1

  @EntityGraph(attributePaths = "items")
  List<Order> findByCustomerEmail(String email);                // same fix, declarative
  @Modifying @Query("update Order o set o.status = :s where o.id in :ids")
  int updateStatus(@Param("s") Status s, @Param("ids") List<Long> ids);
}
java
  • Hierarchy: Repository, CrudRepository/ListCrudRepository, PagingAndSortingRepository, JpaRepository (adds flush and batch methods). JpaSpecificationExecutor for dynamic filters.
  • Derived-query keywords: And, Or, Between, LessThan, Like, Containing, In, IsNull, IgnoreCase, OrderBy...Desc, findTop10By, existsBy, countBy. Projections: interfaces or DTO records.
  • @Modifying queries need a surrounding @Transactional.
  • save() persists a new entity or merges a detached one. Managed entities are dirty-checked and flushed at commit, with no save() call needed.
  • @Version gives optimistic locking; @Lock(LockModeType.PESSIMISTIC_WRITE) gives SELECT ... FOR UPDATE.
Association Default fetch
@ManyToOne, @OneToOne EAGER (make it LAZY)
@OneToMany, @ManyToMany, @ElementCollection LAZY
  • N+1: one query for N parents, then one per parent for a lazy association. Fix with join fetch, @EntityGraph, hibernate.default_batch_fetch_size, or DTO projections. Spot it with logging.level.org.hibernate.SQL=debug.
  • Fetch-joining a collection with paging makes Hibernate paginate in memory; fetching two List collections at once throws MultipleBagFetchException.
  • LazyInitializationException means you touched a lazy association after the session closed: fetch it in the query or map to a DTO inside the transaction.

@Transactional rules

Propagation Existing transaction No transaction
REQUIRED (default) join it start one
REQUIRES_NEW suspend it, start a new one start one
NESTED savepoint inside it (JDBC) start one
SUPPORTS join it run without
NOT_SUPPORTED suspend it run without
MANDATORY join it throw
NEVER throw run without
  • It works through a proxy: only calls from outside the bean are intercepted. Self-invocation (this.save()) runs with no transaction. Fix: move the method to another bean, use TransactionTemplate, or AspectJ mode.
  • Proxies can’t intercept private methods, and CGLIB can’t override final ones. Since 6.0, protected and package-private methods work with class-based proxies.
  • Rollback happens by default only for RuntimeException and Error. Checked exceptions commit unless you set rollbackFor = Exception.class.
  • Swallowing an exception thrown from a joined REQUIRED method still marks the transaction rollback-only: the outer commit throws UnexpectedRollbackException.
  • readOnly = true is a hint (Hibernate skips dirty checking). Keep transactions in the service layer and short: no remote calls inside.

Security & JWT

@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
  return http
      .csrf(csrf -> csrf.disable())                       // only for stateless token APIs
      .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .authorizeHttpRequests(auth -> auth
          .requestMatchers("/actuator/health", "/auth/**").permitAll()
          .requestMatchers(HttpMethod.DELETE, "/api/**").hasAuthority("SCOPE_admin")
          .anyRequest().authenticated())
      .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
      .build();
}
java
  • Filter chain: the servlet DelegatingFilterProxy calls FilterChainProxy, which runs the first matching SecurityFilterChain: exploit protection (CsrfFilter), then authentication filters (BearerTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter), then ExceptionTranslationFilter and AuthorizationFilter.
  • Authentication: AuthenticationManager (ProviderManager) asks AuthenticationProviders (such as DaoAuthenticationProvider with a UserDetailsService and PasswordEncoder); the result sits in SecurityContextHolder, a ThreadLocal by default.
  • Not authenticated gives 401 via AuthenticationEntryPoint; authenticated but not allowed gives 403 via AccessDeniedHandler.
  • Security 6: WebSecurityConfigurerAdapter is gone (declare SecurityFilterChain beans), authorizeHttpRequests plus requestMatchers replace authorizeRequests/antMatchers, and @EnableMethodSecurity enables @PreAuthorize("hasRole('ADMIN')").
  • hasRole("ADMIN") checks authority ROLE_ADMIN. Hash passwords with BCryptPasswordEncoder or the delegating encoder ({bcrypt} prefix).

JWT flow

  1. The client authenticates with the authorization server and receives a signed JWT (header.payload.signature, Base64URL-encoded, not encrypted).
  2. It sends Authorization: Bearer <token> on every request.
  3. BearerTokenAuthenticationFilter extracts it; JwtDecoder verifies the signature against the JWKS from spring.security.oauth2.resourceserver.jwt.issuer-uri and checks exp, nbf and iss (aud only if configured).
  4. JwtAuthenticationConverter maps scopes to SCOPE_* authorities (customize it to turn a roles claim into ROLE_*), then authorization runs as usual.
  • Keep access tokens short-lived with refresh tokens; revoking a JWT early needs a denylist. Never put secrets in the payload.

Actuator

Endpoint Shows
/actuator/health UP/DOWN; /health/liveness and /health/readiness probes (auto-enabled on Kubernetes)
/actuator/info build and git info
/actuator/metrics, /actuator/prometheus Micrometer metrics; Prometheus format needs micrometer-registry-prometheus
/actuator/env, /actuator/configprops resolved properties (sanitized)
/actuator/beans, /actuator/conditions, /actuator/mappings beans, auto-config report, request mappings
/actuator/loggers view or change log levels at runtime (POST)
/actuator/threaddump, /actuator/heapdump JVM diagnostics
/actuator/shutdown graceful shutdown, disabled by default
  • Only health is exposed over HTTP by default: management.endpoints.web.exposure.include=health,info,prometheus.
  • management.endpoint.health.show-details defaults to never. Put management on its own port with management.server.port and secure env and heapdump.
  • Custom checks implement HealthIndicator. Tracing uses Micrometer Tracing (Sleuth’s replacement in Boot 3) with OpenTelemetry or Zipkin.

Testing

Annotation Loads Pair with
@SpringBootTest full context, mock web environment by default webEnvironment = RANDOM_PORT, @AutoConfigureMockMvc
@WebMvcTest(UserController.class) MVC slice: controllers, advice, converters MockMvc, mocked services
@DataJpaTest JPA slice, embedded DB, rollback after each test TestEntityManager
@JsonTest, @RestClientTest Jackson or REST-client slice JacksonTester, MockRestServiceServer
@MockitoBean, @MockitoSpyBean replace a bean with a mock or spy Framework 6.2+
@ServiceConnection wire a Testcontainers container into Boot (3.1+) @Testcontainers, @Container
@ActiveProfiles, @TestPropertySource, @DynamicPropertySource test profiles and properties
@WithMockUser run as a fake authenticated user spring-security-test
@WebMvcTest(UserController.class)
class UserControllerTest {
  @Autowired MockMvc mvc;
  @MockitoBean UserService users;

  @Test void returnsUser() throws Exception {
    given(users.find(1L)).willReturn(new UserDto(1L, "Ada"));
    mvc.perform(get("/api/users/1"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.name").value("Ada"));
  }
}
java
  • @MockBean/@SpyBean were deprecated in 3.4 and removed in Boot 4: use @MockitoBean/@MockitoSpyBean.
  • Spring caches contexts across tests; every distinct mock set or @DirtiesContext forces a new, slow context.
  • Plain unit tests need no Spring at all: @ExtendWith(MockitoExtension.class) with @Mock/@InjectMocks.

Caching, async & scheduling

@Cacheable(cacheNames = "products", key = "#id")
public Product find(Long id) { return repo.findById(id).orElseThrow(); }

@CacheEvict(cacheNames = "products", key = "#p.id")
public void update(Product p) { repo.save(p); }

@Scheduled(cron = "0 0 2 * * *", zone = "UTC")   // sec min hour day month weekday
public void nightly() { reports.rebuild(); }
java
  • Switch each on with @EnableCaching, @EnableAsync, @EnableScheduling. All are proxy-based, so self-invocation skips them too.
  • Caching: @CachePut updates, @CacheEvict(allEntries = true) clears; condition, unless, sync = true. With no provider, Boot uses an in-memory ConcurrentHashMap; add Caffeine or Redis for TTLs and eviction.
  • @Async returns void or CompletableFuture<T> and runs on Boot’s ThreadPoolTaskExecutor (8 core threads; tune spring.task.execution.pool.*). Exceptions from void methods go to AsyncUncaughtExceptionHandler.
  • @Scheduled takes fixedRate, fixedDelay, initialDelay or a 6-field cron. The scheduler has one thread by default (spring.task.scheduling.pool.size), and every instance runs the job: use ShedLock or similar in a cluster.
  • spring.threads.virtual.enabled=true (3.2+, Java 21) moves Tomcat, @Async and scheduling onto virtual threads.

Spring Cloud & Resilience4j

Need Tool
API gateway Spring Cloud Gateway (reactive, plus a Servlet variant)
Service discovery Eureka, Consul, or plain Kubernetes DNS
Client-side load balancing Spring Cloud LoadBalancer (Ribbon is gone)
Declarative HTTP client OpenFeign @FeignClient, or Spring’s own @HttpExchange interfaces
Central config Config Server with spring.config.import=configserver:
Messaging Spring Cloud Stream binders for Kafka and RabbitMQ
Resilience Resilience4j (Hystrix is gone)
@CircuitBreaker(name = "inventory", fallbackMethod = "stockFallback")
@Retry(name = "inventory")
public Stock stock(String sku) { return inventoryClient.get(sku); }

public Stock stockFallback(String sku, Throwable ex) { return Stock.unknown(sku); }
java
resilience4j:
  circuitbreaker.instances.inventory:
    sliding-window-size: 20
    failure-rate-threshold: 50
    wait-duration-in-open-state: 10s
  retry.instances.inventory:
    max-attempts: 3            # includes the first call
    wait-duration: 200ms
    enable-exponential-backoff: true
yaml
  • Circuit breaker states: CLOSED, OPEN (calls fail fast with CallNotPermittedException), HALF_OPEN (a few trial calls). Defaults: count-based window of 100 calls, 50% failure threshold, 60 s open.
  • The fallback lives in the same class with the same parameters plus a trailing exception.
  • Default aspect order: Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(call))))), so each retry passes through the breaker. Also @Bulkhead, @RateLimiter, @TimeLimiter.

Quick answers

  • Spring vs Spring Boot? Spring is the framework (DI, MVC, data); Boot adds auto-configuration, starters, an embedded server and production features.
  • What does @SpringBootApplication do? Configuration, auto-configuration and component scanning from its package down.
  • How does auto-configuration work? Conditional @AutoConfiguration classes from the imports file that back off when you define your own bean.
  • @Component vs @Bean? Class-level scanning for your classes vs a factory method, typically for third-party classes.
  • Are singleton beans thread-safe? No. Keep them stateless; shared mutable state needs synchronization.
  • Run code at startup? CommandLineRunner or ApplicationRunner beans, or @EventListener(ApplicationReadyEvent.class).
  • @Value vs @ConfigurationProperties? One value (SpEL allowed) vs a typed, validated group with relaxed binding.
  • Why isn’t my @Transactional working? Self-invocation, a private method, a checked exception, or a bean Spring didn’t create.
  • Fix N+1? join fetch, @EntityGraph, batch fetching or DTO projections.
  • Swap Tomcat? Exclude spring-boot-starter-tomcat and add spring-boot-starter-jetty.
  • Boot 2 to 3 migration? Java 17, javax.* to jakarta.*, Security 6 filter-chain beans, Sleuth to Micrometer Tracing.
  • Secure a REST API? Stateless resource server validating JWTs, URL rules plus @PreAuthorize, HTTPS, CORS configured in Security.

Gotchas & traps

  • @Transactional, @Cacheable, @Async and @PreAuthorize are ignored on self-invocation and private methods.
  • Checked exceptions don’t roll back: a throws IOException method commits half its work.
  • spring.jpa.open-in-view is true by default (Boot logs a warning): lazy loading in the web layer hides N+1. Set it to false.
  • ddl-auto defaults to create-drop only for embedded databases. Never run update in production; use Flyway or Liquibase.
  • Lombok @Data on entities: equals, hashCode and toString touch lazy collections and bidirectional links (loops, exceptions).
  • GenerationType.IDENTITY disables Hibernate’s JDBC insert batching; SEQUENCE keeps it.
  • Beans outside the main class’s package aren’t scanned; @Value on a static field is never injected.
  • Spring 6.1 no longer reads parameter names from debug info: without -parameters (Boot’s build plugins set it), write @PathVariable("id").
  • Exposing env, heapdump or * actuator endpoints publicly leaks secrets.

Gotcha

Boot 3 moved to jakarta.*. Old javax.persistence or javax.validation imports fail to compile, or, if a stale jar is still on the classpath, compile and are silently ignored.

esc