Spring Boot – Cheat Sheet

August 16, 20265 min readUpdated 8/18/2026

Everything from this track on one page, grouped by what you are trying to do rather than by topic. Written to be scanned. Each section links to the lesson behind it.

All of it is Spring Boot 4.1 on Java 21. Where Boot 3 differs, it is marked ⚠️.

Start a project

curl https://start.spring.io/starter.zip \
  -d type=maven-project -d language=java \
  -d bootVersion=4.1.0 -d javaVersion=21 \
  -d dependencies=web,data-jpa,validation,lombok \
  -o demo.zip && unzip demo.zip -d demo
./mvnw spring-boot:run                        # run
./mvnw spring-boot:run -Dspring-boot.run.arguments=--debug   # + condition report
./mvnw test                                   # test
./mvnw clean compile                          # when behaviour ≠ source
./mvnw spotless:apply                         # format

⚠️ Boot 3 → Boot 4 renames

Lesson 3

Boot 3Boot 4 / Framework 7
spring-boot-starter-webspring-boot-starter-webmvc
spring-boot-starter-aopspring-boot-starter-aspectj
spring-boot-starter-testspring-boot-starter-webmvc-test
liquibase-corespring-boot-starter-liquibase
boot.autoconfigure.jms.*boot.jms.autoconfigure.*
MappingJackson2MessageConverterJacksonJsonMessageConverter
spring-retry + @EnableRetrybuilt in + @EnableResilientMethods
@MockBean@MockitoBean
Java 17Java 21

Wire a bean

Lesson 5 · 6

@Service @Repository @RestController @Component   // your classes
@Configuration + @Bean                            // someone else's classes

// Constructor injection — the default. No @Autowired needed.
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final ProductDAO productDAO;
    private final PizzaProperties properties;
}

// Optional dependency — degrades instead of failing startup
private final ObjectProvider<JavaMailSender> senderProvider;
JavaMailSender sender = senderProvider.getIfAvailable();   // null if absent

@Primary            // the default when two beans fit
@Qualifier("name")  // pick one explicitly; beats @Primary
@Scope("prototype") // new instance per injection point

@PostConstruct void init() { }      // after injection — validate config here
@PreDestroy   void close() { }      // on graceful shutdown

Configure it

Lesson 7

@Validated
@ConfigurationProperties(prefix = "pizza")
public record PizzaProperties(@Valid Jwt jwt, @Valid Pricing pricing) {
    public record Jwt(@NotBlank @Size(min = 32) String secret, @Positive long expirationMinutes) {}
    public record Pricing(@DecimalMin("0.0") @DecimalMax("1.0") BigDecimal taxRate) {}
}

@SpringBootApplication
@ConfigurationPropertiesScan          // finds them all
public class Application { }
pizza.jwt.secret=${JWT_SECRET:}       # env var with a default after the colon
spring.profiles.active=local          # profile → application-local.properties

Precedence, lowest first: jar properties → profile → env vars → -D → command line.

Expose an endpoint

Lesson 11 · 10

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

    @GetMapping                       public List<ProductDTO> list(
            @RequestParam(required = false) ProductType type) { }
    @GetMapping("/{id}")              public ProductDTO get(@PathVariable UUID id) { }
    @PostMapping                      public ResponseEntity<ProductDTO> create(
            @Valid @RequestBody ProductCreateDTO dto) { }
    @PutMapping("/{id}")              // replace
    @PatchMapping("/{id}/deactivate") // partial state change
    @DeleteMapping("/{id}")           public ResponseEntity<Void> delete(@PathVariable UUID id) {
        return ResponseEntity.noContent().build();       // 204
    }
}
@RequestParam(defaultValue = "0") int page
@RequestHeader("Stripe-Signature") String signature
@RequestPart("file") MultipartFile file          // + consumes = MULTIPART_FORM_DATA_VALUE
Authentication authentication                     // the current caller

200 ok · 201 created · 204 no content · 400 your fault · 401 who are you · 403 no · 404 not there · 409 conflict · 500 our fault.

Handle errors

Lesson 12

@RestControllerAdvice
public class RestExceptionHandler {

    @ExceptionHandler(ApiException.class)                     // yours
    @ExceptionHandler(MethodArgumentNotValidException.class)  // @Valid failed
    @ExceptionHandler(HttpMessageNotReadableException.class)  // unparseable body → 400
    @ExceptionHandler(MethodArgumentTypeMismatchException.class) // bad path var → 400
    @ExceptionHandler(AccessDeniedException.class)            // → 403
    @ExceptionHandler(Exception.class)                        // catch-all → 500, log only
}

throw ApiException.notFound("Product", id);
throw ApiException.badRequest("An email address is required");

⚠️ Filter-level security errors bypass this — configure authenticationEntryPoint / accessDeniedHandler instead.

Query a database

Lesson 16 · 17

@Entity
@SQLRestriction("deleted = false")                    // soft delete, JPA reads only
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
    @JdbcTypeCode(SqlTypes.CHAR) private UUID publicId;   // MySQL: else BINARY(16)
    @Enumerated(EnumType.STRING) private ProductType type; // NEVER ordinal
    @CreationTimestamp private LocalDateTime createdAt;
    @UpdateTimestamp   private LocalDateTime updatedAt;

    @ToString.Exclude @EqualsAndHashCode.Exclude
    @OneToMany(mappedBy = "product", cascade = ALL, orphanRemoval = true, fetch = LAZY)
    private List<ProductSize> sizes = new ArrayList<>();
}
// Derived query
Optional<User> findByEmailIgnoreCase(String email);
boolean existsByEmail(String email);

