Awesome-omni-skill spring-boot-performance

Guide for optimizing Spring Boot application performance including caching, pagination, async processing, and JPA optimization. Use this when addressing performance issues or implementing high-traffic features.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/spring-boot-performance" ~/.claude/skills/diegosouzapw-awesome-omni-skill-spring-boot-performance && rm -rf "$T"
manifest: skills/backend/spring-boot-performance/SKILL.md
source content

Spring Boot Performance Optimization

Follow these practices to optimize application performance.

Pagination for Large Datasets

NEVER load entire tables into memory:

// ❌ WRONG - Can cause OutOfMemoryError
List<User> allUsers = userRepository.findAll();

// ✅ CORRECT - Use pagination
@GetMapping("/users")
public Page<UserDTO> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "id") String sortBy) {

    Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
    return userRepository.findAll(pageable)
        .map(userMapper::toDto);
}

Projections for Partial Data

Use projections when you don't need full entities:

// Interface projection
public interface UserSummary {
    Long getId();
    String getName();
    String getEmail();
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    List<UserSummary> findAllProjectedBy();
    
    @Query("SELECT u.id as id, u.name as name FROM User u")
    List<UserSummary> findUserSummaries();
}

Avoiding N+1 Query Problem

// ❌ WRONG - N+1 queries
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
    // This causes N additional queries
    List<OrderItem> items = order.getItems();
}

// ✅ CORRECT - Use JOIN FETCH
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o LEFT JOIN FETCH o.items")
    List<Order> findAllWithItems();
    
    @EntityGraph(attributePaths = {"items", "customer"})
    List<Order> findAll();
}

Caching Configuration

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats());
        return cacheManager;
    }
}

@Service
public class ServiceTypeService {

    @Cacheable(value = "serviceTypes", key = "#id")
    public ServiceType getServiceType(Long id) {
        return serviceTypeRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Service type not found"));
    }

    @CacheEvict(value = "serviceTypes", key = "#serviceType.id")
    public ServiceType updateServiceType(ServiceType serviceType) {
        return serviceTypeRepository.save(serviceType);
    }

    @CacheEvict(value = "serviceTypes", allEntries = true)
    public void clearCache() {
        // Clears entire cache
    }
}

Async Processing

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("Async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

@Service
public class NotificationService {

    @Async("taskExecutor")
    public CompletableFuture<Void> sendEmailNotification(String email, String message) {
        // Long-running email sending operation
        // This runs in a separate thread
        return CompletableFuture.completedFuture(null);
    }
}

// Usage in controller
@PostMapping("/appointments")
public AppointmentDTO createAppointment(@RequestBody AppointmentRequest request) {
    AppointmentDTO appointment = appointmentService.create(request);
    
    // Fire and forget - doesn't block response
    notificationService.sendEmailNotification(
        appointment.getCustomerEmail(),
        "Your appointment is confirmed"
    );
    
    return appointment;
}

Connection Pool Configuration

# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 30000
      connection-timeout: 20000
      max-lifetime: 1800000
      pool-name: SalonHubPool

JPA Optimization

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        # Batch processing
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
        
        # Second-level cache (optional)
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
        
        # Query hints
        default_batch_fetch_size: 25

Batch Processing

@Service
@Transactional
public class BulkImportService {

    @PersistenceContext
    private EntityManager entityManager;

    public void bulkInsert(List<Customer> customers) {
        int batchSize = 50;
        for (int i = 0; i < customers.size(); i++) {
            entityManager.persist(customers.get(i));
            if (i > 0 && i % batchSize == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        entityManager.flush();
        entityManager.clear();
    }
}

Lazy Loading Best Practices

@Entity
public class Order {
    @Id
    private Long id;
    
    // Lazy by default for collections
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items;
    
    // Consider lazy for large objects
    @Basic(fetch = FetchType.LAZY)
    @Lob
    private String description;
}

// Initialize lazy collections when needed
@Transactional(readOnly = true)
public Order getOrderWithItems(Long id) {
    Order order = orderRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
    // Force initialization within transaction
    Hibernate.initialize(order.getItems());
    return order;
}

Response Compression

# application.yml
server:
  compression:
    enabled: true
    mime-types: application/json,application/xml,text/html,text/xml,text/plain
    min-response-size: 1024

Index Optimization

-- Create indexes for frequently queried columns
CREATE INDEX idx_appointments_customer_id ON appointments(customer_id);
CREATE INDEX idx_appointments_employee_id ON appointments(employee_id);
CREATE INDEX idx_appointments_date ON appointments(appointment_time);

-- Composite index for common query patterns
CREATE INDEX idx_appointments_status_date ON appointments(status, appointment_time);

Performance Monitoring

@Aspect
@Component
public class PerformanceLoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(PerformanceLoggingAspect.class);

    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - start;
        
        logger.info("{} executed in {} ms", joinPoint.getSignature(), duration);
        return result;
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}

Performance Checklist

  • Use pagination for all list endpoints
  • Implement caching for frequently accessed, rarely changed data
  • Check for N+1 queries using logging or profiler
  • Use projections when full entities aren't needed
  • Add database indexes for frequently queried columns
  • Configure connection pooling appropriately
  • Use async processing for non-critical operations
  • Enable response compression
  • Monitor slow queries and optimize