Spring Boot Expert Examples

Externalized from the agent definition per the few-shot-examples rule (#1587).

Spring Boot Expert — Worked Examples

Externalized from the agent definition per the few-shot-examples rule (#1587).

Your Process

1. Project Assessment

# Check Spring Boot version and dependency health
./mvnw dependency:tree | grep "spring-boot"

# Identify slow startup components
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-verbose:class" 2>&1 | grep "Loaded" | wc -l

# Check for common anti-patterns
grep -r "FetchType.EAGER" src/ --include="*.java" --include="*.kt"
grep -r "new RestTemplate()" src/ --include="*.java" --include="*.kt"

# Run static analysis
./mvnw spotbugs:check pmd:check

2. Spring Security Configuration

// SecurityConfig.java — method security + JWT filter chain
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            final String jwt = authHeader.substring(7);
            final String userEmail = jwtService.extractUsername(jwt);

            if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
                if (jwtService.isTokenValid(jwt, userDetails)) {
                    UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        } catch (JwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

3. JPA/Hibernate Optimization

// Entity design — explicit fetch strategies and N+1 prevention
@Entity
@Table(name = "orders",
    indexes = {
        @Index(name = "idx_orders_user_id", columnList = "user_id"),
        @Index(name = "idx_orders_status_created", columnList = "status, created_at DESC")
    })
@NamedEntityGraph(
    name = "Order.withItemsAndProducts",
    attributeNodes = {
        @NamedAttributeNode(value = "items", subgraph = "items-subgraph")
    },
    subgraphs = @NamedSubgraph(
        name = "items-subgraph",
        attributeNodes = @NamedAttributeNode("product")
    )
)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
    @SequenceGenerator(name = "order_seq", sequenceName = "order_id_seq", allocationSize = 50)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)  // Always LAZY — load explicitly when needed
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;
}

// Repository with JPQL and entity graph
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Use entity graph to avoid N+1 in list queries
    @EntityGraph("Order.withItemsAndProducts")
    List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);

    // Projection for list views — only fetch what's needed
    @Query("SELECT new com.example.dto.OrderSummary(o.id, o.status, o.createdAt, COUNT(i)) " +
           "FROM Order o LEFT JOIN o.items i " +
           "WHERE o.user.id = :userId " +
           "GROUP BY o.id, o.status, o.createdAt")
    List<OrderSummary> findSummariesByUserId(@Param("userId") Long userId);

    // Bulk status update — no entity loading needed
    @Modifying
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int bulkUpdateStatus(@Param("ids") List<Long> ids, @Param("status") OrderStatus status);
}

4. WebFlux Reactive Pipeline

// Reactive controller with WebFlux
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    private final ProductService productService;

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Product> streamProducts(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size
    ) {
        return productService.findAll(PageRequest.of(page, size))
            .delayElements(Duration.ofMillis(100))  // Back-pressure example
            .onErrorResume(ex -> {
                log.error("Error streaming products", ex);
                return Flux.error(new ResponseStatusException(
                    HttpStatus.INTERNAL_SERVER_ERROR, "Stream failed"));
            });
    }

    @PostMapping
    public Mono<ResponseEntity<Product>> create(@RequestBody @Valid Mono<CreateProductRequest> request) {
        return request
            .flatMap(productService::create)
            .map(product -> ResponseEntity.status(HttpStatus.CREATED).body(product))
            .onErrorMap(DataIntegrityViolationException.class, ex ->
                new ResponseStatusException(HttpStatus.CONFLICT, "Product SKU already exists"));
    }
}

// Service with reactive composition
@Service
public class ProductService {

    private final ProductRepository repository;
    private final ReactiveRedisTemplate<String, Product> cache;
    private final StockServiceClient stockClient;

    public Mono<Product> findById(Long id) {
        String cacheKey = "product:" + id;
        return cache.opsForValue().get(cacheKey)
            .switchIfEmpty(
                repository.findById(id)
                    .switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
                    .flatMap(product ->
                        cache.opsForValue()
                             .set(cacheKey, product, Duration.ofHours(1))
                             .thenReturn(product))
            );
    }

    // Parallel calls with zipWith
    public Mono<ProductDetail> findDetailById(Long id) {
        return Mono.zip(
            findById(id),
            stockClient.getStock(id).defaultIfEmpty(StockInfo.UNAVAILABLE)
        ).map(tuple -> ProductDetail.of(tuple.getT1(), tuple.getT2()));
    }
}

5. Configuration and Profile Management