// JOIN FETCH kills N+1
@Query("SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.sizes WHERE p.publicId = :id")
Optional<Product> findByPublicIdWithSizes(@Param("id") UUID id);

// JdbcTemplate for aggregates
String query = """
        SELECT COUNT(*) AS total_orders, COALESCE(SUM(o.total), 0) AS total_revenue
        FROM customer_order o
        WHERE o.deleted = 0 AND o.created_at >= :from
        """;
jdbcTemplate.queryForObject(query, Map.of("from", from), summaryRowMapper);
spring.jpa.hibernate.ddl-auto=validate    # never `update`
spring.jpa.open-in-view=false             # surfaces N+1 in development
spring.jpa.show-sql=true

⚠️ Hand-written SQL must filter deleted = 0 itself. @SQLRestriction only applies to queries Hibernate builds.

Secure it

Lesson 24 · 26

@Configuration @EnableWebSecurity @EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable())      // ONLY safe for header-based tokens
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/login").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/orders/mine").authenticated()  // specific
                .requestMatchers(HttpMethod.GET, "/api/orders/*").permitAll()         // general
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())                                        // default deny
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
}
@PreAuthorize("hasRole('ADMIN')")                          // = authority ROLE_ADMIN
@PreAuthorize("#email == authentication.name")
@PreAuthorize("@orderSecurity.isOwner(#id, authentication.name)")

First match wins — specific rules before general ones.

Cross-cutting concerns

Lesson 8 · 9 · 21 · 28 · 29

// AOP
@Aspect @Component
@Pointcut("execution(* com.pizza.api.entity..*ServiceImpl.*(..))")
@Around("serviceLayer()") public Object time(ProceedingJoinPoint jp) throws Throwable {
    try { return jp.proceed(); } finally { /* … */ }
}

// Events
events.publishEvent(OrderPlacedEvent.from(saved));
@EventListener                                              // inside the transaction
@TransactionalEventListener(phase = AFTER_COMMIT)           // after commit — use this
@Order(10)                                                  // sequence listeners

// Caching
@EnableCaching
@Cacheable(value = "menu", key = "#type")
@Caching(evict = { @CacheEvict(value = "menu", allEntries = true) })   // EVERY write path

// Async + scheduling
@EnableAsync @EnableScheduling
@Bean(name = "taskExecutor")     // the name matters
@Async  @Scheduled(cron = "0 0 3 * * *")  @Scheduled(fixedDelay = 60000)

// Retry — Framework 7, no dependency
@EnableResilientMethods
@Retryable(includes = {ApiConnectionException.class}, maxRetries = 3,
           delay = 200, multiplier = 2.0, maxDelay = 2000, jitter = 100)

⚠️ The proxy rule

Lesson 8 — the most useful thing on this page.

public void a() { b(); }   // b()'s annotations DO NOT RUN — self-invocation, no proxy
@Transactional public void b() { }

Applies identically to @Transactional, @Cacheable, @Async, @Retryable, @PreAuthorize and every @Aspect. All fail silently. Also: private and final methods are never advised.

Map and reduce boilerplate

Lesson 19 · 20

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface EntityDTOMapper {
    @Mapping(target = "id", source = "publicId")     // never expose the BIGINT
    ProductDTO mapProductToProductDTO(Product product);

    @Mapping(target = "id", ignore = true)           // mass-assignment guard
    @Mapping(target = "deleted", ignore = true)
    Product mapProductCreateDTOToProduct(ProductCreateDTO dto);
}

⚠️ Processor order: lombok → lombok-mapstruct-binding → mapstruct-processor. Wrong order compiles and maps nothing.

@Slf4j                      // log.info("Created {}", id)
@RequiredArgsConstructor    // constructor from final fields
@Builder @Builder.Default   // Default or initialisers are IGNORED
@Getter @Setter
@ToString.Exclude           // on every secret and every collection
// NEVER @Data on a JPA entity

Test it

Lesson 32

@WebMvcTest(ProductRestController.class)   // web layer only
@DataJpaTest                                // JPA only, rolled back
@SpringBootTest @AutoConfigureMockMvc       // everything
@Transactional                              // roll back after each test
@MockitoBean private ProductService service;
@WithMockUser(roles = "ADMIN")

mockMvc.perform(get("/api/products"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$[0].name").value("Pepperoni"));

assertThat(priced.total()).isEqualByComparingTo("22.42");   // NOT isEqualTo for BigDecimal

Ten things that fail silently

  1. Self-invocation — every proxy annotation stops working.
  2. Missing @EnableCaching / @EnableAsync / @EnableMethodSecurity — annotations inert.
  3. @Valid missing on the parameter — no validation runs.
  4. MapStruct processor order — mappers map nothing.
  5. Hand-written SQL and soft deletes — deleted rows counted.
  6. A cache name typo — reads and evictions use different caches.
  7. Executor bean not named taskExecutor — unbounded default pool.
  8. @EnumType.ORDINAL (the default) — reordering the enum corrupts every row.
  9. @Builder without @Builder.Default — initialisers ignored.
  10. Missing autoconfiguration in Boot 4 — fails far from the cause. Use --debug.

Next: interview questions.