# application.yml — base configuration
spring:
  application:
    name: myapp
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: ${DB_POOL_SIZE:10}
      minimum-idle: 2
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
  jpa:
    open-in-view: false   # CRITICAL: disable OSIV in production
    hibernate:
      ddl-auto: validate   # Never 'update' or 'create' in production
    properties:
      hibernate:
        default_batch_fetch_size: 100   # Batch fetching for collections
        generate_statistics: false
  cache:
    type: redis
  data:
    redis:
      url: ${REDIS_URL:redis://localhost:6379}

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      probes:
        enabled: true   # /actuator/health/liveness and /readiness for K8s

---
# application-prod.yml — production overrides
spring:
  config:
    activate:
      on-profile: prod
  jpa:
    properties:
      hibernate:
        generate_statistics: false

server:
  tomcat:
    threads:
      max: ${TOMCAT_MAX_THREADS:200}
      min-spare: ${TOMCAT_MIN_THREADS:10}
    connection-timeout: 5000
    accept-count: 100

logging:
  level:
    root: WARN
    com.example: INFO
  pattern:
    console: '{"timestamp":"%d","level":"%p","logger":"%c","message":"%m","thread":"%t"}%n'

6. GraalVM Native Image

<!-- pom.xml — native profile -->
<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.buildtools</groupId>
          <artifactId>native-maven-plugin</artifactId>
          <configuration>
            <buildArgs>
              <buildArg>--initialize-at-build-time=org.slf4j</buildArg>
              <buildArg>-H:+ReportExceptionStackTraces</buildArg>
              <buildArg>--strict-image-heap</buildArg>
            </buildArgs>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>
// Native hints for reflection-heavy code
@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
public class NativeConfig {

    static class MyRuntimeHints implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            // Register classes that use reflection
            hints.reflection()
                .registerType(OrderEvent.class, MemberCategory.values())
                .registerType(ProductDto.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                    MemberCategory.DECLARED_FIELDS);
            // Register resources
            hints.resources().registerPattern("messages/*.properties");
        }
    }
}
# Build and verify native image
./mvnw -Pnative native:compile -DskipTests
./mvnw -Pnative test -Dspring.test.context.cache.maxSize=1

# Measure startup improvement
time ./target/myapp   # vs: time java -jar target/myapp.jar

Few-Shot Examples

Example 1: Configuration Review — OSIV and DDL Anti-Pattern

Input: "Our app is slow in production and we're seeing random DB connection timeouts"

Diagnosis:

# Found in application.yml — two critical misconfigurations
spring:
  jpa:
    open-in-view: true    # PROBLEM 1: holds DB connection for entire HTTP request
    hibernate:
      ddl-auto: update    # PROBLEM 2: modifies schema on startup — dangerous in prod

Fix:

spring:
  jpa:
    open-in-view: false   # Release DB connection when service layer returns
    hibernate:
      ddl-auto: validate  # Only validate schema matches — never modify
  datasource:
    hikari:
      maximum-pool-size: 20   # Size based on: DB max_connections / (instances * avg_hold_time)
      leak-detection-threshold: 10000  # Alert on connections held >10s

Result: Connection pool exhaustion eliminated. Timeouts resolved.


Example 2: Spring Security — Role-Based with Method Security

Input: "We need admins to manage all orders, users to see only their own"

// SecurityConfig.java
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain chain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/orders/**").authenticated()
                .anyRequest().authenticated())
            .build();
    }
}

// OrderService.java — method-level authorization
@Service
public class OrderService {

    private final OrderRepository repository;

    // ADMIN can access any order; USER can access only their own
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
    public Order findById(Long orderId) {
        return repository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }
}

// OrderSecurityEvaluator.java — SpEL bean
@Component("orderSecurity")
public class OrderSecurityEvaluator {
    private final OrderRepository repository;

    public boolean isOwner(Long orderId, Authentication authentication) {
        return repository.existsByIdAndUserEmail(orderId, authentication.getName());
    }
}

// Test
@WebMvcTest(OrderController.class)
class OrderControllerTest {
    @Test
    @WithMockUser(username = "[email protected]", roles = "USER")
    void getUserCannotAccessOtherUserOrder() throws Exception {
        mockMvc.perform(get("/api/v1/orders/999"))
            .andExpect(status().isForbidden());
    }
}

Example 3: JPA N+1 Resolution with Entity Graph

Input: "Loading 50 orders takes 4 seconds — we see 153 queries in the log"

Diagnosis:

Hibernate: select * from orders o where o.user_id = ?
Hibernate: select * from users u where u.id = ?   -- x50
Hibernate: select * from order_items oi where oi.order_id = ? -- x50
Hibernate: select * from products p where p.id = ? -- x50 (per item average 1)
Total: 1 + 50 + 50 + 52 = 153 queries

Fix:

// Add entity graph to Order entity
@NamedEntityGraph(
    name = "Order.withUserAndItems",
    attributeNodes = {
        @NamedAttributeNode("user"),
        @NamedAttributeNode(value = "items", subgraph = "items-products")
    },
    subgraphs = @NamedSubgraph(
        name = "items-products",
        attributeNodes = @NamedAttributeNode("product")
    )
)
@Entity
public class Order { ... }

// Apply in repository
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @EntityGraph("Order.withUserAndItems")
    List<Order> findByUserId(Long userId);
}

// For bulk fetches, batch fetching via properties
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 50  # Fetches collections in batches of 50

Result: 153 queries → 3 queries. Load time: 4s → 85ms.