From fbb3d2347721889ad3ba64c4eaaf74f9719a6f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Bre=C3=B1a=20Moral?= Date: Sun, 29 Jun 2025 22:00:18 +0200 Subject: [PATCH] Removing spring-boot rules to make more modular the initiative --- .../rules/301-frameworks-spring-boot-core.mdc | 1207 ----------------- .../rules/302-frameworks-spring-boot-rest.mdc | 765 ----------- .../rules/303-frameworks-spring-data-jdbc.mdc | 728 ---------- .../304-frameworks-spring-boot-hikari.mdc | 396 ------ ...1-frameworks-spring-boot-slice-testing.mdc | 415 ------ ...eworks-spring-boot-integration-testing.mdc | 648 --------- ...3-frameworks-spring-boot-local-testing.mdc | 477 ------- ...meworks-spring-boot-native-compilation.mdc | 396 ------ .cursor/rules/500-sql.mdc | 274 ---- .../templates/java-checklist-template.md | 19 - README.md | 19 - 11 files changed, 5344 deletions(-) delete mode 100644 .cursor/rules/301-frameworks-spring-boot-core.mdc delete mode 100644 .cursor/rules/302-frameworks-spring-boot-rest.mdc delete mode 100644 .cursor/rules/303-frameworks-spring-data-jdbc.mdc delete mode 100644 .cursor/rules/304-frameworks-spring-boot-hikari.mdc delete mode 100644 .cursor/rules/311-frameworks-spring-boot-slice-testing.mdc delete mode 100644 .cursor/rules/312-frameworks-spring-boot-integration-testing.mdc delete mode 100644 .cursor/rules/313-frameworks-spring-boot-local-testing.mdc delete mode 100644 .cursor/rules/321-frameworks-spring-boot-native-compilation.mdc delete mode 100644 .cursor/rules/500-sql.mdc diff --git a/.cursor/rules/301-frameworks-spring-boot-core.mdc b/.cursor/rules/301-frameworks-spring-boot-core.mdc deleted file mode 100644 index bafb0b30..00000000 --- a/.cursor/rules/301-frameworks-spring-boot-core.mdc +++ /dev/null @@ -1,1207 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Boot Core - -Spring Boot Core guidelines focus on proper usage of main annotations, bean management, and configuration best practices to build maintainable and efficient Spring Boot applications. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- Principle 1: Use appropriate Spring annotations to clearly express component responsibilities -- Principle 2: Leverage Spring's dependency injection and IoC container effectively -- Principle 3: Follow configuration best practices for maintainable and testable applications -- Principle 4: Apply proper bean lifecycle management and scoping - -## Table of contents - -- Rule 0: Spring Boot Main Application Class -- Rule 1: Main Spring Boot Annotations Usage -- Rule 2: Bean Definition and Management -- Rule 3: Configuration Classes and Properties -- Rule 4: Component Scanning and Package Organization -- Rule 5: Conditional Configuration and Profiles -- Rule 6: Constructor Dependency Injection Best Practices -- Rule 7: Bean Minimization and Composition -- Rule 8: Scheduled Tasks and Background Processing - -## Rule 0: Spring Boot Main Application Class - -Title: Create a Proper Spring Boot Main Application Class -Description: Every Spring Boot application should have a main application class annotated with @SpringBootApplication. This class serves as the entry point and configuration root, combining @Configuration, @EnableAutoConfiguration, and @ComponentScan annotations. - -**Good example:** - -```java -@SpringBootApplication -public class MainApplication { - - public static void main(String[] args) { - SpringApplication.run(MainApplication.class, args); - } -} - -// For more complex scenarios with custom configuration -@SpringBootApplication( - scanBasePackages = { - "com.company.app.controller", - "com.company.app.service", - "com.company.app.repository", - "com.company.app.config" - }, - exclude = { - DataSourceAutoConfiguration.class, - SecurityAutoConfiguration.class - } -) -``` - -**Bad Example:** - -```java -// Missing @SpringBootApplication annotation -public class MainApplication { - public static void main(String[] args) { - // Manual Spring context setup instead of SpringApplication.run() - ApplicationContext context = new AnnotationConfigApplicationContext(); - // Manual configuration - loses Spring Boot benefits - } -} - -// Using individual annotations instead of @SpringBootApplication -@Configuration -@EnableAutoConfiguration -@ComponentScan -public class MainApplication { // Verbose and error-prone - public static void main(String[] args) { - SpringApplication.run(MainApplication.class, args); - } -} - -// Poor naming and structure -@SpringBootApplication -public class App { // Non-descriptive name - - @Autowired - private UserService userService; // Business logic in main class - - public static void main(String[] args) { - SpringApplication.run(App.class, args); - - // Business logic in main method - should be in separate components - System.out.println("Processing users..."); - } -} -``` - -## Rule 1: Main Spring Boot Annotations Usage - -Title: Use Appropriate Spring Boot Annotations for Component Definition -Description: Use the correct Spring Boot annotations to define components, controllers, services, and repositories. Each annotation has specific semantics and should be used according to the layer's responsibility. - -**Good example:** - -```java -@RestController -@RequestMapping("/api/users") -public class UserController { - - @Autowired - private UserService userService; - - @GetMapping("/{id}") - public ResponseEntity getUser(@PathVariable Long id) { - return ResponseEntity.ok(userService.findById(id)); - } -} - -@Service -@Transactional -public class UserService { - - @Autowired - private UserRepository userRepository; - - public User findById(Long id) { - return userRepository.findById(id) - .orElseThrow(() -> new UserNotFoundException(id)); - } -} - -@Repository -public interface UserRepository extends CrudRepository { - - @Query("SELECT * FROM users WHERE email = :email") - Optional findByEmail(@Param("email") String email); - - @Modifying - @Query("UPDATE users SET last_login = :lastLogin WHERE id = :id") - void updateLastLogin(@Param("id") Long id, @Param("lastLogin") LocalDateTime lastLogin); -} - -@Table("users") -public class User { - - @Id - private Long id; - - @Column("email") - private String email; - - @Column("first_name") - private String firstName; - - @Column("last_name") - private String lastName; - - @Column("last_login") - private LocalDateTime lastLogin; - - // Constructors, getters, and setters -} -``` - -**Bad Example:** - -```java -@Component // Should be @RestController -public class UserController { - - @Inject // Use @Autowired for Spring Boot - private UserService userService; -} - -@Component // Should be @Service -public class UserService { - // Missing @Transactional for data operations -} - -@Component // Should be @Repository -public class UserRepository { - // Manual JDBC instead of using Spring Data JDBC -} -``` - -## Rule 2: Bean Definition and Management - -Title: Proper Bean Definition, Scoping, and Lifecycle Management -Description: Define beans with appropriate scope, use constructor injection, and manage bean lifecycle properly. Prefer constructor injection over field injection for better testability and immutability. - -**Good example:** - -```java -@Configuration -public class AppConfig { - - @Bean - @Scope("singleton") // Default, but explicit for clarity - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - @Scope("prototype") - public AuditLogger auditLogger() { - return new AuditLogger(); - } -} - -@Service -public class UserService { - - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - - // Constructor injection - preferred approach - public UserService(UserRepository userRepository, - PasswordEncoder passwordEncoder) { - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - } -} - -@Component -public class DatabaseMigration { - - @EventListener - public void onApplicationReady(ApplicationReadyEvent event) { - // Perform initialization after Spring context is ready - performMigration(); - } - - @PreDestroy - public void cleanup() { - // Cleanup resources before bean destruction - } -} -``` - -**Bad Example:** - -```java -@Configuration -public class AppConfig { - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); // Creates new instance every time - } -} - -@Service -public class UserService { - - @Autowired // Field injection - harder to test - private UserRepository userRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - // No constructor, relies on reflection -} - -@Component -public class DatabaseMigration { - - @PostConstruct - public void init() { - // Heavy operations in PostConstruct can block application startup - performHeavyMigration(); - } -} -``` - -## Rule 3: Configuration Classes and Properties - -Title: Organize Configuration Using @Configuration Classes and External Properties -Description: Use @Configuration classes to organize beans logically, leverage @ConfigurationProperties for type-safe configuration, and externalize configuration values properly. - -**Good example:** - -```java -@Configuration -@EnableConfigurationProperties({DatabaseProperties.class, SecurityProperties.class}) -public class AppConfig { - - @Bean - @ConditionalOnProperty(name = "app.cache.enabled", havingValue = "true") - public CacheManager cacheManager() { - return new ConcurrentMapCacheManager("users", "products"); - } -} - -@ConfigurationProperties(prefix = "app.database") -@ConstructorBinding -public class DatabaseProperties { - - private final String url; - private final String username; - private final int maxConnections; - private final Duration connectionTimeout; - - public DatabaseProperties(String url, String username, - int maxConnections, Duration connectionTimeout) { - this.url = url; - this.username = username; - this.maxConnections = maxConnections; - this.connectionTimeout = connectionTimeout; - } - - // Getters only - immutable -} - -@Configuration -@Profile("!test") -public class ProductionConfig { - - @Bean - public DataSource dataSource(DatabaseProperties properties) { - HikariConfig config = new HikariConfig(); - config.setJdbcUrl(properties.getUrl()); - config.setUsername(properties.getUsername()); - config.setMaximumPoolSize(properties.getMaxConnections()); - return new HikariDataSource(config); - } -} -``` - -**Bad Example:** - -```java -@Configuration -public class AppConfig { - - @Value("${database.url}") // Scattered @Value annotations - private String databaseUrl; - - @Value("${database.username}") - private String username; - - @Bean - public DataSource dataSource() { - // Hardcoded values mixed with properties - HikariConfig config = new HikariConfig(); - config.setJdbcUrl(databaseUrl); - config.setUsername(username); - config.setPassword("hardcoded-password"); // Security risk - config.setMaximumPoolSize(10); // Magic number - return new HikariDataSource(config); - } -} - -// No type safety, no validation -public class DatabaseConfig { - @Value("${app.database.max-connections:#{null}}") - private Integer maxConnections; // Can be null, no validation -} -``` - -## Rule 4: Component Scanning and Package Organization - -Title: Organize Components with Proper Package Structure and Component Scanning -Description: Use logical package organization and configure component scanning appropriately. Avoid over-broad scanning and organize code by feature or layer consistently. - -**Good example:** - -```java -@SpringBootApplication -@ComponentScan(basePackages = { - "com.company.app.controller", - "com.company.app.service", - "com.company.app.repository", - "com.company.app.config" -}) -@EnableJdbcRepositories("com.company.app.repository") -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -// Package structure: -// com.company.app -// ├── controller/ -// │ ├── UserController.java -// │ └── ProductController.java -// ├── service/ -// │ ├── UserService.java -// │ └── ProductService.java -// ├── repository/ -// │ ├── UserRepository.java -// │ └── ProductRepository.java -// ├── config/ -// │ ├── DatabaseConfig.java -// │ └── SecurityConfig.java -// └── model/ -// ├── User.java -// └── Product.java - -@Component("userService") // Explicit bean name when needed -public class UserService { - // Implementation -} -``` - -**Bad Example:** - -```java -@SpringBootApplication -@ComponentScan("com") // Too broad - scans entire classpath -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -// Poor package structure: -// com.company.app -// ├── UserController.java // Mixed responsibilities -// ├── UserService.java // in same package -// ├── UserRepository.java -// ├── ProductStuff.java // Unclear naming -// └── Utils.java // Generic naming - -@Component -public class UserService { // No explicit naming strategy - // Multiple unrelated responsibilities in one class - public void handleUser() { } - public void sendEmail() { } - public void generateReport() { } -} -``` - -## Rule 5: Conditional Configuration and Profiles - -Title: Use Conditional Configuration and Profiles for Environment-Specific Setup -Description: Leverage Spring's conditional annotations and profiles to create flexible, environment-aware configurations that adapt to different deployment scenarios. - -**Good example:** - -```java -@Configuration -@Profile("development") -public class DevConfig { - - @Bean - @ConditionalOnMissingBean - public Clock clock() { - return Clock.systemDefaultZone(); - } - - @Bean - public DataSource devDataSource() { - HikariConfig config = new HikariConfig(); - config.setJdbcUrl("jdbc:postgresql://localhost:5432/devdb"); - config.setUsername("dev_user"); - config.setPassword("dev_password"); - config.setMaximumPoolSize(5); - return new HikariDataSource(config); - } -} - -@Configuration -@Profile("production") -public class ProdConfig { - - @Bean - @ConditionalOnProperty( - name = "app.monitoring.enabled", - havingValue = "true", - matchIfMissing = true - ) - public MeterRegistry meterRegistry() { - return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); - } - - @Bean - @ConditionalOnClass(name = "redis.clients.jedis.Jedis") - public RedisTemplate redisTemplate() { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(jedisConnectionFactory()); - return template; - } -} - -@Service -@ConditionalOnProperty(name = "features.advanced-analytics", havingValue = "true") -public class AdvancedAnalyticsService { - // Only available when feature flag is enabled -} - -// application-dev.yml -// spring: -// profiles: -// active: development -// datasource: -// url: jdbc:postgresql://localhost:5432/devdb -// username: dev_user -// password: dev_password - -// application-prod.yml -// spring: -// profiles: -// active: production -// datasource: -// url: ${DATABASE_URL} -// username: ${DATABASE_USERNAME} -// password: ${DATABASE_PASSWORD} -``` - -**Bad Example:** - -```java -@Configuration -public class AppConfig { - - @Value("${spring.profiles.active:}") - private String activeProfile; - - @Bean - public DataSource dataSource() { - if ("development".equals(activeProfile)) { - // Manual profile checking instead of @Profile - return createDevDataSource(); - } else if ("production".equals(activeProfile)) { - return createProdDataSource(); - } - return createDefaultDataSource(); - } - - @Bean - public FeatureService featureService() { - // No conditional logic - always creates bean - return new ExpensiveFeatureService(); - } -} - -@Service -public class NotificationService { - - @Value("${app.env}") - private String environment; - - public void sendNotification(String message) { - if ("prod".equals(environment)) { - // Environment logic scattered in business code - sendRealNotification(message); - } else { - // Development behavior mixed with production code - System.out.println("DEV: " + message); - } - } -} -``` - -## Rule 6: Constructor Dependency Injection Best Practices - -Title: Favor Constructor Injection for Immutable and Testable Components -Description: Use constructor injection as the primary dependency injection mechanism. It promotes immutability, makes dependencies explicit, enables easier testing, and prevents circular dependencies. - -**Good example:** - -```java -@Service -public class UserService { - - private final UserRepository userRepository; - private final EmailService emailService; - private final AuditService auditService; - - // Single constructor - @Autowired is optional since Spring 4.3 - public UserService(UserRepository userRepository, - EmailService emailService, - AuditService auditService) { - this.userRepository = Objects.requireNonNull(userRepository, "userRepository cannot be null"); - this.emailService = Objects.requireNonNull(emailService, "emailService cannot be null"); - this.auditService = Objects.requireNonNull(auditService, "auditService cannot be null"); - } - - public User createUser(CreateUserRequest request) { - User user = new User(request.getEmail(), request.getName()); - User savedUser = userRepository.save(user); - emailService.sendWelcomeEmail(savedUser); - auditService.logUserCreation(savedUser); - return savedUser; - } -} - -@Configuration -public class ServiceConfig { - - // Constructor injection for configuration classes - private final DatabaseProperties databaseProperties; - - public ServiceConfig(DatabaseProperties databaseProperties) { - this.databaseProperties = databaseProperties; - } - - @Bean - public DataSource dataSource() { - return DataSourceBuilder.create() - .url(databaseProperties.getUrl()) - .username(databaseProperties.getUsername()) - .password(databaseProperties.getPassword()) - .build(); - } -} - -// Optional dependencies using constructor with default values -@Service -public class NotificationService { - - private final EmailService emailService; - private final SmsService smsService; - - // Primary constructor for all dependencies - public NotificationService(EmailService emailService, SmsService smsService) { - this.emailService = emailService; - this.smsService = smsService; - } - - // Secondary constructor for partial dependencies - public NotificationService(EmailService emailService) { - this(emailService, new NoOpSmsService()); - } -} -``` - -**Bad Example:** - -```java -@Service -public class UserService { - - @Autowired // Field injection - harder to test and debug - private UserRepository userRepository; - - @Autowired - private EmailService emailService; - - @Autowired - private AuditService auditService; - - // No constructor - dependencies can be null - // Cannot create immutable fields - // Harder to unit test -} - -@Service -public class OrderService { - - private UserService userService; - private PaymentService paymentService; - - @Autowired // Setter injection - allows partial initialization - public void setUserService(UserService userService) { - this.userService = userService; - } - - @Autowired - public void setPaymentService(PaymentService paymentService) { - this.paymentService = paymentService; - } - - // Service can be in invalid state if setters not called - public void processOrder(Order order) { - // NullPointerException risk if dependencies not injected - userService.validateUser(order.getUserId()); - paymentService.processPayment(order.getPayment()); - } -} - -@Configuration -public class BadConfig { - - @Autowired // Field injection in configuration - private Environment environment; - - @Bean - public ApiClient apiClient() { - // Configuration depends on field injection - return new ApiClient(environment.getProperty("api.url")); - } -} -``` - -## Rule 7: Bean Minimization and Composition - -Title: Minimize Bean Count Through Composition and Logical Grouping -Description: Reduce the number of Spring beans by composing related functionality, using factory methods, and avoiding over-decomposition. Prefer composition over excessive bean granularity. - -**Good example:** - -```java -// Compose related services into cohesive units -@Service -public class UserManagementService { - - private final UserRepository userRepository; - private final UserValidator userValidator; - private final UserNotificationService notificationService; - - public UserManagementService(UserRepository userRepository) { - this.userRepository = userRepository; - this.userValidator = new UserValidator(); // Simple composition - this.notificationService = new UserNotificationService(new EmailClient(), new SmsClient()); - } - - public User createUser(CreateUserRequest request) { - userValidator.validate(request); - User user = userRepository.save(new User(request)); - notificationService.sendWelcomeNotification(user); - return user; - } -} - -// Use factory methods for related beans -@Configuration -public class CommunicationConfig { - - @Bean - public CommunicationService communicationService( - @Value("${app.email.enabled:true}") boolean emailEnabled, - @Value("${app.sms.enabled:false}") boolean smsEnabled) { - - List channels = new ArrayList<>(); - - if (emailEnabled) { - channels.add(createEmailChannel()); - } - if (smsEnabled) { - channels.add(createSmsChannel()); - } - - return new CommunicationService(channels); - } - - // Private factory methods instead of separate beans - private EmailChannel createEmailChannel() { - return new EmailChannel(new SmtpClient()); - } - - private SmsChannel createSmsChannel() { - return new SmsChannel(new TwilioClient()); - } -} - -// Compose utilities and helpers as inner classes or packages -@Service -public class ReportService { - - private final ReportRepository reportRepository; - private final ReportFormatter formatter; - private final ReportExporter exporter; - - public ReportService(ReportRepository reportRepository) { - this.reportRepository = reportRepository; - this.formatter = new ReportFormatter(); - this.exporter = new ReportExporter(); - } - - // Inner class for related functionality - private static class ReportFormatter { - public String formatAsJson(Report report) { return "..."; } - public String formatAsXml(Report report) { return "..."; } - } - - private static class ReportExporter { - public void exportToPdf(String content) { /* implementation */ } - public void exportToExcel(String content) { /* implementation */ } - } -} - -// Use configuration properties instead of multiple property beans -@ConfigurationProperties(prefix = "app") -public class ApplicationProperties { - - private final Database database = new Database(); - private final Security security = new Security(); - private final Cache cache = new Cache(); - - // Nested static classes for logical grouping - public static class Database { - private String url; - private String username; - private int maxConnections = 10; - // getters and setters - } - - public static class Security { - private boolean enabled = true; - private String algorithm = "SHA-256"; - // getters and setters - } - - public static class Cache { - private boolean enabled = false; - private Duration ttl = Duration.ofMinutes(30); - // getters and setters - } -} -``` - -**Bad Example:** - -```java -// Over-decomposition - too many beans for simple functionality -@Component -public class EmailValidator { - public boolean isValid(String email) { return email.contains("@"); } -} - -@Component -public class PasswordValidator { - public boolean isValid(String password) { return password.length() >= 8; } -} - -@Component -public class PhoneValidator { - public boolean isValid(String phone) { return phone.matches("\\d{10}"); } -} - -@Component -public class UserValidator { - @Autowired private EmailValidator emailValidator; - @Autowired private PasswordValidator passwordValidator; - @Autowired private PhoneValidator phoneValidator; - - // Three beans for simple validation logic -} - -// Separate beans for configuration values -@Component -public class DatabaseUrlProvider { - @Value("${database.url}") - private String url; - public String getUrl() { return url; } -} - -@Component -public class DatabaseUsernameProvider { - @Value("${database.username}") - private String username; - public String getUsername() { return username; } -} - -@Component -public class DatabasePasswordProvider { - @Value("${database.password}") - private String password; - public String getPassword() { return password; } -} - -// Multiple similar beans instead of composition -@Component -public class EmailSender { - public void send(String to, String message) { /* implementation */ } -} - -@Component -public class SmsSender { - public void send(String phone, String message) { /* implementation */ } -} - -@Component -public class PushNotificationSender { - public void send(String deviceId, String message) { /* implementation */ } -} - -@Service -public class NotificationService { - @Autowired private EmailSender emailSender; - @Autowired private SmsSender smsSender; - @Autowired private PushNotificationSender pushSender; - - // Managing multiple beans instead of composed solution -} - -// Utility classes as beans -@Component -public class StringUtils { - public boolean isEmpty(String str) { return str == null || str.trim().isEmpty(); } -} - -@Component -public class DateUtils { - public String format(LocalDate date) { return date.toString(); } -} -``` - -## Rule 8: Scheduled Tasks and Background Processing - -Title: Implement Robust Scheduled Tasks with Proper Configuration and Error Handling -Description: Use Spring's scheduling capabilities effectively with appropriate configuration, error handling, and monitoring. Ensure scheduled tasks are resilient, maintainable, and don't impact application performance. - -**Good example:** - -```java -@Configuration -@EnableScheduling -@EnableAsync -public class SchedulingConfig { - - @Bean - @Primary - public TaskScheduler taskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setPoolSize(5); - scheduler.setThreadNamePrefix("scheduled-task-"); - scheduler.setAwaitTerminationSeconds(30); - scheduler.setWaitForTasksToCompleteOnShutdown(true); - scheduler.setErrorHandler(new CustomErrorHandler()); - return scheduler; - } - - @Bean - public TaskExecutor asyncExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(3); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("async-task-"); - executor.setWaitForTasksToCompleteOnShutdown(true); - return executor; - } -} - -@Component -public class DataMaintenanceScheduler { - - private static final Logger logger = LoggerFactory.getLogger(DataMaintenanceScheduler.class); - - private final UserRepository userRepository; - private final AuditLogRepository auditLogRepository; - private final MeterRegistry meterRegistry; - - public DataMaintenanceScheduler(UserRepository userRepository, - AuditLogRepository auditLogRepository, - MeterRegistry meterRegistry) { - this.userRepository = userRepository; - this.auditLogRepository = auditLogRepository; - this.meterRegistry = meterRegistry; - } - - // Fixed rate - executes every 30 minutes regardless of previous execution time - @Scheduled(fixedRateString = "${app.cleanup.rate:1800000}") // 30 minutes default - public void cleanupExpiredSessions() { - Timer.Sample sample = Timer.start(meterRegistry); - try { - logger.info("Starting session cleanup task"); - - int deletedCount = userRepository.deleteExpiredSessions( - LocalDateTime.now().minusHours(24) - ); - - logger.info("Cleaned up {} expired sessions", deletedCount); - meterRegistry.counter("scheduled.cleanup.sessions", "status", "success") - .increment(deletedCount); - - } catch (Exception e) { - logger.error("Failed to cleanup expired sessions", e); - meterRegistry.counter("scheduled.cleanup.sessions", "status", "error") - .increment(); - } finally { - sample.stop(Timer.builder("scheduled.cleanup.duration") - .tag("task", "sessions") - .register(meterRegistry)); - } - } - - // Fixed delay - waits specified time after previous execution completes - @Scheduled(fixedDelayString = "${app.audit.cleanup.delay:3600000}") // 1 hour default - public void cleanupOldAuditLogs() { - try { - logger.debug("Starting audit log cleanup"); - - LocalDateTime cutoffDate = LocalDateTime.now().minusDays(90); - int deletedCount = auditLogRepository.deleteLogsOlderThan(cutoffDate); - - if (deletedCount > 0) { - logger.info("Cleaned up {} old audit logs", deletedCount); - } - - } catch (Exception e) { - logger.error("Failed to cleanup old audit logs", e); - // Don't rethrow - let other scheduled tasks continue - } - } - - // Cron expression - runs at 2 AM every day - @Scheduled(cron = "${app.reports.schedule:0 0 2 * * *}") - @Async("asyncExecutor") - public CompletableFuture generateDailyReports() { - return CompletableFuture.runAsync(() -> { - try { - logger.info("Starting daily report generation"); - - // Long-running task executed asynchronously - generateUserActivityReport(); - generateSystemHealthReport(); - - logger.info("Daily reports generated successfully"); - - } catch (Exception e) { - logger.error("Failed to generate daily reports", e); - // Could send alert or notification here - } - }); - } - - // Conditional scheduling based on profiles - @Scheduled(fixedRate = 300000) // 5 minutes - @ConditionalOnProperty(name = "app.monitoring.enabled", havingValue = "true") - public void healthCheck() { - logger.debug("Performing health check"); - // Implementation - } - - private void generateUserActivityReport() { - // Heavy computation that should run async - } - - private void generateSystemHealthReport() { - // Another heavy computation - } -} - -@Component -public class CustomErrorHandler implements ErrorHandler { - - private static final Logger logger = LoggerFactory.getLogger(CustomErrorHandler.class); - - @Override - public void handleError(Throwable t) { - logger.error("Scheduled task failed with error", t); - - // Could implement alerting, metrics, or other error handling logic - if (t instanceof DataAccessException) { - // Handle database-related errors - logger.warn("Database error in scheduled task, will retry on next execution"); - } else { - // Handle other types of errors - logger.error("Unexpected error in scheduled task", t); - } - } -} - -// Configuration properties for scheduling -@ConfigurationProperties(prefix = "app.scheduling") -public class SchedulingProperties { - - private boolean enabled = true; - private int poolSize = 5; - private Duration shutdownTimeout = Duration.ofSeconds(30); - - // getters and setters -} -``` - -**Bad Example:** - -```java -@Component -@EnableScheduling // Should be in @Configuration class -public class BadScheduler { - - @Autowired // Field injection - private UserRepository userRepository; - - // Hardcoded timing, no error handling - @Scheduled(fixedRate = 30000) // 30 seconds - too frequent for cleanup - public void cleanupUsers() { - // No logging, no error handling - userRepository.deleteInactiveUsers(); - - // Blocking operation in scheduled thread - sendEmailNotifications(); // Should be async - } - - @Scheduled(cron = "0 0 2 * * *") // Hardcoded, not configurable - public void heavyProcessing() { - // Long-running synchronous operation blocks scheduler - for (int i = 0; i < 1000000; i++) { - performComplexCalculation(); - // No progress tracking, no way to monitor or stop - } - - // No error handling - exception will break scheduling - riskyOperation(); - } - - @Scheduled(fixedDelay = 1000) // Too frequent, will impact performance - public void constantPolling() { - // Polling database every second - checkForNewMessages(); // Should use messaging or webhooks instead - } - - // Multiple methods with same timing - inefficient - @Scheduled(fixedRate = 60000) - public void task1() { /* implementation */ } - - @Scheduled(fixedRate = 60000) - public void task2() { /* implementation */ } - - @Scheduled(fixedRate = 60000) - public void task3() { /* implementation */ } - - private void sendEmailNotifications() { - // Synchronous email sending blocks the scheduler - for (User user : getAllUsers()) { - emailService.sendEmail(user.getEmail(), "notification"); - // No timeout, no retry logic, no error handling - } - } - - private void riskyOperation() { - // Operation that might throw uncaught exception - throw new RuntimeException("This will break all scheduling"); - } -} - -// No thread pool configuration -@Configuration -public class BadSchedulingConfig { - // Using default single-threaded scheduler - // No error handling configuration - // No monitoring or metrics -} - -@Service -public class BlockingScheduledService { - - @Scheduled(fixedRate = 5000) - public void blockingTask() { - try { - // Blocking I/O operation - Thread.sleep(30000); // 30 second sleep blocks scheduler - - // Synchronous HTTP calls - restTemplate.getForObject("http://slow-service/api", String.class); - - } catch (InterruptedException e) { - // Poor exception handling - e.printStackTrace(); // Never use printStackTrace in production - } - } -} -``` - -### Scheduling Best Practices - -**Configuration Guidelines:** -- Always use `@EnableScheduling` in a `@Configuration` class -- Configure custom `TaskScheduler` with appropriate thread pool size -- Set up proper error handling with `ErrorHandler` -- Use externalized configuration for timing and scheduling parameters - -**Error Handling:** -- Implement comprehensive logging for all scheduled tasks -- Use try-catch blocks to prevent one task failure from affecting others -- Consider implementing retry logic for transient failures -- Add metrics and monitoring for scheduled task execution - -**Performance Considerations:** -- Use `@Async` for long-running tasks to avoid blocking the scheduler -- Choose appropriate scheduling intervals based on business requirements -- Monitor thread pool usage and adjust pool sizes accordingly -- Avoid frequent polling - consider event-driven alternatives - -**Testing:** -```java -@TestConfiguration -public class TestSchedulingConfig { - - @Bean - @Primary - public TaskScheduler testTaskScheduler() { - // Use synchronous scheduler for testing - return new SyncTaskExecutor(); - } -} - -@SpringBootTest -class ScheduledTaskTest { - - @Test - @DirtiesContext - void shouldExecuteScheduledTask() { - // Test scheduled task logic without actual scheduling - scheduler.cleanupExpiredSessions(); - // Verify expected behavior - } -} -``` - -### Advanced Configuration Patterns - -For complex applications, consider these additional patterns: - -- **@ConfigurationPropertiesBinding**: Create custom property converters -- **@ConditionalOnBean/@ConditionalOnMissingBean**: Fine-grained bean creation control -- **@Import**: Compose configuration classes -- **@EnableAutoConfiguration(exclude = {...})**: Disable specific auto-configurations -- **ApplicationContextInitializer**: For programmatic context customization - diff --git a/.cursor/rules/302-frameworks-spring-boot-rest.mdc b/.cursor/rules/302-frameworks-spring-boot-rest.mdc deleted file mode 100644 index 5faa75ef..00000000 --- a/.cursor/rules/302-frameworks-spring-boot-rest.mdc +++ /dev/null @@ -1,765 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Java REST API Design Principles - -This comprehensive guide provides essential principles for designing robust, maintainable, and secure REST APIs using Spring Boot. These rules ensure your APIs follow industry best practices, maintain consistency, and provide excellent developer experience for API consumers. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Semantic Consistency**: Use HTTP methods, status codes, and URI patterns according to their intended semantics -- **Clear Communication**: Provide unambiguous API contracts through proper DTOs, error handling, and documentation -- **Security by Design**: Implement authentication, authorization, and input validation from the start -- **Evolutionary Design**: Version APIs and structure them to support future changes without breaking existing clients - -## Table of contents - -- Rule 1: Use HTTP Methods Correctly -- Rule 2: Design Clear and Consistent Resource URIs -- Rule 3: Use HTTP Status Codes Appropriately -- Rule 4: Implement Effective Request and Response Payloads (DTOs) -- Rule 5: Version Your APIs -- Rule 6: Handle Errors Gracefully -- Rule 7: Secure Your APIs -- Rule 8: Document Your APIs -- Rule 9: Use Controller Advice for Global Exception Handling -- Rule 10: Implement Problem Details for Error Responses - -## Rule 1: Use HTTP Methods Correctly - -Title: Employ HTTP Methods Semantically -Description: Use HTTP methods according to their defined semantics to ensure predictability and compliance with web standards. `GET` for retrieval, `POST` for creation, `PUT` for update/replace, `PATCH` for partial update, and `DELETE` for removal. - -**Good example:** - -```java -// Using Spring MVC annotations for illustration -@RestController -@RequestMapping("/users") -public class UserController { - - @GetMapping("/{id}") // GET for retrieving a user - public ResponseEntity getUser(@PathVariable String id) { - // ... logic to fetch user ... - return ResponseEntity.ok(new UserDTO()); - } - - @PostMapping // POST for creating a new user - public ResponseEntity createUser(@RequestBody UserCreateDTO userCreateDTO) { - // ... logic to create user ... - UserDTO newUser = new UserDTO(); // Assume it gets an ID after creation - return ResponseEntity.created(URI.create("/users/" + newUser.getId())).body(newUser); - } - - @PutMapping("/{id}") // PUT for replacing/updating a user - public ResponseEntity updateUser(@PathVariable String id, @RequestBody UserUpdateDTO userUpdateDTO) { - // ... logic to update user ... - return ResponseEntity.ok(new UserDTO()); - } - - @DeleteMapping("/{id}") // DELETE for removing a user - public ResponseEntity deleteUser(@PathVariable String id) { - // ... logic to delete user ... - return ResponseEntity.noContent().build(); - } - - @PatchMapping("/{id}") // PATCH for partial updates - public ResponseEntity partiallyUpdateUser(@PathVariable String id, @RequestBody Map updates) { - // ... logic to partially update user ... - return ResponseEntity.ok(new UserDTO()); - } -} -// Dummy DTO classes -class UserDTO { private String id; public String getId() { return id; } /* ... other fields, getters, setters ... */ } -class UserCreateDTO { /* ... fields ... */ } -class UserUpdateDTO { /* ... fields ... */ } -``` - -**Bad Example:** - -```java -@RestController -@RequestMapping("/api") -public class BadUserController { - - // Bad: Using GET to perform a state change (e.g., delete) - @GetMapping("/deleteUser") - public ResponseEntity deleteUserViaGet(@RequestParam String id) { - System.out.println("Deleting user: " + id + " (Bad: GET used for delete)"); - // ... delete logic ... - return ResponseEntity.ok("User deleted (but GET was used!)"); - } - - // Bad: Using POST for all operations, including retrieval - @PostMapping("/getUser") - public ResponseEntity getUserViaPost(@RequestBody String idPayload) { - System.out.println("Fetching user: " + idPayload + " (Bad: POST used for GET)"); - // ... fetch logic ... - return ResponseEntity.ok(new UserDTO()); - } -} -``` - -## Rule 2: Design Clear and Consistent Resource URIs - -Title: Use Nouns for Resources and Maintain URI Consistency -Description: Design URIs that are intuitive and clearly represent resources. Use nouns (e.g., `/users`, `/orders`) instead of verbs. Keep URIs consistent in style (e.g., lowercase, hyphenated or camelCase for path segments). - -**Good example:** - -``` -GET /users // Get all users -GET /users/{userId} // Get a specific user -GET /users/{userId}/orders // Get all orders for a specific user -GET /users/{userId}/orders/{orderId} // Get a specific order for a user -POST /users // Create a new user -``` - -**Bad Example:** - -``` -GET /getAllUsers -GET /fetchUserById?id={userId} -POST /createNewUser -GET /userOrders?userId={userId} // Mixing query params and path styles inconsistently -POST /processUserOrderCreation // URI contains verbs and is overly complex -``` - -## Rule 3: Use HTTP Status Codes Appropriately - -Title: Return Meaningful HTTP Status Codes -Description: Utilize standard HTTP status codes to accurately reflect the outcome of API requests. This helps clients understand the result without needing to parse the response body for basic success/failure information. -- `200 OK`: General success. -- `201 Created`: Resource successfully created (often with a `Location` header pointing to the new resource). -- `204 No Content`: Success, but no content to return (e.g., after a successful `DELETE`). -- `400 Bad Request`: Client error (e.g., invalid syntax, missing parameters). -- `401 Unauthorized`: Authentication is required and has failed or has not yet been provided. -- `403 Forbidden`: Authenticated client does not have permission to access the resource. -- `404 Not Found`: Resource not found. -- `500 Internal Server Error`: A generic error message for unexpected server-side errors. - -**Good example:** - -```java -// (Inside a Spring @RestController method) -if (resourceNotFound) { - return ResponseEntity.notFound().build(); // 404 -} -if (!userHasPermission) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied"); // 403 -} -if (validationFailed) { - return ResponseEntity.badRequest().body("Invalid input data"); // 400 -} -// For creation: -// return ResponseEntity.created(newResourceUri).body(newResource); // 201 -// For successful deletion: -// return ResponseEntity.noContent().build(); // 204 -``` - -**Bad Example:** - -```java -import java.util.Objects; - -public ResponseEntity processSomething(String input) { - try { - if (Objects.isNull(input)) { - // Client should receive a 400 Bad Request, not 200 with an error message in body. - return ResponseEntity.ok("{\"error\":\"Input cannot be null\"}"); - } - // ... process ... - return ResponseEntity.ok("{\"data\":\"Success!\"}"); - } catch (Exception e) { - // Client should receive a 500 Internal Server Error, not 200. - return ResponseEntity.ok("{\"error\":\"Something went wrong on the server\"}"); - } -} -``` - -## Rule 4: Implement Effective Request and Response Payloads (DTOs) - -Title: Use Data Transfer Objects (DTOs) for Payloads and Keep Them Lean -Description: Use dedicated DTO classes for request and response bodies instead of exposing internal domain/entity objects directly. This decouples your API contract from your internal data model. Keep DTOs focused on the data needed for the specific API operation. Use consistent naming conventions (e.g., JSON with camelCase keys). - -**Good example:** - -```java -// Domain Entity (internal) -class User { - private Long id; - private String username; - private String passwordHash; // Internal field, should not be in API responses - private String email; - private java.time.LocalDateTime createdAt; - // getters, setters -} - -// DTO for API responses (exposes only necessary fields) -class UserResponseDTO { - private Long id; - private String username; - private String email; - // getters, setters -} - -// DTO for creating a user -class UserCreateRequestDTO { - private String username; - private String password; // Received in request, then hashed internally - private String email; - // getters, setters -} - -// In controller: -// public ResponseEntity getUser(@PathVariable Long id) { ... } -// public ResponseEntity createUser(@RequestBody UserCreateRequestDTO createDto) { ... } -``` - -**Bad Example:** - -```java -// Bad: Exposing internal User entity directly in API, including sensitive fields like passwordHash. -@RestController -public class AnotherUserController { - @GetMapping("/rawusers/{id}") - public ResponseEntity getRawUser(@PathVariable String id) { - // Assume User class has passwordHash and other internal fields - User internalUser = findUserById(id); // Method that returns the internal User entity - return ResponseEntity.ok(internalUser); // Exposes passwordHash, createdAt, etc. - } - private User findUserById(String id) { return new User(); /* ... */} -} -``` - -## Rule 5: Version Your APIs - -Title: Implement a Clear API Versioning Strategy -Description: Introduce API versioning from the beginning to manage changes and evolution without breaking existing clients. Common strategies include URI versioning (e.g., `/v1/users`), custom request header versioning (e.g., `X-API-Version: 1`), or media type versioning (e.g., `Accept: application/vnd.example.v1+json`). Choose a strategy and apply it consistently. - -**Good example (URI Versioning):** - -```java -@RestController -@RequestMapping("/api/v1/products") // Version in URI -public class ProductControllerV1 { - // ... v1 endpoints ... -} - -@RestController -@RequestMapping("/api/v2/products") // New version in URI -public class ProductControllerV2 { - // ... v2 endpoints with potential breaking changes or new features ... -} -``` - -**Bad Example (No Versioning):** - -```java -@RestController -@RequestMapping("/products") // No version information -public class UnversionedProductController { - // If a breaking change is made here (e.g., field removed from response), - // all existing clients might break. - @GetMapping("/{id}") - public ProductDTO getProduct(@PathVariable String id) { - // ... logic ... - return new ProductDTO(); - } -} -class ProductDTO {} -``` - -## Rule 6: Handle Errors Gracefully - -Title: Provide Clear and Consistent Error Responses -Description: When an error occurs, return a standardized, machine-readable error response format (e.g., JSON). Include a unique error code or type, a human-readable message, and optionally, details about specific fields that caused validation errors. Do not expose sensitive internal details like stack traces in production error responses. - -**Good example (Error DTO and @ControllerAdvice for Spring):** - -```java -// Error Response DTO -class ErrorResponse { - private String errorCode; - private String message; - private List details; // Optional: for field-specific validation errors - // Constructor, getters - public ErrorResponse(String errorCode, String message) { this.errorCode = errorCode; this.message = message; } - public ErrorResponse(String errorCode, String message, List details) { /* ... */ } -} - -@ControllerAdvice -class GlobalExceptionHandler { - @ExceptionHandler(ResourceNotFoundException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ErrorResponse handleResourceNotFound(ResourceNotFoundException ex) { - return new ErrorResponse("RESOURCE_NOT_FOUND", ex.getMessage()); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) // Example for bean validation errors - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) { - List errors = ex.getBindingResult().getFieldErrors().stream() - .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) - .collect(Collectors.toList()); - return new ErrorResponse("VALIDATION_ERROR", "Input validation failed", errors); - } - - @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public ErrorResponse handleGenericError(Exception ex) { - // Log the full exception internally - // logger.error("Unhandled exception:", ex); - return new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred."); - } -} -// Custom exception -class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String msg){ super(msg);}} -``` - -**Bad Example:** - -```java -@RestController -public class BadErrorHandlingController { - @GetMapping("/items/{id}") - public ResponseEntity getItem(@PathVariable String id) { - if (id.equals("unknown")) { - // Bad: Returning plain text error, or HTML, or inconsistent format. - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Item not found!"); - } - try { - // ... some logic that might throw an exception ... - if(id.equals("fail")) throw new NullPointerException("Simulated internal error"); - return ResponseEntity.ok("Item data"); - } catch (Exception e) { - // Bad: Exposing stack trace to the client in production. - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.toString()); - } - } -} -``` - -## Rule 7: Secure Your APIs - -Title: Implement Robust Security Measures -Description: Protect your APIs from common threats. Use HTTPS for all communication. Implement proper authentication (e.g., OAuth 2.0, JWT) and authorization (e.g., role-based access control). Validate all input data to prevent injection attacks (SQLi, XSS). Apply rate limiting and throttling to prevent abuse. - -**Good example (Conceptual - actual implementation involves security frameworks like Spring Security):** - -```java -// In a Spring Security configuration: -@Configuration -@EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .csrf().disable() // Consider CSRF protection needs - .authorizeRequests() - .antMatchers("/public/**").permitAll() - .antMatchers("/admin/**").hasRole("ADMIN") // Role-based authorization - .anyRequest().authenticated() // All other requests need authentication - .and() - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); // Example: JWT authentication - // .httpBasic(); // Or basic auth for simplicity in some cases - } - // ... user details service, password encoder, etc. ... -} - -// In a controller method: -// @PreAuthorize("hasAuthority('SCOPE_read:users')") // Example: OAuth2 scope-based authorization -// @GetMapping("/users/{id}") -// public UserDTO getUser(@PathVariable String id) { ... } -``` - -**Bad Example:** - -```java -@RestController -public class InsecureController { - // Bad: No authentication or authorization for sensitive operations. - @PostMapping("/admin/deleteAllData") - public String deleteAllData() { - System.out.println("All data deleted! (INSECURE - NO AUTH)"); - return "All data wiped."; - } - - // Bad: Trusting user input directly in a query (conceptual SQLi vulnerability). - @GetMapping("/products") - public List searchProducts(@RequestParam String category) { - // String query = "SELECT * FROM products WHERE category = '" + category + "'"; // SQL Injection risk! - // Use PreparedStatement or an ORM to prevent SQLi. - System.out.println("Searching for category (raw input): " + category); - return List.of(); - } -} -``` - -## Rule 8: Document Your APIs - -Title: Provide Clear and Comprehensive API Documentation -Description: Maintain up-to-date documentation for your API. Tools like Swagger/OpenAPI can be used to generate interactive documentation from code annotations. Documentation should cover resource URIs, HTTP methods, request/response formats (including DTO schemas), expected status codes, authentication methods, and error responses. - -**Good example (Using Springdoc OpenAPI / Swagger annotations):** - -```java -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -// Assume other necessary imports like Spring MVC, DTOs etc. - -@RestController -@RequestMapping("/api/v1/widgets") -@Tag(name = "Widget API", description = "APIs for managing widgets") -public class WidgetController { - - @Operation(summary = "Get a widget by its ID", - description = "Returns a single widget based on the provided ID.", - responses = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved widget", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WidgetDTO.class))), - @ApiResponse(responseCode = "404", description = "Widget not found", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) - }) - @GetMapping("/{widgetId}") - public ResponseEntity getWidgetById( - @Parameter(description = "ID of the widget to retrieve", required = true) - @PathVariable String widgetId) { - // ... logic ... - // return ResponseEntity.ok(new WidgetDTO(widgetId, "Sample Widget")); - // For example, if not found: - if ("unknown".equals(widgetId)) { - throw new ResourceNotFoundException("Widget with ID " + widgetId + " not found."); - } - return ResponseEntity.ok(new WidgetDTO()); // Placeholder - } -} -class WidgetDTO { /* fields, getters, setters */ } -// ErrorResponse and ResourceNotFoundException as defined in Rule 6 -``` - -**Bad Example (No Documentation or Incomplete):** - -```java -// No API documentation tool usage, comments are sparse or missing. -@RestController -@RequestMapping("/legacy/things") -public class LegacyThingController { - // What does this do? What are parameters? What are responses? - @GetMapping("/{id}") - public Object getAThing(@PathVariable String id, @RequestParam(required = false) String type) { - // ... complex logic ... - return new Object(); // Unclear what this object structure is. - } -} -// Clients have to guess or read source code to understand how to use the API. -``` - -## Rule 9: Use Controller Advice for Global Exception Handling - -Title: Implement Centralized Exception Handling with @ControllerAdvice -Description: Use `@ControllerAdvice` to create a centralized exception handling mechanism that can catch and handle both checked `Exception` and unchecked `RuntimeException` across all controllers. Use Spring Boot's built-in `ProblemDetail` class for consistent, standardized error responses that follow RFC 7807. This approach promotes DRY principles, ensures consistent error responses, and separates error handling logic from business logic. - -**Good example:** - -```java -@ControllerAdvice -public class GlobalExceptionHandler { - - private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgument( - IllegalArgumentException ex, HttpServletRequest request) { - logger.warn("Invalid argument: {}", ex.getMessage()); - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.BAD_REQUEST, ex.getMessage() - ); - problemDetail.setType(URI.create("https://example.com/problems/invalid-argument")); - problemDetail.setTitle("Invalid Argument"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - - return ResponseEntity.badRequest().body(problemDetail); - } - - @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity handleEntityNotFound( - EntityNotFoundException ex, HttpServletRequest request) { - logger.warn("Entity not found: {}", ex.getMessage()); - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.NOT_FOUND, ex.getMessage() - ); - problemDetail.setType(URI.create("https://example.com/problems/entity-not-found")); - problemDetail.setTitle("Entity Not Found"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); - } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException( - RuntimeException ex, HttpServletRequest request) { - String errorId = UUID.randomUUID().toString(); - logger.error("Unexpected runtime exception with ID: {}", errorId, ex); - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.INTERNAL_SERVER_ERROR, - "An unexpected error occurred while processing the request" - ); - problemDetail.setType(URI.create("https://example.com/problems/internal-error")); - problemDetail.setTitle("Internal Server Error"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - problemDetail.setProperty("errorId", errorId); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException( - Exception ex, HttpServletRequest request) { - String errorId = UUID.randomUUID().toString(); - logger.error("Unexpected exception with ID: {}", errorId, ex); - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.INTERNAL_SERVER_ERROR, - "An unexpected error occurred while processing the request" - ); - problemDetail.setType(URI.create("https://example.com/problems/internal-error")); - problemDetail.setTitle("Internal Server Error"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - problemDetail.setProperty("errorId", errorId); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationException( - MethodArgumentNotValidException ex, HttpServletRequest request) { - List errors = ex.getBindingResult().getFieldErrors().stream() - .map(error -> error.getField() + ": " + error.getDefaultMessage()) - .collect(Collectors.toList()); - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.BAD_REQUEST, "Validation failed for the provided input" - ); - problemDetail.setType(URI.create("https://example.com/problems/validation-error")); - problemDetail.setTitle("Validation Error"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - problemDetail.setProperty("violations", errors); - - return ResponseEntity.badRequest().body(problemDetail); - } -} - -// Custom exceptions -class EntityNotFoundException extends RuntimeException { - public EntityNotFoundException(String message) { super(message); } -} -``` - -**Bad Example:** - -```java -@RestController -public class BadUserController { - - // Bad: Exception handling scattered across multiple controllers - @GetMapping("/users/{id}") - public ResponseEntity getUser(@PathVariable String id) { - try { - if (id.equals("invalid")) { - throw new IllegalArgumentException("Invalid user ID"); - } - if (id.equals("notfound")) { - throw new EntityNotFoundException("User not found"); - } - // ... business logic ... - return ResponseEntity.ok(new UserDTO()); - } catch (IllegalArgumentException e) { - // Bad: Inconsistent error format, not using ProblemDetail - return ResponseEntity.badRequest().body("Error: " + e.getMessage()); - } catch (EntityNotFoundException e) { - // Bad: Different error format, no error details - return ResponseEntity.notFound().build(); - } catch (Exception e) { - // Bad: Exposing stack trace and inconsistent format - return ResponseEntity.status(500).body("Server error: " + e.toString()); - } - } - - // Bad: Different controller with different exception handling approach - @PostMapping("/users") - public ResponseEntity createUser(@RequestBody UserCreateDTO user) { - try { - // ... business logic ... - return ResponseEntity.ok().build(); - } catch (RuntimeException e) { - // Bad: Yet another inconsistent error format, not using ProblemDetail - Map error = Map.of("error", e.getMessage()); - return ResponseEntity.status(500).body(error); - } - } - - // Bad: Using custom error response instead of standard ProblemDetail - @DeleteMapping("/users/{id}") - public ResponseEntity deleteUser(@PathVariable String id) { - try { - // ... business logic ... - return ResponseEntity.noContent().build(); - } catch (Exception e) { - // Bad: Custom error format instead of ProblemDetail - CustomErrorResponse error = new CustomErrorResponse( - "DELETE_ERROR", e.getMessage(), new Date() - ); - return ResponseEntity.status(500).body(error); - } - } -} - -// Bad: Custom error response class instead of using ProblemDetail -class CustomErrorResponse { - private String code; - private String message; - private Date timestamp; - // constructors, getters, setters... -} -``` - -## Rule 10: Implement Problem Details for Error Responses - -Title: Use RFC 7807 Problem Details for HTTP APIs -Description: Implement standardized error responses using RFC 7807 Problem Details format for HTTP 500 (Internal Server Error) and other error responses. This provides machine-readable error information that includes a type, title, status, detail, and instance to help clients understand and handle errors consistently. - -**Good example:** - -```java -@ControllerAdvice -public class ProblemDetailsExceptionHandler { - - private static final Logger logger = LoggerFactory.getLogger(ProblemDetailsExceptionHandler.class); - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleRuntimeException( - RuntimeException ex, HttpServletRequest request) { - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.INTERNAL_SERVER_ERROR, - "An unexpected error occurred while processing the request" - ); - - problemDetail.setType(URI.create("https://example.com/problems/internal-error")); - problemDetail.setTitle("Internal Server Error"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - problemDetail.setProperty("errorId", UUID.randomUUID().toString()); - - // Log the actual exception for debugging (don't expose to client) - logger.error("Internal server error with ID: {}", - problemDetail.getProperties().get("errorId"), ex); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail); - } - - @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity handleEntityNotFound( - EntityNotFoundException ex, HttpServletRequest request) { - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.NOT_FOUND, ex.getMessage() - ); - - problemDetail.setType(URI.create("https://example.com/problems/entity-not-found")); - problemDetail.setTitle("Entity Not Found"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); - } - - @ExceptionHandler(ValidationException.class) - public ResponseEntity handleValidation( - ValidationException ex, HttpServletRequest request) { - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( - HttpStatus.BAD_REQUEST, "Validation failed for the provided input" - ); - - problemDetail.setType(URI.create("https://example.com/problems/validation-error")); - problemDetail.setTitle("Validation Error"); - problemDetail.setInstance(URI.create(request.getRequestURI())); - problemDetail.setProperty("timestamp", Instant.now()); - problemDetail.setProperty("violations", ex.getViolations()); - - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); - } -} - -// Custom validation exception -class ValidationException extends RuntimeException { - private final List violations; - - public ValidationException(String message, List violations) { - super(message); - this.violations = violations; - } - - public List getViolations() { return violations; } -} -``` - -**Bad Example:** - -```java -@ControllerAdvice -public class BadExceptionHandler { - - @ExceptionHandler(RuntimeException.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public Map handleRuntimeException(RuntimeException ex) { - // Bad: Non-standard error format, inconsistent fields - Map error = new HashMap<>(); - error.put("error", true); - error.put("msg", "Something went wrong"); - error.put("exception_type", ex.getClass().getSimpleName()); - error.put("time", new Date()); - - // Bad: Exposing sensitive stack trace information - error.put("stack_trace", Arrays.toString(ex.getStackTrace())); - - return error; - } - - @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity handleNotFound(EntityNotFoundException ex) { - // Bad: Inconsistent response format (string vs JSON vs problem details) - return ResponseEntity.status(404).body("Not found: " + ex.getMessage()); - } - - @ExceptionHandler(ValidationException.class) - public ResponseEntity handleValidation(ValidationException ex) { - // Bad: Yet another inconsistent format - return ResponseEntity.badRequest().body( - Map.of("validationErrors", ex.getViolations()) - ); - } - - // Bad: Missing proper error ID, timestamps, and structured format - // Bad: No type URIs or standard problem details structure - // Bad: Inconsistent error formats across different exception types -} -``` diff --git a/.cursor/rules/303-frameworks-spring-data-jdbc.mdc b/.cursor/rules/303-frameworks-spring-data-jdbc.mdc deleted file mode 100644 index 7dd8b9a1..00000000 --- a/.cursor/rules/303-frameworks-spring-data-jdbc.mdc +++ /dev/null @@ -1,728 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Data JDBC with Records - -Spring Data JDBC provides a simpler alternative to JPA, offering direct SQL control while maintaining Spring's repository abstractions. When combined with Java records, it creates clean, immutable data models perfect for modern Java applications. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Immutability**: Use records for immutable entities that are thread-safe and predictable -- **Simplicity**: Leverage Spring Data JDBC's straightforward approach over complex ORM mapping -- **Constructor Injection**: Always use constructor-based dependency injection for better testability -- **Transaction Boundaries**: Keep transactions at the service layer, not repository layer -- **SQL Control**: Use custom queries when needed for optimal performance - -## Table of contents - -- Rule 1: Use Records for Entity Classes -- Rule 2: Implement Repository Pattern Correctly -- Rule 3: Handle Updates with Immutable Records -- Rule 4: Design Aggregate Relationships Properly -- Rule 5: Use Custom Queries for Complex Operations -- Rule 6: Implement Proper Transaction Management -- Rule 7: Embrace Single Query Loading to Eliminate N+1 Problems - -## Rule 1: Use Records for Entity Classes - -Title: Prefer Records Over Classes for Entity Definitions -Description: Records provide immutability, automatic equals/hashCode, and clean constructor-based mapping that works perfectly with Spring Data JDBC. They eliminate boilerplate code and ensure thread safety. Use @PersistenceCreator when you have multiple constructors to specify which one Spring Data JDBC should use. Use @Column to explicitly map record fields to database columns, especially when field names differ from column names. - -**Good example:** - -```java -public record Customer( - @Id - @Column("customer_id") - Long id, - - @Column("first_name") - String firstName, - - @Column("last_name") - String lastName, - - @Column("email_address") - String email, - - @Column("created_at") - LocalDateTime createdAt -) { - // Constructor for Spring Data JDBC (explicit annotation when multiple constructors exist) - @PersistenceCreator - public Customer(Long id, String firstName, String lastName, String email, LocalDateTime createdAt) { - this.id = id; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.createdAt = createdAt; - } - - // Factory method for new entities - public static Customer of(String firstName, String lastName, String email) { - return new Customer(null, firstName, lastName, email, LocalDateTime.now()); - } -} -``` - -**Bad Example:** - -```java -// Missing @PersistenceCreator annotation with multiple constructors -public record Customer( - @Id Long id, - String firstName, - String lastName, - String email, - LocalDateTime createdAt -) { - // Multiple constructors without @PersistenceCreator - Spring Data JDBC won't know which to use - public Customer(Long id, String firstName, String lastName, String email, LocalDateTime createdAt) { - this.id = id; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.createdAt = createdAt; - } - - public Customer(String firstName, String lastName, String email) { - this(null, firstName, lastName, email, LocalDateTime.now()); - } -} - -// Or using mutable entity class with boilerplate -public class Customer { - @Id - private Long id; - private String firstName; - private String lastName; - private String email; - private LocalDateTime createdAt; - - // Constructors, getters, setters, equals, hashCode... - // 50+ lines of boilerplate code -} -``` - -## Rule 2: Implement Repository Pattern Correctly - -Title: Extend Appropriate Repository Interfaces -Description: Use CrudRepository or PagingAndSortingRepository as base interfaces. Leverage method query derivation for simple queries and @Query for complex ones. Always annotate with @Repository. - -**Good example:** - -```java -@Repository -public interface CustomerRepository extends CrudRepository { - - // Method query derivation - List findByLastName(String lastName); - Optional findByEmail(String email); - - // Custom query for complex operations - @Query("SELECT * FROM customer WHERE email LIKE :pattern") - List findByEmailPattern(@Param("pattern") String pattern); -} -``` - -**Bad Example:** - -```java -// Missing @Repository annotation and poor method naming -public interface CustomerRepository extends CrudRepository { - - // Unclear method names that don't follow Spring Data conventions - List getCustomersWithLastName(String lastName); - - // Raw SQL without parameters - @Query("SELECT * FROM customer WHERE email LIKE '%@gmail.com%'") - List findGmailUsers(); -} -``` - -## Rule 3: Handle Updates with Immutable Records - -Title: Create New Record Instances for Updates -Description: Since records are immutable, create update methods that return new instances with modified values. This ensures data integrity and prevents accidental mutations. - -**Good example:** - -```java -public record Customer( - @Id - @Column("customer_id") - Long id, - - @Column("first_name") - String firstName, - - @Column("last_name") - String lastName, - - @Column("email_address") - String email, - - @Column("created_at") - LocalDateTime createdAt -) { - // Update method returns new instance - public Customer withEmail(String newEmail) { - return new Customer(id, firstName, lastName, newEmail, createdAt); - } - - public Customer withName(String firstName, String lastName) { - return new Customer(id, firstName, lastName, email, createdAt); - } -} - -// Service layer update -@Transactional -public Customer updateCustomerEmail(Long customerId, String newEmail) { - return customerRepository.findById(customerId) - .map(customer -> customer.withEmail(newEmail)) - .map(customerRepository::save) - .orElseThrow(() -> new CustomerNotFoundException(customerId)); -} -``` - -**Bad Example:** - -```java -// Trying to use setters with records (won't compile) -public record Customer(@Id Long id, String email) { - public void setEmail(String email) { // This won't work! - this.email = email; - } -} - -// Or using mutable wrapper approach -@Transactional -public Customer updateCustomerEmail(Long customerId, String newEmail) { - Customer customer = customerRepository.findById(customerId).orElseThrow(); - // Creating entirely new record instead of using update methods - Customer updated = new Customer(customer.id(), newEmail); - return customerRepository.save(updated); -} -``` - -## Rule 4: Design Aggregate Relationships Properly - -Title: Model Aggregates with Records and Sets -Description: Spring Data JDBC supports limited relationship types compared to JPA. Use records for aggregate roots and contained entities. Model one-to-many relationships with Set collections, use foreign key references for many-to-one, and avoid bidirectional references to maintain aggregate boundaries. For unsupported relationships like many-to-many, use junction tables or denormalization. - -**Good example:** - -```java -// ✅ One-to-Many: Primary supported relationship -public record Order( - @Id - @Column("order_id") - Long id, - - @Column("customer_id") - Long customerId, // Foreign key reference (Many-to-One) - - @Column("order_date") - LocalDateTime orderDate, - - @Column("order_status") - OrderStatus status, - - Set items // One-to-Many aggregate collection -) {} - -public record OrderItem( - @Id - @Column("item_id") - Long id, - - @Column("product_name") - String productName, - - @Column("price") - BigDecimal price, - - @Column("quantity") - int quantity -) {} - -// ✅ One-to-One: Using embedded objects -public record Customer( - @Id - @Column("customer_id") - Long id, - - @Column("customer_name") - String name, - - @Embedded.OnEmpty(USE_NULL) Address address // One-to-One embedded -) {} - -public record Address( - @Column("street_address") - String street, - - @Column("city") - String city, - - @Column("postal_code") - String postalCode, - - @Column("country") - String country -) {} - -// ✅ Many-to-Many workaround: Junction table with explicit entity -public record Student( - @Id - @Column("student_id") - Long id, - - @Column("student_name") - String name, - - Set enrollments // Access courses through junction -) {} - -public record StudentCourse( - @Id - @Column("enrollment_id") - Long id, - - @Column("student_id") - Long studentId, - - @Column("course_id") - Long courseId, - - @Column("enrolled_at") - LocalDateTime enrolledAt, - - @Column("grade") - String grade -) {} - -// ✅ Many-to-Many alternative: Store as delimited string or JSON -public record User( - @Id - @Column("user_id") - Long id, - - @Column("username") - String username, - - @Column("role_ids") - String roleIds // Store as "1,2,3" - simple cases only -) { - public List getRoleIdsList() { - return Arrays.stream(roleIds.split(",")) - .map(Long::parseLong) - .toList(); - } -} - -// Repository focuses on aggregate root only -@Repository -public interface OrderRepository extends CrudRepository { - List findByCustomerId(Long customerId); - List findByStatus(OrderStatus status); -} -``` - -**Bad Example:** - -```java -// ❌ Bidirectional references break aggregate boundaries -public record Order( - @Id - @Column("order_id") - Long id, - - Customer customer, // Don't embed full objects - use foreign keys - Set items -) {} - -public record OrderItem( - @Id - @Column("item_id") - Long id, - - @Column("product_name") - String productName, - - Order order // Don't include parent reference in aggregate -) {} - -// ❌ Attempting unsupported Many-to-Many directly -public record Student( - @Id - @Column("student_id") - Long id, - - @Column("student_name") - String name, - - Set courses // Spring Data JDBC doesn't support this -) {} - -public record Course( - @Id - @Column("course_id") - Long id, - - @Column("course_title") - String title, - - Set students // Bidirectional Many-to-Many not supported -) {} - -// ❌ Repository that violates aggregate boundaries -@Repository -public interface OrderItemRepository extends CrudRepository { - List findByOrderId(Long orderId); // Should go through Order aggregate -} - -// ❌ Overly large aggregates -public record Customer( - @Id - @Column("customer_id") - Long id, - - @Column("customer_name") - String name, - - Set orders, // Too large - creates performance issues - Set
addresses, - Set paymentMethods, - Set preferences -) {} -``` - -### Relationship Modeling Guidelines - -**Supported Relationships:** -- **One-to-Many**: Use `Set` in aggregate root (primary pattern) -- **One-to-One**: Use `@Embedded` for value objects or foreign key references -- **Many-to-One**: Use foreign key fields (`Long parentId`) - -**Unsupported Relationships:** -- **Many-to-Many**: Use junction tables or denormalization workarounds -- **Bidirectional**: Always model relationships unidirectionally - -**Best Practices:** -- Keep aggregates small and focused -- Use foreign key references between aggregates -- Load related data separately when needed -- Consider eventual consistency for cross-aggregate operations - -### Summary of Relationship Capabilities: - -| Relationship Type | Support Level | Approach | -|------------------|---------------|----------| -| **One-to-Many** | ✅ Full | Collections in aggregate root | -| **One-to-One** | ✅ Good | Embedded objects | -| **Many-to-One** | ⚠️ Limited | Foreign key references | -| **Many-to-Many** | ❌ None | Junction tables or denormalization | - -## Rule 5: Use Custom Queries for Complex Operations - -Title: Leverage @Query for Complex SQL Operations -Description: Use @Query annotation for complex queries that can't be expressed through method naming. Use proper parameter binding and consider performance implications. - -**Good example:** - -```java -@Repository -public interface CustomerRepository extends CrudRepository { - - @Query(""" - SELECT c.* FROM customer c - JOIN orders o ON c.id = o.customer_id - WHERE o.order_date BETWEEN :startDate AND :endDate - GROUP BY c.id - HAVING COUNT(o.id) >= :minOrders - """) - List findActiveCustomers( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - @Param("minOrders") int minOrders - ); - - @Modifying - @Query("UPDATE customer SET email = :email WHERE id = :id") - void updateCustomerEmail(@Param("id") Long id, @Param("email") String email); -} -``` - -**Bad Example:** - -```java -@Repository -public interface CustomerRepository extends CrudRepository { - - // SQL injection risk - not using parameters - @Query("SELECT * FROM customer WHERE email = '" + "email" + "'") - Customer findByEmailUnsafe(String email); - - // Overly complex query that should be broken down - @Query(""" - SELECT c.*, o.*, oi.*, p.* FROM customer c - LEFT JOIN orders o ON c.id = o.customer_id - LEFT JOIN order_item oi ON o.id = oi.order_id - LEFT JOIN product p ON oi.product_id = p.id - WHERE c.created_at > ?1 AND o.status = ?2 - """) - List findComplexCustomerData(LocalDateTime date, String status); -} -``` - -## Rule 6: Implement Proper Transaction Management - -Title: Use @Transactional at Service Layer -Description: Apply transaction boundaries at the service layer, not repository layer. Use readOnly=true for read operations and ensure proper transaction propagation. - -**Good example:** - -```java -@Service -@Transactional(readOnly = true) -public class CustomerService { - - private final CustomerRepository customerRepository; - - public CustomerService(CustomerRepository customerRepository) { - this.customerRepository = customerRepository; - } - - public Optional findByEmail(String email) { - return customerRepository.findByEmail(email); - } - - @Transactional - public Customer createCustomer(String firstName, String lastName, String email) { - var customer = Customer.of(firstName, lastName, email); - return customerRepository.save(customer); - } - - @Transactional - public Customer updateCustomerEmail(Long customerId, String newEmail) { - return customerRepository.findById(customerId) - .map(customer -> customer.withEmail(newEmail)) - .map(customerRepository::save) - .orElseThrow(() -> new CustomerNotFoundException(customerId)); - } -} -``` - -**Bad Example:** - -```java -// No transaction management -public class CustomerService { - - private final CustomerRepository customerRepository; - - // Auto-commit for each operation - no transaction control - public Customer createCustomer(String firstName, String lastName, String email) { - var customer = Customer.of(firstName, lastName, email); - return customerRepository.save(customer); // Each call is separate transaction - } - - // Read operation without readOnly optimization - @Transactional - public Optional findByEmail(String email) { - return customerRepository.findByEmail(email); // Should be readOnly=true - } -} -``` - -## Rule 7: Embrace Single Query Loading to Eliminate N+1 Problems - -Title: Leverage Spring Data JDBC's Eager Loading to Avoid N+1 Query Issues -Description: Spring Data JDBC loads entire aggregates in single queries, automatically eliminating the N+1 problem that plagues JPA/Hibernate applications. Unlike JPA's lazy loading approach, Spring Data JDBC eagerly loads all aggregate data in one query, ensuring predictable performance and eliminating the need for complex fetch strategies. - -**Good example:** - -```java -// ✅ Spring Data JDBC loads entire aggregate in single query -public record Order( - @Id - @Column("order_id") - Long id, - - @Column("customer_id") - Long customerId, - - @Column("order_date") - LocalDateTime orderDate, - - @Column("total_amount") - BigDecimal totalAmount, - - Set items // All items loaded in single query -) {} - -public record OrderItem( - @Id - @Column("item_id") - Long id, - - @Column("product_name") - String productName, - - @Column("unit_price") - BigDecimal unitPrice, - - @Column("quantity") - int quantity -) {} - -@Repository -public interface OrderRepository extends CrudRepository { - List findByCustomerId(Long customerId); -} - -// ✅ Service that benefits from single query loading -@Service -@Transactional(readOnly = true) -public class OrderService { - - private final OrderRepository orderRepository; - - public List getCustomerOrderSummaries(Long customerId) { - // Single query loads all orders with their items - return orderRepository.findByCustomerId(customerId) - .stream() - .map(order -> new OrderSummary( - order.id(), - order.orderDate(), - order.totalAmount(), - order.items().size(), // No additional query needed - order.items().stream() - .mapToDouble(item -> item.unitPrice().doubleValue() * item.quantity()) - .sum() - )) - .toList(); - } -} - -public record OrderSummary( - Long orderId, - LocalDateTime orderDate, - BigDecimal totalAmount, - int itemCount, - double calculatedTotal -) {} - -// Generated SQL - Single query with JOIN: -// SELECT o.order_id, o.customer_id, o.order_date, o.total_amount, -// oi.item_id, oi.product_name, oi.unit_price, oi.quantity -// FROM orders o -// LEFT JOIN order_item oi ON o.order_id = oi.order_id -// WHERE o.customer_id = ? -``` - -**Bad Example:** - -```java -// ❌ JPA-style thinking that would cause N+1 problems -@Entity // Wrong - this is JPA, not Spring Data JDBC -public class Order { - @Id - private Long id; - - @OneToMany(fetch = FetchType.LAZY) // Lazy loading causes N+1 - private Set items; - - // getters/setters... -} - -// ❌ Code that would trigger N+1 in JPA (but works fine in Spring Data JDBC) -@Service -public class OrderService { - - public List getCustomerOrderSummaries(Long customerId) { - List orders = orderRepository.findByCustomerId(customerId); - - return orders.stream() - .map(order -> new OrderSummary( - order.getId(), - order.getOrderDate(), - order.getTotalAmount(), - order.getItems().size(), // In JPA: N+1 query here! - order.getItems().stream() // In JPA: Additional queries! - .mapToDouble(item -> item.getUnitPrice() * item.getQuantity()) - .sum() - )) - .toList(); - } -} - -// JPA would generate N+1 queries: -// 1. SELECT * FROM orders WHERE customer_id = ? -// 2. SELECT * FROM order_item WHERE order_id = 1 -- For each order -// 3. SELECT * FROM order_item WHERE order_id = 2 -// 4. SELECT * FROM order_item WHERE order_id = 3 -// ... N additional queries for N orders - -// ❌ Trying to manually optimize with separate queries -@Service -public class OrderService { - - public List getCustomerOrderSummaries(Long customerId) { - // Manually loading in separate steps - unnecessary complexity - List orders = orderRepository.findByCustomerId(customerId); - - List orderIds = orders.stream() - .map(Order::id) - .toList(); - - // Additional repository method needed - List allItems = orderItemRepository.findByOrderIdIn(orderIds); - - // Complex manual mapping required - Map> itemsByOrder = allItems.stream() - .collect(Collectors.groupingBy(OrderItem::orderId)); - - // Error-prone manual association - return orders.stream() - .map(order -> { - List orderItems = itemsByOrder.getOrDefault(order.id(), List.of()); - return new OrderSummary(/* complex mapping */); - }) - .toList(); - } -} -``` - -### Key Benefits of Spring Data JDBC's Approach - -**Eliminates N+1 Problems:** -- Entire aggregates loaded in single query with JOINs -- No lazy loading means no surprise additional queries -- Predictable query patterns and performance - -**Simplified Development:** -- No need for `@EntityGraph` or fetch strategies -- No need to worry about Hibernate session management -- No proxy objects or lazy initialization exceptions - -**Performance Transparency:** -- What you see is what you get - one query per repository call -- Easy to predict and optimize database access patterns -- No hidden queries triggered by accessing collections - -**Trade-offs to Consider:** -- Larger initial queries (but often more efficient overall) -- Cannot selectively load parts of aggregates -- May load more data than needed in some scenarios (design aggregates carefully) - - - - - - diff --git a/.cursor/rules/304-frameworks-spring-boot-hikari.mdc b/.cursor/rules/304-frameworks-spring-boot-hikari.mdc deleted file mode 100644 index 47d3426b..00000000 --- a/.cursor/rules/304-frameworks-spring-boot-hikari.mdc +++ /dev/null @@ -1,396 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Boot HikariCP Connection Pool Configuration - -HikariCP is the default connection pool for Spring Boot and is known for being the fastest, most reliable connection pool available for Java applications. This guide will help you configure HikariCP optimally for your Spring Boot applications. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Performance First**: Configure pool sizes based on your application's actual database concurrency needs -- **Resource Efficiency**: Balance connection availability with memory and database server resources -- **Monitoring & Observability**: Enable metrics and logging to understand pool behavior -- **Environment-Specific**: Adjust configurations based on development, testing, and production environments -- **Fail-Fast**: Configure appropriate timeouts to detect issues quickly - -## Table of contents - -- Rule 1: Essential Pool Sizing Configuration -- Rule 2: Connection Timeout and Lifecycle Management -- Rule 3: Health Check and Validation Configuration -- Rule 4: Performance Monitoring and Metrics -- Rule 5: Environment-Specific Configuration Strategies - -## Rule 1: Essential Pool Sizing Configuration - -**Title**: Right-size your connection pool based on application needs - -**Description**: The most critical aspect of HikariCP configuration is determining the optimal pool size. Ask yourself: "How many concurrent database operations does my application actually need?" Most applications need far fewer connections than developers initially think. - -**Key Questions to Ask:** -- How many concurrent users will access my application? -- How many database operations happen per user request? -- What's my database server's connection limit? -- Am I running multiple application instances? - -**Good example:** - -```yaml -# application.yml -spring: - datasource: - hikari: - # Start with this formula: connections = ((core_count * 2) + effective_spindle_count) - # For most web apps: 10-15 connections is often sufficient - maximum-pool-size: 10 - minimum-idle: 5 - # Allow pool to shrink during low activity - idle-timeout: 300000 # 5 minutes -``` - -```java -// For programmatic configuration -@Configuration -public class DatabaseConfig { - - @Bean - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig hikariConfig() { - HikariConfig config = new HikariConfig(); - // Conservative pool sizing for most applications - config.setMaximumPoolSize(10); - config.setMinimumIdle(5); - config.setIdleTimeout(300_000); - return config; - } -} -``` - -**Bad Example:** - -```yaml -# application.yml - DON'T DO THIS -spring: - datasource: - hikari: - # Too many connections - wastes resources and can overwhelm DB - maximum-pool-size: 100 - minimum-idle: 50 - # Never let connections be idle - keeps unnecessary connections - idle-timeout: 0 -``` - -## Rule 2: Connection Timeout and Lifecycle Management - -**Title**: Configure appropriate timeouts for reliable connection handling - -**Description**: Proper timeout configuration ensures your application fails fast when database issues occur and doesn't hold onto stale connections. Ask yourself: "How long should my application wait for a database connection before giving up?" - -**Key Questions to Ask:** -- What's an acceptable wait time for users when the database is under load? -- How quickly should I detect database connectivity issues? -- What's my application's typical query execution time? - -**Good example:** - -```yaml -# application.yml -spring: - datasource: - hikari: - # Fast failure for connection acquisition - connection-timeout: 20000 # 20 seconds - adjust based on your needs - # Detect stale connections quickly - max-lifetime: 1800000 # 30 minutes - less than DB connection timeout - # Quick validation of connections - validation-timeout: 5000 # 5 seconds - # Test connections when borrowed from pool - connection-test-query: SELECT 1 -``` - -```java -// Programmatic configuration with monitoring -@Configuration -public class DatabaseConfig { - - @Bean - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig hikariConfig() { - HikariConfig config = new HikariConfig(); - config.setConnectionTimeout(20_000); - config.setMaxLifetime(1_800_000); - config.setValidationTimeout(5_000); - - // Enable connection testing - config.setConnectionTestQuery("SELECT 1"); - return config; - } -} -``` - -**Bad Example:** - -```yaml -# application.yml - DON'T DO THIS -spring: - datasource: - hikari: - # Too long - users will think app is frozen - connection-timeout: 120000 - # Too long - may exceed DB server timeout - max-lifetime: 7200000 - # No validation - stale connections may be used - # connection-test-query: # missing -``` - -## Rule 3: Health Check and Validation Configuration - -**Title**: Implement robust connection health checking - -**Description**: Configure HikariCP to validate connections and maintain pool health. Ask yourself: "How can I ensure my application always gets working database connections?" - -**Key Questions to Ask:** -- Does my database server have connection timeouts? -- How can I detect network issues between app and database? -- Should I validate connections proactively or reactively? - -**Good example:** - -```yaml -# application.yml -spring: - datasource: - hikari: - # Lightweight validation query for most databases - connection-test-query: SELECT 1 - # Validate connections when borrowed (recommended for production) - validation-timeout: 5000 - # Remove connections that fail validation - leak-detection-threshold: 60000 # 60 seconds - helps find connection leaks -``` - -```java -// Database-specific configuration -@Configuration -@Profile("production") -public class ProductionDatabaseConfig { - - @Bean - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig hikariConfig() { - HikariConfig config = new HikariConfig(); - - // PostgreSQL-specific validation - config.setConnectionTestQuery("SELECT 1"); - config.setValidationTimeout(5_000); - config.setLeakDetectionThreshold(60_000); - - // Additional PostgreSQL optimizations - config.addDataSourceProperty("socketTimeout", "30"); - config.addDataSourceProperty("loginTimeout", "10"); - - return config; - } -} -``` - -**Bad Example:** - -```yaml -# application.yml - DON'T DO THIS -spring: - datasource: - hikari: - # Heavy validation query that impacts performance - connection-test-query: "SELECT COUNT(*) FROM large_table WHERE complex_condition = 'value'" - # No leak detection - memory leaks may go unnoticed - # leak-detection-threshold: # missing -``` - -## Rule 4: Performance Monitoring and Metrics - -**Title**: Enable comprehensive monitoring and metrics collection - -**Description**: Configure HikariCP to provide visibility into connection pool behavior. Ask yourself: "How will I know if my connection pool is properly sized and performing well?" - -**Key Questions to Ask:** -- How can I monitor pool utilization in production? -- What metrics indicate pool sizing issues? -- How do I correlate application performance with database connection patterns? - -**Good example:** - -```yaml -# application.yml -spring: - datasource: - hikari: - # Enable detailed metrics - register-mbeans: true - pool-name: "HikariPool-${spring.application.name}" - -# Enable JMX metrics for monitoring tools -management: - endpoints: - web: - exposure: - include: health,metrics,hikari - metrics: - export: - prometheus: - enabled: true -``` - -```java -// Comprehensive monitoring setup -@Configuration -@ConditionalOnProperty(name = "management.metrics.enabled", matchIfMissing = true) -public class DatabaseMonitoringConfig { - - @Bean - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig hikariConfig(MeterRegistry meterRegistry) { - HikariConfig config = new HikariConfig(); - - // Enable metrics collection - config.setRegisterMbeans(true); - config.setPoolName("HikariPool-" + getApplicationName()); - config.setMetricRegistry(meterRegistry); - - // Configure alerts for pool exhaustion - config.setConnectionTimeout(20_000); - config.setLeakDetectionThreshold(60_000); - - return config; - } - - private String getApplicationName() { - return System.getProperty("spring.application.name", "app"); - } -} -``` - -**Bad Example:** - -```yaml -# application.yml - DON'T DO THIS -spring: - datasource: - hikari: - # No monitoring enabled - flying blind in production - register-mbeans: false - # Generic pool name - hard to identify in monitoring tools - pool-name: "pool" -``` - -## Rule 5: Environment-Specific Configuration Strategies - -**Title**: Adapt HikariCP configuration for different environments - -**Description**: Configure HikariCP differently for development, testing, and production environments. Ask yourself: "What are the different requirements for each environment where my application runs?" - -**Key Questions to Ask:** -- How do my development and production database loads differ? -- Should I use different pool sizes for testing vs production? -- How can I make troubleshooting easier in development? - -**Good example:** - -```yaml -# application-dev.yml - Development environment -spring: - datasource: - hikari: - maximum-pool-size: 5 # Smaller pool for dev - minimum-idle: 2 - connection-timeout: 30000 # Longer timeout for debugging - leak-detection-threshold: 30000 # Faster leak detection for dev - register-mbeans: true # Enable for local monitoring - -# application-prod.yml - Production environment -spring: - datasource: - hikari: - maximum-pool-size: 20 # Larger pool for production load - minimum-idle: 10 - connection-timeout: 20000 # Fast failure in production - idle-timeout: 300000 # Allow shrinking during low load - max-lifetime: 1800000 # Refresh connections regularly - leak-detection-threshold: 60000 - register-mbeans: true - pool-name: "${spring.application.name}-prod" -``` - -```java -// Environment-specific configuration -@Configuration -public class EnvironmentSpecificDatabaseConfig { - - @Bean - @Profile("development") - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig devHikariConfig() { - HikariConfig config = new HikariConfig(); - // Development: Favor debugging over performance - config.setMaximumPoolSize(5); - config.setConnectionTimeout(30_000); - config.setLeakDetectionThreshold(30_000); - config.setRegisterMbeans(true); - return config; - } - - @Bean - @Profile("production") - @ConfigurationProperties("spring.datasource.hikari") - public HikariConfig prodHikariConfig() { - HikariConfig config = new HikariConfig(); - // Production: Favor performance and reliability - config.setMaximumPoolSize(20); - config.setMinimumIdle(10); - config.setConnectionTimeout(20_000); - config.setIdleTimeout(300_000); - config.setMaxLifetime(1_800_000); - config.setLeakDetectionThreshold(60_000); - config.setRegisterMbeans(true); - config.setPoolName(getApplicationName() + "-prod"); - return config; - } - - private String getApplicationName() { - return System.getProperty("spring.application.name", "app"); - } -} -``` - -**Bad Example:** - -```yaml -# Same configuration for all environments - DON'T DO THIS -spring: - datasource: - hikari: - maximum-pool-size: 50 # Too many for dev, maybe wrong for prod - minimum-idle: 25 # Wastes resources in all environments - connection-timeout: 60000 # Too slow for production - # No environment-specific tuning -``` - ---- - -## Quick Configuration Checklist - -Before deploying your HikariCP configuration, ask yourself these questions: - -1. **Pool Sizing**: Have I calculated the right pool size based on my application's concurrency needs? -2. **Timeouts**: Are my timeouts appropriate for fast failure detection without being too aggressive? -3. **Monitoring**: Can I see pool utilization and performance metrics in my monitoring system? -4. **Environment Differences**: Do I have different configurations for dev, test, and production? -5. **Database Specifics**: Have I configured database-specific optimizations? -6. **Connection Health**: Am I validating connections appropriately without impacting performance? -7. **Resource Limits**: Are my pool settings within my database server's connection limits? - -Remember: Start with conservative settings and adjust based on monitoring data from your actual production load! \ No newline at end of file diff --git a/.cursor/rules/311-frameworks-spring-boot-slice-testing.mdc b/.cursor/rules/311-frameworks-spring-boot-slice-testing.mdc deleted file mode 100644 index 00c6d541..00000000 --- a/.cursor/rules/311-frameworks-spring-boot-slice-testing.mdc +++ /dev/null @@ -1,415 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Boot Slice Testing - -Spring Boot slice testing allows you to test specific layers or "slices" of your application in isolation, providing faster and more focused tests than full integration tests. This approach helps maintain test clarity, reduces test execution time, and improves maintainability. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Layer Isolation**: Test each application layer independently without loading the entire Spring context -- **Focused Testing**: Use appropriate slice annotations to load only the components needed for specific functionality -- **Mock Dependencies**: Mock external dependencies and other layers to achieve true unit testing at the slice level -- **Fast Execution**: Minimize Spring context loading to achieve rapid test feedback cycles - -## Table of contents - -- Rule 1: Use @WebMvcTest for Web Layer Testing -- Rule 2: Use @JdbcTest for Repository Layer Testing -- Rule 3: Use @JsonTest for JSON Serialization Testing -- Rule 4: Use @MockBean for Mocking Dependencies -- Rule 5: Configure Test Profiles Appropriately -- Rule 6: Use @TestConfiguration for Custom Test Setup - -## Rule 1: Use @WebMvcTest for Web Layer Testing - -Title: Test Controllers in Isolation with @WebMvcTest -Description: Use @WebMvcTest to test only the web layer (controllers) without loading the full application context. This annotation configures Spring MVC infrastructure and auto-configures MockMvc for testing HTTP requests and responses. - -**Good example:** - -```java -@WebMvcTest(UserController.class) -class UserControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private UserService userService; - - @Test - void shouldReturnUserWhenValidId() throws Exception { - // Given - User user = new User(1L, "John Doe", "john@example.com"); - when(userService.findById(1L)).thenReturn(user); - - // When & Then - mockMvc.perform(get("/api/users/1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.name").value("John Doe")) - .andExpect(jsonPath("$.email").value("john@example.com")); - } -} -``` - -**Bad Example:** - -```java -@SpringBootTest -@AutoConfigureTestDatabase -class UserControllerTest { - - @Autowired - private TestRestTemplate restTemplate; - - @Test - void shouldReturnUser() { - // This loads the entire application context unnecessarily - // and requires database setup for a simple controller test - ResponseEntity response = restTemplate.getForEntity("/api/users/1", User.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } -} -``` - -## Rule 2: Use @JdbcTest for Repository Layer Testing - -Title: Test JDBC Repositories with @JdbcTest -Description: Use @JdbcTest to test Spring Data JDBC repositories in isolation. This annotation configures an in-memory database, auto-configures JdbcTemplate and NamedParameterJdbcTemplate, and loads Spring Data JDBC repositories without loading the full application context. - -**Good example:** - -```java -@JdbcTest -class UserRepositoryTest { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Autowired - private UserRepository userRepository; - - @Test - void shouldFindUserByEmail() { - // Given - User user = new User(null, "John Doe", "john@example.com"); - User saved = userRepository.save(user); - - // When - Optional found = userRepository.findByEmail("john@example.com"); - - // Then - assertThat(found).isPresent(); - assertThat(found.get().getName()).isEqualTo("John Doe"); - assertThat(found.get().getEmail()).isEqualTo("john@example.com"); - } - - @Test - void shouldReturnEmptyWhenUserNotFound() { - // When - Optional found = userRepository.findByEmail("nonexistent@example.com"); - - // Then - assertThat(found).isEmpty(); - } - - @Test - void shouldUseJdbcTemplateForCustomQueries() { - // Given - jdbcTemplate.update( - "INSERT INTO users (name, email) VALUES (?, ?)", - "Jane Smith", "jane@example.com" - ); - - // When - Long count = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM users WHERE email LIKE '%@example.com'", - Long.class - ); - - // Then - assertThat(count).isEqualTo(1L); - } -} -``` - -**Bad Example:** - -```java -@SpringBootTest -class UserRepositoryTest { - - @Autowired - private UserRepository userRepository; - - @Test - void shouldFindUserByEmail() { - // This loads the entire application context and all beans - // unnecessarily for a simple repository test - User user = new User(null, "John Doe", "john@example.com"); - userRepository.save(user); - - Optional found = userRepository.findByEmail("john@example.com"); - assertThat(found).isPresent(); - } -} -``` - -## Rule 3: Use @JsonTest for JSON Serialization Testing - -Title: Test JSON Serialization/Deserialization with @JsonTest -Description: Use @JsonTest to test JSON serialization and deserialization logic in isolation. This annotation auto-configures Jackson ObjectMapper and provides JacksonTester helper for testing JSON operations. - -**Good example:** - -```java -@JsonTest -class UserJsonTest { - - @Autowired - private JacksonTester json; - - @Test - void shouldSerializeUser() throws Exception { - // Given - User user = new User(1L, "John Doe", "john@example.com"); - - // When & Then - assertThat(json.write(user)) - .hasJsonPathNumberValue("$.id", 1) - .hasJsonPathStringValue("$.name", "John Doe") - .hasJsonPathStringValue("$.email", "john@example.com"); - } - - @Test - void shouldDeserializeUser() throws Exception { - // Given - String content = """ - { - "id": 1, - "name": "John Doe", - "email": "john@example.com" - } - """; - - // When & Then - assertThat(json.parse(content)) - .usingRecursiveComparison() - .isEqualTo(new User(1L, "John Doe", "john@example.com")); - } -} -``` - -**Bad Example:** - -```java -@SpringBootTest -class UserJsonTest { - - @Autowired - private ObjectMapper objectMapper; - - @Test - void shouldSerializeUser() throws Exception { - // Loading full application context just for JSON testing - // is overkill and slow - User user = new User(1L, "John Doe", "john@example.com"); - String json = objectMapper.writeValueAsString(user); - - assertThat(json).contains("John Doe"); - } -} -``` - -## Rule 4: Use @MockBean for Mocking Dependencies - -Title: Mock External Dependencies with @MockBean -Description: Use @MockBean to mock Spring beans that are dependencies of the component under test. This replaces the bean in the Spring context with a Mockito mock, allowing you to control its behavior during tests. - -**Good example:** - -```java -@WebMvcTest(OrderController.class) -class OrderControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private OrderService orderService; - - @MockBean - private PaymentService paymentService; - - @Test - void shouldCreateOrder() throws Exception { - // Given - CreateOrderRequest request = new CreateOrderRequest("Product A", 2); - Order order = new Order(1L, "Product A", 2, BigDecimal.valueOf(100.00)); - - when(orderService.createOrder(any(CreateOrderRequest.class))).thenReturn(order); - - // When & Then - mockMvc.perform(post("/api/orders") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "productName": "Product A", - "quantity": 2 - } - """)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.productName").value("Product A")); - } -} -``` - -**Bad Example:** - -```java -@WebMvcTest(OrderController.class) -class OrderControllerTest { - - @Autowired - private MockMvc mockMvc; - - // Missing @MockBean - this will cause the test to fail - // because OrderService is not available in the context - - @Test - void shouldCreateOrder() throws Exception { - mockMvc.perform(post("/api/orders") - .contentType(MediaType.APPLICATION_JSON) - .content("{}")) - .andExpect(status().isCreated()); - // This test will fail due to missing dependencies - } -} -``` - -## Rule 5: Configure Test Profiles Appropriately - -Title: Use Test Profiles for Environment-Specific Configuration -Description: Configure specific test profiles to override application properties for testing scenarios. Use @ActiveProfiles to activate test-specific configurations that differ from production settings. - -**Good example:** - -```java -@JdbcTest -@ActiveProfiles("test") -class UserRepositoryIntegrationTest { - - @Autowired - private UserRepository userRepository; - - @Test - void shouldUseTestDatabaseConfiguration() { - // Test will use application-test.yml configuration - // which might specify H2 in-memory database - User user = new User(null, "Test User", "test@example.com"); - User saved = userRepository.save(user); - - assertThat(saved.getId()).isNotNull(); - } -} -``` - -**Bad Example:** - -```java -@JdbcTest -class UserRepositoryIntegrationTest { - - // No @ActiveProfiles annotation - // This might use production database configuration - // leading to unreliable or slow tests - - @Autowired - private UserRepository userRepository; - - @Test - void shouldSaveUser() { - User user = new User(null, "Test User", "test@example.com"); - userRepository.save(user); - } -} -``` - -## Rule 6: Use @TestConfiguration for Custom Test Setup - -Title: Create Custom Test Configuration with @TestConfiguration -Description: Use @TestConfiguration to define test-specific bean configurations that override or supplement the main application configuration during testing. - -**Good example:** - -```java -@WebMvcTest(UserController.class) -class UserControllerTest { - - @TestConfiguration - static class TestConfig { - - @Bean - @Primary - public Clock testClock() { - return Clock.fixed( - Instant.parse("2023-12-01T10:00:00Z"), - ZoneOffset.UTC - ); - } - } - - @Autowired - private MockMvc mockMvc; - - @MockBean - private UserService userService; - - @Test - void shouldUseFixedTimeForTesting() throws Exception { - // Test with predictable time for consistent results - mockMvc.perform(get("/api/users/current-time")) - .andExpect(status().isOk()) - .andExpect(content().string("2023-12-01T10:00:00Z")); - } -} -``` - -**Bad Example:** - -```java -@WebMvcTest(UserController.class) -class UserControllerTest { - - // No test configuration for time-dependent tests - // This makes tests unreliable and hard to reproduce - - @Autowired - private MockMvc mockMvc; - - @MockBean - private UserService userService; - - @Test - void shouldReturnCurrentTime() throws Exception { - mockMvc.perform(get("/api/users/current-time")) - .andExpect(status().isOk()); - // Cannot assert exact time value due to system clock dependency - } -} -``` - -### Additional Slice Testing Annotations - -**@WebFluxTest**: For testing Spring WebFlux reactive web applications -**@RestClientTest**: For testing REST clients and @RestTemplate configurations -**@AutoConfigureTestDatabase**: For configuring test databases in slice tests -**@TestPropertySource**: For overriding specific properties in test scenarios -**@DataJdbcTest**: Alternative to @JdbcTest that focuses specifically on Spring Data JDBC repositories -**@Sql**: For executing SQL scripts before test execution in JDBC tests \ No newline at end of file diff --git a/.cursor/rules/312-frameworks-spring-boot-integration-testing.mdc b/.cursor/rules/312-frameworks-spring-boot-integration-testing.mdc deleted file mode 100644 index d3ec007f..00000000 --- a/.cursor/rules/312-frameworks-spring-boot-integration-testing.mdc +++ /dev/null @@ -1,648 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Java Integration testing guidelines - -These guidelines aim to ensure consistency, reliability, and maintainability of integration tests within the project. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- Principle 1: Test Isolation - Each integration test must be independent and not affect other tests -- Principle 2: Environment Reproducibility - Use containerized dependencies for consistent test environments -- Principle 3: Clear Test Boundaries - Focus on integration points rather than duplicating unit test logic -- Principle 4: Performance Optimization - Balance thorough testing with execution speed -- Principle 5: Maintainable Assertions - Use specific, clear assertions that provide meaningful feedback -- Principle 6: Resource Management - Properly manage external dependencies and cleanup after tests - -## Table of contents - -- Rule 1: Define Clear Scope and Purpose for Integration Tests -- Rule 2: Manage Test Environment & Dependencies with Testcontainers -- Rule 3: Utilize TestRestTemplate for Robust API Testing -- Rule 4: Implement Consistent Data Management Strategies -- Rule 5: Maintain Clear Test Structure and Assertions -- Rule 6: Optimize for Performance and Ensure Proper Cleanup - -## Rule 1: Define Clear Scope and Purpose for Integration Tests - -Title: Clearly Define the Scope and Purpose of Each Integration Test -Description: -- Integration tests must verify the interaction between multiple components or systems (e.g., service layer with database, service-to-service communication over HTTP). -- Clearly define the boundary of each integration test. What specific interaction, contract, or flow is being tested? -- Prefer integration tests for verifying contracts between services (APIs) and interactions with external dependencies (databases, message queues, etc.). -- Avoid replicating complex business logic in integration tests if it is already thoroughly covered by unit tests. Focus on the integration points. - -**Good example:** -```java -// Assume: ProductService interacts with ProductRepository (database) and NotificationService (external HTTP) - -// @SpringBootTest // or similar context for the test -// @Testcontainers // if using Testcontainers -public class ProductServiceIT { - private static final Logger log = LoggerFactory.getLogger(ProductServiceIT.class); - - // @Autowired - // private ProductService productService; - - // @Autowired - // private ProductRepository productRepository; // To verify DB state - - // Mock or use a Testcontainer for NotificationService if its actual calls are out of scope - // @MockBean - // private NotificationService mockNotificationService; - - // @Test - void should_createProduct_saveToDatabase_and_sendNotification() { - // Scope: Test the flow of creating a product, ensuring it's saved, - // and that a notification attempt is made. - - // Given: A product DTO - // ProductDto newProductDto = new ProductDto("Laptop X1", 1500.00); - - // When: ProductService creates the product - // Product createdProduct = productService.createProduct(newProductDto); - - // Then: Verify interactions - // 1. Product is saved in the database (verify via repository or direct query) - // Optional savedEntity = productRepository.findById(createdProduct.getId()); - // assertThat(savedEntity).isPresent(); - // assertThat(savedEntity.get().getName()).isEqualTo("Laptop X1"); - - // 2. Notification service was called (verify via mock or wiremock if testing HTTP contract) - // verify(mockNotificationService).sendProductCreationNotification(any(Product.class)); - log.info("Conceptual test: Product creation flow verified."); - } -} -``` - -**Bad Example:** -```java -// @SpringBootTest -public class OverlappingProductLogicIT { - private static final Logger log = LoggerFactory.getLogger(OverlappingProductLogicIT.class); - - // @Autowired - // private ProductService productService; - - // @Test - void should_calculateComplexPricing_duringProductCreation() { - // Bad: This test might be re-testing complex pricing logic - // that should already be unit-tested in ProductService or a PricingEngine unit test. - // The integration test should focus on whether ProductService correctly integrates - // with the database and other services during creation, assuming pricing logic is correct. - - // ProductDto productWithComplexPricing = new ProductDto("ComplexItem", 10.0, List.of(new DiscountRule(...))); - // Product createdProduct = productService.createProduct(productWithComplexPricing); - - // If asserts here are deeply checking specific price calculations, it's likely a unit test concern. - // assertThat(createdProduct.getFinalPrice()).isEqualTo(9.99); // This might be too specific for an IT - log.warn("Conceptual bad test: Replicating unit test logic for pricing."); - } -} -``` - -## Rule 2: Manage Test Environment & Dependencies with Testcontainers - -Title: Use Testcontainers for Reliable Management of External Dependencies -Description: -- Use Testcontainers (`org.testcontainers:testcontainers`) to manage external dependencies (databases, message brokers, caches, other services) required for the test. Avoid relying on pre-existing, shared external environments to ensure test isolation and reproducibility. -- Declare containerized dependencies using `@Testcontainers` and `@Container` annotations for JUnit 5 integration (`org.testcontainers:junit-jupiter`). Manage container lifecycles appropriately (per test suite using `static @Container` or per test method, favoring suite-level for performance). -- Use official or well-maintained Docker images for dependencies. Pin image versions (e.g., `"postgres:15-alpine"`) to ensure reproducible builds. -- Configure containers programmatically (ports, environment variables, wait strategies) within the test setup. Use `Wait.for...` strategies (e.g., `Wait.forHttp("/health")`, `Wait.forLogMessage(...)`) to ensure containers are ready before tests run. -- Inject dynamic container properties (like mapped ports or JDBC URLs) into the application context or test configuration. For Spring Boot, use `@DynamicPropertySource` with a static method. For others, manually retrieve properties in setup methods. - -**Good example:** -```java -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import static org.assertj.core.api.Assertions.assertThat; // For assertion - -@Testcontainers -@SpringBootTest // Or relevant test context setup -class MyRepositoryIT { - private static final Logger log = LoggerFactory.getLogger(MyRepositoryIT.class); - - @Container // Static -> shared container for all tests in this class - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine") - .withDatabaseName("testdb") - .withUsername("testuser") - .withPassword("testpass"); - // .waitingFor(Wait.forListeningPort()); // Default wait strategy is often sufficient for DBs - - // Dynamically set properties based on container info - @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); - registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop"); - } - - // Inject your repository/service here - // @Autowired - // private MyRepository repository; - - @Test - void should_connectAndInteractWithDatabase() { - // Test logic interacting with the repository, - // which uses the Testcontainer database - assertThat(postgres.isRunning()).isTrue(); - log.info("PostgreSQL container is running on JDBC URL: {}", postgres.getJdbcUrl()); - // ... perform repository operations and assertions ... - // Example: MyEntity entity = new MyEntity("testData"); - // repository.save(entity); - // Optional found = repository.findById(entity.getId()); - // assertThat(found).isPresent(); - } -} -``` - -**Bad Example:** -```java -// @SpringBootTest -public class MyServiceReliesOnExternalDbIT { - private static final Logger log = LoggerFactory.getLogger(MyServiceReliesOnExternalDbIT.class); - - // @Autowired - // private MyDataService dataService; - - // No Testcontainers. This test assumes an external PostgreSQL server - // is running on localhost:5432 with specific credentials and schema. - // spring.datasource.url=jdbc:postgresql://localhost:5432/mydb_dev - // spring.datasource.username=dev_user - // spring.datasource.password=dev_secret - - // @Test - void should_fetchDataFromPreConfiguredExternalDatabase() { - // Bad: Test depends on an external, manually configured database. - // - Not isolated: Other tests or developers might change the DB state. - // - Not reproducible: Fails if DB is down, schema changes, or on CI without the DB. - // - Hard to manage data state between tests. - // List data = dataService.findAll(); - // assertThat(data).isNotEmpty(); // This might pass or fail based on external DB state. - log.warn("Conceptual bad test: Relies on external, shared database."); - } -} -``` - -## Rule 3: Utilize TestRestTemplate for Robust API Testing - -Title: Employ TestRestTemplate for Testing RESTful APIs Following Arrange/Act/Assert -Description: -- Use Spring Boot's `TestRestTemplate` (provided by `spring-boot-starter-test`) for testing RESTful APIs in integration tests. -- Structure tests using the Arrange/Act/Assert pattern: - - **Arrange**: Set up request data, headers, authentication, and test prerequisites. - - **Act**: Perform the HTTP request using TestRestTemplate methods (`getForEntity()`, `postForEntity()`, `exchange()`, etc.). - - **Assert**: Validate the response (status code, headers, response body) using AssertJ or JUnit assertions. -- Always validate the HTTP status code first using `ResponseEntity.getStatusCode()`. -- Use AssertJ assertions for clear and fluent validation of response content. Be specific but avoid overly brittle assertions (e.g., don't assert entire large JSON bodies if only a few fields matter). -- Configure TestRestTemplate in a `@BeforeEach` method or inject it via `@Autowired`. For custom configurations (authentication, headers), use `TestRestTemplate.withBasicAuth()` or create `HttpEntity` with custom headers. -- Handle authentication consistently using TestRestTemplate's built-in methods or by setting appropriate headers in `HttpEntity`. -- For complex request/response bodies, use POJOs that will be automatically serialized/deserialized by Spring's message converters (Jackson by default). - -**Good example:** -```java -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.Assertions.assertThat; - -// Assuming a simple DTO for request/response -class ResourceDto { - public int id; - public String name; - public String data; - public ResourceDto() {} - public ResourceDto(int id, String name, String data) { this.id = id; this.name = name; this.data = data;} -} - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -// Assume a controller exists at /resources that uses ResourceDto -// and has GET /resources/{id}, POST /resources -class MyApiControllerIT { - - @LocalServerPort - private int port; - - @Autowired - private TestRestTemplate restTemplate; - - @Test - void getResourceById_shouldReturnOkAndResource() { - // Arrange: Set up the expected resource ID - int resourceId = 123; - - // Act: Perform GET request - ResponseEntity response = restTemplate.getForEntity( - "/resources/{id}", - ResourceDto.class, - resourceId - ); - - // Assert: Validate response - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().id).isEqualTo(123); - assertThat(response.getBody().name).contains("ResourceName"); // Flexible assertion - assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); - } - - @Test - void createResource_shouldReturnCreatedAndResourceLocation() { - // Arrange: Set up request data and headers - ResourceDto newResource = new ResourceDto(0, "New Item", "Some data"); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity request = new HttpEntity<>(newResource, headers); - - // Act: Perform POST request - ResponseEntity response = restTemplate.postForEntity( - "/resources", - request, - ResourceDto.class - ); - - // Assert: Validate response - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().id).isNotNull(); // Assert that an ID was generated - assertThat(response.getBody().name).isEqualTo("New Item"); - assertThat(response.getHeaders().getLocation()).isNotNull(); - assertThat(response.getHeaders().getLocation().toString()).contains("/resources/"); - } -} -``` - -**Bad Example:** -```java -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.ResponseEntity; -import static org.assertj.core.api.Assertions.assertThat; - -// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ApiTestAntiPatternsIT { - private static final Logger log = LoggerFactory.getLogger(ApiTestAntiPatternsIT.class); - - // @Autowired private TestRestTemplate restTemplate; - - @Test - void getResource_badAssertions() { - // Bad: Not checking status code first or at all. - // Bad: Extracting entire response as string and doing string manipulations. - // ResponseEntity response = restTemplate.getForEntity("/resources/1", String.class); - // String responseBody = response.getBody(); - // assertThat(responseBody).contains("\"id\":1"); // Brittle, hard to read, no status check - - // Bad: Overly specific assertions on large JSON strings. - // assertThat(responseBody).isEqualTo("{very long and complex json string...}"); // Brittle - log.warn("Conceptual bad API test: Poor assertions, missing status code check."); - } - - @Test - void createResource_noBodyValidation() { - // Bad: Not validating the structure or content of the response body upon creation. - // ResourceDto newResource = new ResourceDto(0, "Test", "data"); - // ResponseEntity response = restTemplate.postForEntity("/resources", newResource, Void.class); - // assertThat(response.getStatusCode().value()).isEqualTo(201); // Only checks status code, ignores response body - log.warn("Conceptual bad API test: Missing response body validation."); - } -} -``` - -## Rule 4: Implement Consistent Data Management Strategies - -Title: Ensure Controlled and Isolated Data States for Each Test -Description: -- Each integration test must run with a known, controlled data state to ensure reliability and prevent interference between tests. Tests must be independent. -- Seed necessary test data before each test (`@BeforeEach`) or test suite (`@BeforeAll`). Options include: - - **Application Services:** Call repository or service methods to set up required entities. - - **Object Mothers / Test Data Builders:** Use patterns to create complex test data objects easily and consistently. - - **SQL Scripts:** Use `@Sql` (Spring) or execute scripts via JDBC/Testcontainers `execInContainer()` for setup. -- Clean up persistent data created during a test run to ensure test isolation. Choose one primary strategy: - - **Transaction Rollback:** (Preferred for simplicity if applicable, e.g., Spring Test with `@Transactional`) Annotate test methods or the class. Spring Test will automatically roll back the transaction after each test for database operations within that transaction. - - **Truncate/Delete Tables:** Execute `TRUNCATE TABLE ...` or `DELETE FROM ...` statements in `@AfterEach` or via Testcontainers. Fastest for complex state reset if transactions are not manageable across all interactions. - - **Delete Specific Data:** Use repository/service methods in `@AfterEach` to delete only the data created by the test (can be complex to track and error-prone). - - **Container Recreation:** Recreate the database container per test or class (very slow, generally avoided unless absolutely necessary for complete isolation between test classes). - -**Good example:** -(Using Spring Test `@Transactional` for rollback) -```java -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; -// Assume Entity: Item with id, name -// Assume Repository: ItemRepository extends JpaRepository - -// @Testcontainers // If DB is managed by Testcontainers -// @SpringBootTest -// @Transactional // This will roll back transactions after each test method -public class ItemRepositoryTransactionalIT { - private static final Logger log = LoggerFactory.getLogger(ItemRepositoryTransactionalIT.class); - - // @Autowired - // private ItemRepository itemRepository; - - // @Test - void should_saveAndRetrieveItem() { - // Item newItem = new Item("Test Item"); - // Item savedItem = itemRepository.save(newItem); - // assertThat(savedItem.getId()).isNotNull(); - - // Optional foundItem = itemRepository.findById(savedItem.getId()); - // assertThat(foundItem).isPresent(); - // assertThat(foundItem.get().getName()).isEqualTo("Test Item"); - log.info("Conceptual test: Save and retrieve with @Transactional rollback."); - // Data inserted here will be rolled back automatically after this test method. - } - - // @Test - void should_findNoItems_ifNoneSavedInThisTest() { - // List items = itemRepository.findAll(); - // assertThat(items).isEmpty(); - log.info("Conceptual test: Ensuring test isolation via @Transactional."); - // Due to rollback from other tests, this test starts with a clean state (within its transaction). - } -} -``` - -**Bad Example:** -```java -// @SpringBootTest -// @Testcontainers -public class ItemRepositoryNoCleanupIT { - private static final Logger log = LoggerFactory.getLogger(ItemRepositoryNoCleanupIT.class); - - // @Autowired - // private ItemRepository itemRepository; - private static Long sharedItemId; // Bad: Sharing state between tests via static field - - // @Test // Assume tests run in unpredictable order - void testA_createItem() { - // Item item = new Item("Shared Item"); - // item = itemRepository.save(item); - // sharedItemId = item.getId(); - // assertThat(itemRepository.count()).isGreaterThan(0); - log.warn("Conceptual bad test A: Creates data that might affect other tests."); - } - - // @Test - void testB_checkIfItemExists() { - // Bad: This test's success depends on testA_createItem() having run first - // and no cleanup being performed. This leads to flaky and order-dependent tests. - // if (sharedItemId != null) { - // Optional item = itemRepository.findById(sharedItemId); - // assertThat(item).isPresent(); - // } else { - // List items = itemRepository.findAll(); - // assertThat(items.stream().anyMatch(i -> i.getName().equals("Shared Item"))).isTrue(); // Brittle check - // } - log.warn("Conceptual bad test B: Depends on data from another test due to no cleanup."); - } -} -``` - -## Rule 5: Maintain Clear Test Structure and Assertions - -Title: Structure Integration Tests Clearly and Use Specific Assertions -Description: -- Keep integration tests focused on a single user story, API endpoint interaction, or component integration scenario. -- Use descriptive test method names (e.g., `should_ExpectedBehavior_when_StateUnderTest`) or JUnit 5's `@DisplayName` annotation to clearly explain the scenario being tested. -- Assertions should be specific and provide clear failure messages. - - **TestRestTemplate:** Use AssertJ assertions for clear validation of response status, headers, and body content (e.g., `assertThat(response.getBody().fieldName).isEqualTo(expectedValue)`). - - **Database State:** Use repositories or JDBC to fetch data after the action and assert its state using libraries like AssertJ for fluent and readable assertions. -- For debugging, consider logging response details in test methods during development, but remove verbose logging in committed code to keep test output clean and focus on assertion failures. - -**Good example:** -```java -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import static org.assertj.core.api.Assertions.assertThat; - -class UserDto { - public Long id; - public String username; - public String email; - public UserDto() {} - public UserDto(String username, String email) { this.username = username; this.email = email; } -} - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class UserRegistrationIT { - private static final Logger log = LoggerFactory.getLogger(UserRegistrationIT.class); - - // @Autowired private UserRepository userRepository; - // @Autowired private TestRestTemplate restTemplate; - - @Test - @DisplayName("POST /users with valid data should create user, return 201, and user details") - void postUsers_withValidData_shouldCreateUserAndReturn201() { - // Arrange: Prepare request data - // UserDto newUser = new UserDto("testuser", "test@example.com"); - // HttpHeaders headers = new HttpHeaders(); - // headers.setContentType(MediaType.APPLICATION_JSON); - // HttpEntity request = new HttpEntity<>(newUser, headers); - - // Act: Send POST request - // ResponseEntity response = restTemplate.postForEntity("/users", request, UserDto.class); - - // Assert: Validate API response - // assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - // assertThat(response.getBody()).isNotNull(); - // assertThat(response.getBody().username).isEqualTo("testuser"); - // assertThat(response.getBody().email).isEqualTo("test@example.com"); - // assertThat(response.getBody().id).isNotNull(); - - // Verify database state (using AssertJ for fluent assertions) - // Long newUserId = response.getBody().id; - // Optional createdUser = userRepository.findById(newUserId); - // assertThat(createdUser).isPresent(); - // assertThat(createdUser.get().getEmail()).isEqualTo("test@example.com"); - log.info("Conceptual good test: Clear name, focused scope, specific assertions."); - } -} -``` - -**Bad Example:** -```java -// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class VagueUserActionsIT { - private static final Logger log = LoggerFactory.getLogger(VagueUserActionsIT.class); - - // @Autowired private TestRestTemplate restTemplate; - - // @Test - void testUserActions() { // Bad: Vague test name, unclear scope - // This test might try to do too many things: - // 1. Create a user - // restTemplate.postForEntity("/users", userDto, UserDto.class); // No assertions - - // 2. Update the user - // restTemplate.put("/users/1", updatedUserDto); // No return value validation - - // 3. Fetch the user and verify all fields - // ResponseEntity response = restTemplate.getForEntity("/users/1", String.class); - // String responseBody = response.getBody(); - // Bad: Asserting a large string is brittle. - // assertThat(responseBody).isEqualTo("{ \"id\":1, \"name\":\"updated\", ... very_long_json ... }"); // Should use specific field assertions instead - - // 4. Delete the user - // restTemplate.delete("/users/1"); // No status code validation - log.warn("Conceptual bad test: Vague name, too broad, brittle assertions."); - // Problem: If one part fails, it's hard to know which interaction broke. - // Assertions are not specific enough or are too brittle. - } -} -``` - -## Rule 6: Optimize for Performance and Ensure Proper Cleanup - -Title: Be Mindful of Integration Test Performance and Resource Cleanup -Description: -- Be mindful of integration test execution time. Container startup is often the main overhead. - - **Prefer static `@Container` fields:** This reuses the same container for all tests within a class, significantly speeding up test suites. - - **Consider Singleton Container Pattern:** For sharing a container across multiple test classes (more advanced setup, use with caution to maintain isolation if state leaks). -- Ensure Testcontainers resources are stopped and removed after the test suite finishes. The `testcontainers-junit-jupiter` extension handles this automatically for containers managed via `@Container`. If managing containers manually, ensure `stop()` is called in a suitable cleanup hook (e.g., `@AfterAll` or a JVM shutdown hook for true singletons). -- Separate integration tests (e.g., `*IT.java` or `*IntegrationTest.java`) from unit tests (`*Test.java`) using naming conventions. Configure build tools (Maven Surefire/Failsafe, Gradle) to run them in different phases or tasks if needed (integration tests often run after the application is packaged). - -**Good example:** -(Using static @Container for performance and automatic cleanup by junit-jupiter extension) -```java -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; -import static org.assertj.core.api.Assertions.assertThat; // For assertion - -@Testcontainers -public class MyServiceWithSharedContainerIT { - private static final Logger log = LoggerFactory.getLogger(MyServiceWithSharedContainerIT.class); - - // Good: Static container is started once for all tests in this class - @Container - static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) - .withExposedPorts(6379); - - @BeforeAll - static void beforeAll() { - log.info("Redis container started for suite: {}:{}", redis.getContainerIpAddress(), redis.getMappedPort(6379)); - // Setup SUT to use redis.getMappedPort(6379) etc. - } - - @Test - void testOperationOne_usesRedis() { - assertThat(redis.isRunning()).isTrue(); - // ... test logic interacting with service that uses Redis ... - log.info("Test one with shared Redis."); - } - - @Test - void testOperationTwo_usesRedis() { - assertThat(redis.isRunning()).isTrue(); - // ... another test logic ... - log.info("Test two with shared Redis."); - } - - // @AfterAll // Not strictly needed for @Container, as Testcontainers extension handles stop() - // static void afterAll() { - // log.info("Suite finished, Testcontainers will stop the Redis container."); - // } -} -``` - -**Bad Example:** -```java -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; - -// @Testcontainers // Annotation might be missing or misused -public class MyServiceWithPerMethodContainerIT { - private static final Logger log = LoggerFactory.getLogger(MyServiceWithPerMethodContainerIT.class); - - // Bad: Non-static @Container (or manual management per method) starts a new container for EACH test method. - // This is very slow for multiple tests. - // @Container // If this were not static, it would be per-method if @Testcontainers is on class - private GenericContainer redisPerMethod; - - // @BeforeEach // Manual start/stop per method is slow and error-prone - void setUpPerMethod() { - redisPerMethod = new GenericContainer<>(DockerImageName.parse("redis:5-alpine")) - .withExposedPorts(6379); - redisPerMethod.start(); // Manual start - log.info("Redis started for method at port: {}", redisPerMethod.getMappedPort(6379)); - } - - // @Test - void testA() { - // assertThat(redisPerMethod.isRunning()).isTrue(); - log.info("Test A using its own Redis instance."); - } - - // @Test - void testB() { - // assertThat(redisPerMethod.isRunning()).isTrue(); - log.warn("Test B using its own Redis instance (slow!)."); - } - - // @AfterEach - void tearDownPerMethod() { - if (redisPerMethod != null) { - redisPerMethod.stop(); // Manual stop needed - log.info("Redis stopped for method."); - } - } - // Problem: Significant performance degradation due to container restart for every test. - // Also, higher risk of resource leaks if stop() is missed or fails. -} -``` - ---- -This rule serves as a starting point. Refer to authoritative resources on Java Integration Testing, Testcontainers, and Spring Boot's TestRestTemplate for more in-depth understanding and application. - diff --git a/.cursor/rules/313-frameworks-spring-boot-local-testing.mdc b/.cursor/rules/313-frameworks-spring-boot-local-testing.mdc deleted file mode 100644 index 1e68c86a..00000000 --- a/.cursor/rules/313-frameworks-spring-boot-local-testing.mdc +++ /dev/null @@ -1,477 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Boot Local Testing with Docker Compose - -Best practices for local testing in Spring Boot applications using `spring-boot-docker-compose` for seamless integration with external services like databases, message queues, and caches. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Seamless Integration**: Use spring-boot-docker-compose to automatically manage external service dependencies -- **Environment Parity**: Maintain consistency between local development and production environments -- **Test Isolation**: Ensure tests are independent and can run in any order without side effects -- **Performance Optimization**: Minimize container startup time and resource usage for faster development cycles -- **Configuration Management**: Use profiles and dynamic properties for flexible environment-specific configurations - -## Table of contents - -- Rule 1: Dependency Configuration -- Rule 2: Docker Compose Service Definition -- Rule 3: Application Profile Configuration -- Rule 4: Integration Test Setup -- Rule 5: Service Connection Management -- Rule 6: Health Check Implementation -- Rule 7: Test Data Management -- Rule 8: Performance Optimization - -## Rule 1: Dependency Configuration - -Title: Proper Spring Boot Docker Compose Dependency Setup -Description: Configure the spring-boot-docker-compose dependency correctly for runtime-only usage to automatically manage Docker services during application startup. - -**Good example:** - -```xml - - - org.springframework.boot - spring-boot-docker-compose - runtime - - - - - org.springframework.boot - spring-boot-testcontainers - test - -``` - -**Bad Example:** - -```xml - - - org.springframework.boot - spring-boot-docker-compose - compile - - - -``` - -## Rule 2: Docker Compose Service Definition - -Title: Well-structured Docker Compose Configuration -Description: Define services with proper health checks, environment variables, and port mappings for reliable local testing. - -**Good example:** - -```yaml -# compose.yaml -services: - postgres: - image: 'postgres:15' - environment: - POSTGRES_DB: testdb - POSTGRES_USER: testuser - POSTGRES_PASSWORD: testpass - ports: - - '5432:5432' - healthcheck: - test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - - postgres_data:/var/lib/postgresql/data - -volumes: - postgres_data: -``` - -**Bad Example:** - -```yaml -# compose.yaml -services: - postgres: - image: 'postgres' # No version specified - environment: - - POSTGRES_PASSWORD=password # Hardcoded, no DB/USER - # Missing health checks - # Missing volume persistence - # Missing proper port mapping -``` - -## Rule 3: Application Profile Configuration - -Title: Environment-specific Configuration Management -Description: Use Spring profiles to manage different configurations for local development, testing, and production environments. - -**Good example:** - -```yaml -# application-local.yml -spring: - docker: - compose: - enabled: true - file: compose.yaml - lifecycle-management: start_and_stop - readiness: - wait: HEALTHY - timeout: 2m - - datasource: - url: jdbc:postgresql://localhost:5432/testdb - username: testuser - password: testpass - - jpa: - hibernate: - ddl-auto: create-drop - show-sql: true - -logging: - level: - org.springframework.boot.docker: DEBUG -``` - -**Bad Example:** - -```yaml -# application.yml -spring: - docker: - compose: - enabled: true # Always enabled, no profile separation - lifecycle-management: none # No lifecycle management - - datasource: - url: jdbc:postgresql://localhost:5432/proddb # Production DB in local config - username: root # Unsafe credentials - password: admin -``` - -## Rule 4: Integration Test Setup - -Title: Testcontainers Integration with Service Connections -Description: Use @ServiceConnection and @Testcontainers for clean integration test setup with automatic container management. - -**Good example:** - -```java -@SpringBootTest -@Testcontainers -@ActiveProfiles("test") -class UserRepositoryIntegrationTest { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15") - .withDatabaseName("testdb") - .withUsername("test") - .withPassword("test"); - - @Autowired - private UserRepository userRepository; - - @Test - void shouldSaveAndRetrieveUser() { - User user = new User("john.doe@example.com", "John Doe"); - User saved = userRepository.save(user); - - assertThat(saved.getId()).isNotNull(); - assertThat(userRepository.findByEmail("john.doe@example.com")) - .isPresent() - .get() - .extracting(User::getName) - .isEqualTo("John Doe"); - } -} -``` - -**Bad Example:** - -```java -@SpringBootTest -class UserRepositoryIntegrationTest { - - // No container management - // No profile specification - // Assumes external database is running - - @Autowired - private UserRepository userRepository; - - @Test - void shouldSaveAndRetrieveUser() { - // Test may fail if database is not available - // No cleanup between tests - User user = new User("john.doe@example.com", "John Doe"); - userRepository.save(user); - // No proper assertions - } -} -``` - -## Rule 5: Service Connection Management - -Title: Dynamic Service Configuration -Description: Use @DynamicPropertySource and @ServiceConnection for flexible service configuration in tests. - -**Good example:** - -```java -@TestConfiguration -public class TestContainersConfiguration { - - @Bean - @ServiceConnection - PostgreSQLContainer postgresContainer() { - return new PostgreSQLContainer<>("postgres:15") - .withDatabaseName("testdb") - .withUsername("test") - .withPassword("test") - .withReuse(true); // Reuse container across tests - } - - @Bean - @ServiceConnection - RedisContainer redisContainer() { - return new RedisContainer("redis:7-alpine") - .withReuse(true); - } -} - -// Alternative approach with @DynamicPropertySource -@DynamicPropertySource -static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); -} -``` - -**Bad Example:** - -```java -@TestConfiguration -public class TestConfiguration { - - // Hardcoded connection properties - @Bean - public DataSource dataSource() { - HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:postgresql://localhost:5432/testdb"); - dataSource.setUsername("test"); - dataSource.setPassword("test"); - return dataSource; - } - - // No container lifecycle management - // No dynamic property configuration -} -``` - -## Rule 6: Health Check Implementation - -Title: Robust Service Health Monitoring -Description: Implement proper health checks for all external services to ensure they are ready before running tests. - -**Good example:** - -```yaml -# compose.yaml -services: - postgres: - image: postgres:15 - healthcheck: - test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - - redis: - image: redis:7-alpine - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 3 - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] - interval: 30s - timeout: 10s - retries: 5 -``` - -**Bad Example:** - -```yaml -# compose.yaml -services: - postgres: - image: postgres:15 - # No health check - application may start before DB is ready - - redis: - image: redis:7-alpine - # No readiness verification - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0 - # May cause intermittent test failures -``` - -## Rule 7: Test Data Management - -Title: Clean Test Data Handling -Description: Ensure proper test data setup and cleanup for reliable and isolated tests. - -**Good example:** - -```java -@SpringBootTest -@Testcontainers -@Transactional -@Rollback -class OrderServiceIntegrationTest { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15"); - - @Autowired - private OrderService orderService; - - @Autowired - private TestEntityManager entityManager; - - @Test - @Sql(scripts = "/test-data/orders.sql", executionPhase = BEFORE_TEST_METHOD) - @Sql(scripts = "/test-data/cleanup.sql", executionPhase = AFTER_TEST_METHOD) - void shouldProcessOrderCorrectly() { - Order order = orderService.createOrder(createOrderRequest()); - - entityManager.flush(); - entityManager.clear(); - - Order retrieved = orderService.findById(order.getId()); - assertThat(retrieved.getStatus()).isEqualTo(OrderStatus.PENDING); - } - - private OrderRequest createOrderRequest() { - return OrderRequest.builder() - .customerId(1L) - .items(List.of(new OrderItem("product-1", 2))) - .build(); - } -} -``` - -**Bad Example:** - -```java -@SpringBootTest -class OrderServiceIntegrationTest { - - @Autowired - private OrderService orderService; - - @Test - void shouldProcessOrderCorrectly() { - // No data cleanup - may affect other tests - // No transaction management - // Hardcoded data that may conflict - Order order = new Order(); - order.setId(1L); // Hardcoded ID - order.setCustomerId(999L); // May not exist - - orderService.save(order); - // No proper verification - } -} -``` - -## Rule 8: Performance Optimization - -Title: Optimized Container and Test Execution -Description: Configure container reuse and optimize resource usage for faster development cycles. - -**Good example:** - -```properties -# application-test.properties -spring.docker.compose.lifecycle-management=start_only -testcontainers.reuse.enable=true - -# Resource optimization -spring.jpa.hibernate.ddl-auto=create-drop -spring.jpa.show-sql=false -logging.level.org.hibernate.SQL=WARN -``` - -```java -@TestMethodOrder(OrderAnnotation.class) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class UserServicePerformanceTest { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15") - .withReuse(true) - .withTmpFs(Map.of("/var/lib/postgresql/data", "rw")); - - @BeforeAll - void setupTestData() { - // Setup once for all tests - } - - @AfterAll - void cleanup() { - // Cleanup once after all tests - } -} -``` - -**Bad Example:** - -```properties -# application-test.properties -spring.docker.compose.lifecycle-management=start_and_stop -# No container reuse - slow test execution - -spring.jpa.show-sql=true # Verbose logging slows down tests -logging.level.org.hibernate=DEBUG -``` - -```java -class UserServiceTest { - - @Container - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15"); - // No reuse configuration - // No memory optimization - - @BeforeEach - void setup() { - // Expensive setup for each test - populateDatabase(); - } - - @AfterEach - void cleanup() { - // Expensive cleanup for each test - clearDatabase(); - } -} \ No newline at end of file diff --git a/.cursor/rules/321-frameworks-spring-boot-native-compilation.mdc b/.cursor/rules/321-frameworks-spring-boot-native-compilation.mdc deleted file mode 100644 index 5681a725..00000000 --- a/.cursor/rules/321-frameworks-spring-boot-native-compilation.mdc +++ /dev/null @@ -1,396 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# Spring Boot Native Compilation - -Guidelines for building Spring Boot applications as GraalVM native images for improved startup time, reduced memory footprint, and enhanced performance characteristics. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- **Compile-time analysis**: All code paths must be discoverable at compile time -- **Minimal reflection usage**: Avoid runtime reflection and dynamic class loading -- **Explicit configuration**: Configure native hints for reflection, resources, and proxies -- **Profile-guided optimization**: Use AOT processing and build-time optimizations -- **Resource efficiency**: Optimize for reduced memory usage and faster startup - -## Table of contents - -- Rule 1: Configure Native Build Tools and Dependencies -- Rule 2: Manage Reflection and Dynamic Features -- Rule 3: Handle Resources and Configuration Files -- Rule 4: Optimize Application Profiles for Native -- Rule 5: Test Native Image Compatibility -- Rule 6: Use AOT Processing and Build-time Hints - -## Rule 1: Configure Native Build Tools and Dependencies - -**Title**: Proper Native Build Configuration -**Description**: Configure Maven or Gradle with the Spring Boot Native plugin and GraalVM dependencies. Ensure all necessary build tools are properly set up for native compilation. - -**Good example:** - -```xml - - - 0.9.28 - 3.5.0 - - - - - org.springframework.boot - spring-boot-starter - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - - - native - - - - org.springframework.boot - spring-boot-maven-plugin - - - process-aot - - process-aot - - - - - - org.graalvm.buildtools - native-maven-plugin - - - add-reachability-metadata - - add-reachability-metadata - - - - - - - - -``` - -**Bad Example:** - -```xml - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - -``` - -## Rule 2: Manage Reflection and Dynamic Features - -**Title**: Minimize and Configure Reflection Usage -**Description**: Avoid runtime reflection and dynamic class loading. When reflection is necessary, provide explicit native hints or use Spring's AOT processing to register classes at build time. - -**Good example:** - -```java -// Use @RegisterReflectionForBinding for data classes -@RegisterReflectionForBinding({Person.class, Address.class}) -@RestController -public class PersonController { - - @GetMapping("/person") - public Person getPerson() { - return new Person("John", "Doe"); - } -} - -// For explicit reflection hints -@Component -public class ReflectionHints implements RuntimeHintsRegistrar { - - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - hints.reflection() - .registerType(MyClass.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) - .registerType(AnotherClass.class, MemberCategory.DECLARED_FIELDS); - } -} - -// Use @ImportRuntimeHints to register hints -@ImportRuntimeHints(ReflectionHints.class) -@SpringBootApplication -public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} -``` - -**Bad Example:** - -```java -// Avoid runtime class loading and reflection -@RestController -public class BadController { - - @GetMapping("/dynamic") - public Object getDynamic() throws Exception { - // This will fail in native image - Class clazz = Class.forName("com.example.DynamicClass"); - return clazz.getDeclaredConstructor().newInstance(); - } -} -``` - -## Rule 3: Handle Resources and Configuration Files - -**Title**: Explicit Resource Configuration -**Description**: Register all resources (properties files, templates, static content) that need to be included in the native image. Use resource hints for files accessed at runtime. - -**Good example:** - -```java -@Component -public class ResourceHints implements RuntimeHintsRegistrar { - - @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - hints.resources() - .registerPattern("templates/*.html") - .registerPattern("static/**") - .registerPattern("config/*.properties") - .registerPattern("META-INF/spring.factories"); - } -} - -// For simple cases, use @RegisterReflectionForBinding -@RegisterReflectionForBinding(MyConfigProperties.class) -@ConfigurationProperties(prefix = "app") -public class MyConfigProperties { - private String name; - private int timeout; - // getters and setters -} -``` - -**Bad Example:** - -```java -// Avoid dynamic resource loading without hints -@Service -public class BadResourceService { - - public String loadTemplate(String templateName) { - // This might fail if template is not registered - return Files.readString(Paths.get("templates/" + templateName + ".html")); - } -} -``` - -## Rule 4: Optimize Application Profiles for Native - -**Title**: Native-Specific Configuration -**Description**: Create native-specific application profiles and configurations that optimize for native image characteristics like reduced memory usage and faster startup. - -**Good example:** - -```properties -# application-native.properties -spring.jpa.defer-datasource-initialization=false -spring.sql.init.mode=never -spring.jpa.hibernate.ddl-auto=none - -# Reduce logging overhead -logging.level.root=WARN -logging.level.org.springframework=INFO - -# Optimize connection pools for native -spring.datasource.hikari.maximum-pool-size=5 -spring.datasource.hikari.minimum-idle=1 - -# Disable features that don't work well with native -spring.devtools.enabled=false -management.endpoint.beans.enabled=false -``` - -```java -@Profile("native") -@Configuration -public class NativeConfiguration { - - @Bean - @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "native") - public DataSource nativeDataSource() { - HikariConfig config = new HikariConfig(); - config.setMaximumPoolSize(5); - config.setMinimumIdle(1); - config.setConnectionTimeout(10000); - return new HikariDataSource(config); - } -} -``` - -**Bad Example:** - -```properties -# Don't use development-oriented settings in native -spring.devtools.enabled=true -spring.jpa.show-sql=true -spring.jpa.hibernate.ddl-auto=create-drop -logging.level.root=DEBUG -``` - -## Rule 5: Test Native Image Compatibility - -**Title**: Comprehensive Native Testing Strategy -**Description**: Implement testing strategies that verify native image compatibility, including integration tests that run against the native executable. - -**Good example:** - -```java -@ActiveProfiles("native") -@SpringBootTest -class NativeImageIntegrationTest { - - @Test - void contextLoads() { - // Test that application context loads successfully - } - - @Test - void serializationWorks() { - // Test JSON serialization/deserialization - ObjectMapper mapper = new ObjectMapper(); - Person person = new Person("John", "Doe"); - - assertDoesNotThrow(() -> { - String json = mapper.writeValueAsString(person); - Person deserialized = mapper.readValue(json, Person.class); - assertEquals(person.getName(), deserialized.getName()); - }); - } -} - -// Maven configuration for native tests -``` - -```xml - - org.graalvm.buildtools - native-maven-plugin - - - test-native - - test - - - - -``` - -**Bad Example:** - -```java -// Don't assume JVM tests will work in native -@SpringBootTest -class OnlyJvmTest { - - @Test - void testReflection() { - // This might pass on JVM but fail in native - Class.forName("some.dynamic.Class"); - } -} -``` - -## Rule 6: Use AOT Processing and Build-time Hints - -**Title**: Leverage Ahead-of-Time Processing -**Description**: Use Spring's AOT processing capabilities to generate optimized code and configuration at build time, reducing runtime overhead and improving native image compatibility. - -**Good example:** - -```java -@Component -public class AotHints implements BeanFactoryInitializationAotProcessor { - - @Override - public BeanRegistrationAotContribution processAheadOfTime( - ConfigurableListableBeanFactory beanFactory) { - - return (generationContext, beanRegistrationCode) -> { - // Generate AOT-optimized bean registration code - RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - runtimeHints.reflection().registerType(MyService.class); - }; - } -} - -// Use @Conditional annotations that work at build time -@ConditionalOnProperty(name = "feature.enabled", havingValue = "true") -@Component -public class ConditionalService { - // Implementation -} -``` - -**Bad Example:** - -```java -// Avoid runtime conditional logic that can't be determined at build time -@Component -public class BadConditionalService { - - @PostConstruct - public void init() { - if (System.getProperty("runtime.feature") != null) { - // This kind of runtime decision making doesn't work well with AOT - loadDynamicConfiguration(); - } - } -} -``` - -### Build Commands for Native Compilation - -**Maven Commands:** -```bash -# Build native image -./mvnw -Pnative native:compile - -# Build and test native image -./mvnw -Pnative clean native:test - -# Build container image with native binary -./mvnw spring-boot:build-image -Pnative -``` - -### Performance Considerations - -- **Startup Time**: Native images typically start 10-100x faster than JVM -- **Memory Usage**: Reduced memory footprint, especially for smaller applications -- **Build Time**: Native compilation takes longer than regular JAR builds -- **Image Size**: Native executables are typically larger than JAR files but smaller when considering JVM overhead -- **Runtime Performance**: May have slightly different performance characteristics compared to JVM with JIT compilation \ No newline at end of file diff --git a/.cursor/rules/500-sql.mdc b/.cursor/rules/500-sql.mdc deleted file mode 100644 index 85295092..00000000 --- a/.cursor/rules/500-sql.mdc +++ /dev/null @@ -1,274 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# SQL Development Guidelines - -This document provides a comprehensive set of guidelines for SQL development, covering aspects from naming conventions and table design to query optimization, security, and testing, aimed at promoting best practices in database management and application development. - -## Implementing These Principles - -These guidelines are built upon the following core principles: - -- Principle 1: Clarity and Maintainability: Ensuring SQL code and database structures are easy to understand, modify, and maintain. -- Principle 2: Performance and Scalability: Designing databases and queries for optimal performance and the ability to handle growing data and user loads. -- Principle 3: Data Integrity and Security: Protecting data accuracy, consistency, and safeguarding against unauthorized access or loss. -- Principle 4: Consistency: Applying uniform standards across the database schema and SQL code. - -## Table of contents - -- Rule 1: Naming Conventions -- Rule 2: Table Design -- Rule 3: Query Writing -- Rule 4: Indexing Strategy -- Rule 5: Security Guidelines -- Rule 6: Performance Optimization Tips -- Rule 7: Transaction Guidelines -- Rule 8: Migration Best Practices -- Rule 9: Code Examples -- Rule 10: Testing Guidelines -- Rule 11: Monitoring Practices - -## Rule 1: Naming Conventions - -Title: Consistent Naming for Database Objects -Description: Defines standard naming conventions for database objects like tables, columns, foreign keys, views, triggers, indexes, and constraints to ensure clarity and consistency across the database schema. - -**Good example:** - -**General:** -- Use snake_case for all database objects -- Use plural for table names (e.g., `users`, `orders`) -- Use singular for column names (e.g., `user_id`, `order_date`) -- Prefix foreign keys with referenced table name (e.g., `user_id`) -- Use verb_noun format for stored procedures (e.g., `get_user`, `update_order`) - -**Prefixes:** -- Views: `v_` -- Triggers: `trg_` -- Indexes: `idx_` -- Constraints: - - Primary Key: `pk_` - - Foreign Key: `fk_` - - Unique: `uq_` - - Check: `ck_` - -## Rule 2: Table Design - -Title: Best Practices for Designing Database Tables -Description: Outlines best practices for table design, including primary keys, appropriate data types, normalization (aiming for at least 3NF), inclusion of timestamp columns (`created_at`, `updated_at`), use of foreign key constraints for referential integrity, consideration for soft deletes (`deleted_at`), and strategic indexing for frequently queried columns. - -**Good example:** - -**Best Practices:** -- Always include a primary key. -- Use appropriate data types for columns. -- Normalize to at least 3NF unless there's a good reason not to. -- Include `created_at` and `updated_at` timestamp columns. -- Use foreign key constraints to maintain referential integrity. -- Consider soft deletes using a `deleted_at` timestamp. -- Add appropriate indexes for frequently queried columns. - -**Common Columns:** -- `id: BIGINT AUTO_INCREMENT PRIMARY KEY` -- `created_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP` -- `updated_at: TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` -- `deleted_at: TIMESTAMP NULL` - -## Rule 3: Query Writing - -Title: Guidelines for Writing Effective and Readable SQL Queries -Description: Provides guidelines for formatting SQL queries for readability and optimizing them for performance. This includes advice on `SELECT` statements, using `EXISTS` versus `IN`, appropriate indexing, avoiding correlated subqueries where possible, using `EXPLAIN` to analyze query performance, considering query execution plans, and using batch operations for bulk data modifications. - -**Good example:** - -**Formatting:** -- Use uppercase for SQL keywords (e.g., `SELECT`, `FROM`, `WHERE`). -- Place one clause per line for better readability. -- Indent subqueries and Common Table Expressions (CTEs). -- Align column lists vertically in `SELECT` statements. -- Use meaningful and concise table aliases. - -**Performance:** -- Avoid `SELECT *`; specify only the columns needed. -- Use `EXISTS` instead of `IN` with subqueries for potentially better performance, especially on large datasets. -- Ensure appropriate indexes are available for columns used in `WHERE` clauses, `JOIN` conditions, and `ORDER BY` clauses. -- Avoid correlated subqueries when an equivalent join or non-correlated subquery can be used. -- Use `EXPLAIN` (or similar command for your RDBMS) to analyze query performance and understand the execution plan. -- Consider the query execution plan to identify bottlenecks. -- Use batch operations for bulk updates or inserts to reduce overhead. - -**Example Query:** -```sql -SELECT - u.id, - u.first_name, - u.last_name, - o.order_date -FROM - users u -LEFT JOIN - orders o ON u.id = o.user_id -WHERE - u.deleted_at IS NULL - AND o.status = 'COMPLETED' -ORDER BY - o.order_date DESC; -``` - -## Rule 4: Indexing Strategy - -Title: Principles for Effective Database Indexing -Description: Covers principles for creating and managing database indexes. This includes indexing foreign key columns, columns frequently used in `WHERE` clauses or `JOIN` conditions, considering composite indexes for multi-column queries, and avoiding over-indexing which can negatively impact write performance. It also mentions monitoring index usage and removing unused indexes. - -**Good example:** - -**Principles:** -- Index foreign key columns. -- Index frequently queried columns (those appearing in `WHERE`, `JOIN`, `ORDER BY`, `GROUP BY` clauses). -- Consider composite indexes for queries that filter or sort on multiple columns. The order of columns in a composite index matters. -- Avoid over-indexing, as each index consumes storage and adds overhead to write operations (inserts, updates, deletes). -- Monitor index usage to identify underutilized or unused indexes. -- Remove unused indexes to reclaim space and reduce maintenance overhead. - -**Types (common examples):** -- B-tree (default for most relational databases, good for range queries and equality) -- Hash (good for exact equality checks, not for range queries) -- Full-text (for searching text content within columns) -- Spatial (for indexing geographical data) - -## Rule 5: Security Guidelines - -Title: Database Security Best Practices -Description: Highlights essential security measures for databases, including the use of prepared statements or parameterized queries to prevent SQL injection, implementation of proper access control mechanisms (granting least privilege), encryption of sensitive data both at rest and in transit, auditing access to sensitive data, conducting regular security reviews, establishing a robust backup and recovery strategy, and using appropriate user permissions. - -**Good example:** - -- Use prepared statements (or parameterized queries) to prevent SQL injection vulnerabilities. -- Implement proper access control: Grant users and applications only the permissions necessary to perform their tasks (principle of least privilege). -- Encrypt sensitive data: Protect data at rest (e.g., using TDE or column-level encryption) and in transit (e.g., using SSL/TLS). -- Audit sensitive data access: Log and monitor access to critical data to detect and investigate suspicious activity. -- Conduct regular security reviews and vulnerability assessments. -- Implement a comprehensive backup and recovery strategy. -- Use appropriate and distinct user permissions for different application roles or services. - -## Rule 6: Performance Optimization Tips - -Title: Tips for Optimizing Database Performance -Description: Provides a collection of tips for optimizing database performance. These include selecting appropriate data types for columns to save space and improve comparison speed, normalizing database design to reduce redundancy, indexing strategically, writing optimized queries, using connection pooling to manage database connections efficiently, implementing caching mechanisms where appropriate, performing regular database maintenance (like `VACUUM`, `ANALYZE`, or index rebuilding), and continuously monitoring query performance. - -**Good example:** - -- Use appropriate data types: Choose the smallest data type that can reliably store the required data. -- Normalize database design: Reduce data redundancy and improve data integrity, which can also benefit performance. -- Index strategically: Create indexes on columns used in search conditions and joins, but avoid over-indexing. -- Optimize queries: Rewrite inefficient queries, avoid `SELECT *`, and use `EXPLAIN` to analyze execution plans. -- Use connection pooling: Reduce the overhead of establishing database connections for each request. -- Implement caching where appropriate: Cache frequently accessed, relatively static data to reduce database load. -- Perform regular maintenance: Tasks like `VACUUM` (in PostgreSQL), `ANALYZE`, or rebuilding indexes can improve performance. -- Monitor query performance: Identify and address slow or resource-intensive queries. - -## Rule 7: Transaction Guidelines - -Title: Guidelines for Managing Database Transactions -Description: Offers guidelines for managing database transactions effectively. Key advice includes keeping transactions as short as possible to minimize locking and resource contention, using appropriate isolation levels to balance consistency and concurrency needs, handling potential deadlocks gracefully, implementing robust error handling within transactions, and considering the use of savepoints for more complex transactional logic. - -**Good example:** - -- Keep transactions as short as possible: Long-running transactions can hold locks for extended periods, impacting concurrency. -- Use appropriate isolation levels: Understand the trade-offs (e.g., `READ COMMITTED`, `REPEATABLE READ`, `SERIALIZABLE`) and choose the level that meets the application's consistency requirements without unduly harming performance. -- Handle deadlocks appropriately: Implement mechanisms to detect and retry transactions in case of deadlocks. -- Implement proper error handling: Ensure that transactions are rolled back in case of errors to maintain data consistency. -- Consider using savepoints for complex transactions that might need partial rollbacks. - -## Rule 8: Migration Best Practices - -Title: Best Practices for Database Schema Migrations -Description: Outlines best practices for managing database schema changes and data migrations. This includes version controlling all database changes, using dedicated migration tools (like Flyway or Liquibase), designing migrations to be reversible whenever possible, thoroughly testing migrations in non-production environments before applying them to production, including rollback scripts or procedures, and clearly documenting any breaking changes. - -**Good example:** - -- Version control all database changes: Store schema definitions and migration scripts in a version control system (e.g., Git). -- Use migration tools (e.g., Flyway, Liquibase): These tools help manage, track, and apply database changes systematically. -- Make migrations reversible when possible: Design changes so they can be undone if necessary. -- Test migrations thoroughly in non-production environments that mirror production as closely as possible. -- Include rollback scripts or procedures for migrations. -- Document breaking changes and communicate them to relevant teams. - -## Rule 9: Code Examples - -Title: Illustrative SQL Code Examples -Description: Provides practical examples of SQL code, including table creation and stored procedure definitions, to demonstrate the application of various guidelines such as naming conventions, data type selection, and indexing. - -**Good example:** - -**Table Creation:** -```sql -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT, - email VARCHAR(255) NOT NULL, - first_name VARCHAR(100), - last_name VARCHAR(100), - status ENUM('ACTIVE', 'INACTIVE', 'SUSPENDED') DEFAULT 'ACTIVE', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL, - PRIMARY KEY (pk_users_id), -- Example of pk_ prefix - UNIQUE INDEX uq_users_email (email), -- Example of uq_ prefix for unique index - INDEX idx_users_status (status) -- Example of idx_ prefix for general index -); -``` -*Note: The `pk_users_id` demonstrates the primary key prefix. `AUTO_INCREMENT` syntax might vary by RDBMS (e.g., `SERIAL` or `IDENTITY`). Enum usage also varies.* - -**Stored Procedure:** -```sql --- Syntax for DELIMITER and stored procedure creation can vary by RDBMS. --- This is a MySQL-like example. -DELIMITER // -CREATE PROCEDURE get_active_users(IN status_param VARCHAR(20)) -BEGIN - SELECT - id, - email, - first_name, - last_name - FROM - users - WHERE - status = status_param - AND deleted_at IS NULL; -END // -DELIMITER ; -``` - -## Rule 10: Testing Guidelines - -Title: Guidelines for Database Testing -Description: Covers guidelines for comprehensively testing database components and interactions. This includes testing with representative data volumes to uncover performance issues, including edge cases and boundary conditions in test data, testing performance under various load conditions, verifying data integrity constraints (like primary keys, foreign keys, unique constraints, check constraints), testing stored procedures and functions for correctness, validating migration scripts (both upgrade and rollback), and testing rollback procedures. - -**Good example:** - -- Test with representative data volumes: Small test datasets may not reveal performance issues that appear with larger amounts of data. -- Include edge cases and boundary conditions in test data. -- Test performance under load to simulate real-world usage patterns. -- Verify data integrity constraints: Attempt to insert/update data that violates constraints to ensure they are enforced. -- Test stored procedures, functions, and triggers for correct behavior and logic. -- Validate migration scripts: Test both the application of migrations and, if applicable, their rollback. -- Test rollback procedures to ensure they work as expected in case of deployment failures. - -## Rule 11: Monitoring Practices - -Title: Practices for Database Monitoring -Description: Details essential practices for monitoring database health and performance. This involves monitoring overall query performance, tracking slow queries that may indicate bottlenecks, monitoring disk usage to prevent storage issues, tracking connection pool usage to ensure it's adequately sized, monitoring cache hit rates to assess caching effectiveness, setting up alerts for critical issues (e.g., high error rates, low disk space, long-running queries), and conducting regular performance reviews. - -**Good example:** - -- Monitor query performance: Use database-specific tools or APM solutions to track execution times and resource consumption of queries. -- Track slow queries: Identify and analyze queries that exceed performance thresholds. -- Monitor disk usage: Keep an eye on storage capacity and I/O performance. -- Track connection pool usage: Ensure the pool is not exhausted and is efficiently managing connections. -- Monitor cache hit rates: For database caches or application-level caches interacting with the database. -- Set up alerts for critical issues (e.g., low disk space, high CPU usage, long-running transactions, replication lag). -- Conduct regular performance reviews to proactively identify and address potential issues. ---- diff --git a/.cursor/rules/templates/java-checklist-template.md b/.cursor/rules/templates/java-checklist-template.md index 39d14f1b..f9481134 100644 --- a/.cursor/rules/templates/java-checklist-template.md +++ b/.cursor/rules/templates/java-checklist-template.md @@ -59,25 +59,6 @@ Use the following process to improve the java development in some areas if requi | - | Code Refactoring | `Can you apply the solutions from @profiling-solutions-yyyymmdd.md in @/info to mitigate bottlenecks` | Make a refactoring with the notes from the analysis | | [164-java-profiling-compare](.cursor/rules/162-java-profiling-compare.mdc) | Analyze results | `Review if the problems was solved with last refactoring using the reports located in @/results with the cursor rule 154-java-profiling-compare.mdc` | Put in the context the folder with the results | -### Spring Boot rules - -| Cursor Rule | Description | Prompt | Notes | -|-------------|-------------|--------|-------| -| [301-frameworks-spring-boot-core](.cursor/rules/301-frameworks-spring-boot-core.mdc) | Spring Boot Core | `Review my Spring Boot application using the cursor rule @301-frameworks-spring-boot-core` | Add in the context the Spring Boot classes you want to review | -| [302-frameworks-spring-boot-rest](.cursor/rules/302-frameworks-spring-boot-rest.mdc) | REST API Design Principles | `Review my REST API design using the cursor rule @302-frameworks-spring-boot-rest` | Add in the context the REST controllers to review | -| [303-frameworks-spring-data-jdbc](.cursor/rules/303-frameworks-spring-data-jdbc.mdc) | Spring Data JDBC | `Improve my Spring Data JDBC implementation using the cursor rule @303-frameworks-spring-data-jdbc` | Add in the context the repository classes and entities | -| [304-frameworks-spring-boot-hikari](.cursor/rules/304-frameworks-spring-boot-hikari.mdc) | HikariCP Connection Pool Configuration | `Review my HikariCP configuration using the cursor rule @304-frameworks-spring-boot-hikari` | Add in the context your application properties files | -| [311-frameworks-spring-boot-slice-testing](.cursor/rules/311-frameworks-spring-boot-slice-testing.mdc) | Spring Boot Slice Testing | `Improve my slice tests using the cursor rule @311-frameworks-spring-boot-slice-testing` | Add in the context the test classes to review | -| [312-frameworks-spring-boot-integration-testing](.cursor/rules/312-frameworks-spring-boot-integration-testing.mdc) | Integration Testing Guidelines | `Review my integration tests using the cursor rule @312-frameworks-spring-boot-integration-testing` | Add in the context the integration test classes | -| [313-frameworks-spring-boot-local-testing](.cursor/rules/313-frameworks-spring-boot-local-testing.mdc) | Local Testing with Docker Compose | `Improve my local testing setup using the cursor rule @313-frameworks-spring-boot-local-testing` | Add in the context your docker-compose.yaml and test configuration | -| [321-frameworks-spring-boot-native-compilation](.cursor/rules/321-frameworks-spring-boot-native-compilation.mdc) | Native Compilation | `Optimize my Spring Boot app for native compilation using the cursor rule @321-frameworks-spring-boot-native-compilation` | Add in the context your pom.xml and application configuration | - -### SQL rules - -| Cursor Rule | Description | Prompt | Notes | -|-------------|-------------|--------|-------| -| [500-sql](.cursor/rules/500-sql.mdc) | SQL Development Guidelines | `Review my SQL code and database design using the cursor rule @500-sql` | Add in the context your SQL files, database schema, or migration scripts | - --- **Note:** This guide is self-contained and portable. Copy it into any Java project to get started with Cursor Rules for Java development. \ No newline at end of file diff --git a/README.md b/README.md index be059cab..3a877ea8 100644 --- a/README.md +++ b/README.md @@ -72,25 +72,6 @@ Using the Cursor rules is straightforward: simply `drag and drop` the cursor rul | - | Code Refactoring | `Can you apply the solutions from @profiling-solutions-yyyymmdd.md in @/info to mitigate bottlenecks` | Make a refactoring with the notes from the analysis | | [164-java-profiling-compare](.cursor/rules/164-java-profiling-compare.mdc) | Analyze results | `Review if the problems was solved with last refactoring using the reports located in @/results with the cursor rule 154-java-profiling-compare.mdc` | Put in the context the folder with the results | -### Spring Boot rules - -| Cursor Rule | Description | Prompt | Notes | -|-------------|-------------|--------|-------| -| [301-frameworks-spring-boot-core](.cursor/rules/301-frameworks-spring-boot-core.mdc) | Spring Boot Core | `Review my Spring Boot application using the cursor rule @301-frameworks-spring-boot-core` | Add in the context the Spring Boot classes you want to review | -| [302-frameworks-spring-boot-rest](.cursor/rules/302-frameworks-spring-boot-rest.mdc) | REST API Design Principles | `Review my REST API design using the cursor rule @302-frameworks-spring-boot-rest` | Add in the context the REST controllers to review | -| [303-frameworks-spring-data-jdbc](.cursor/rules/303-frameworks-spring-data-jdbc.mdc) | Spring Data JDBC | `Improve my Spring Data JDBC implementation using the cursor rule @303-frameworks-spring-data-jdbc` | Add in the context the repository classes and entities | -| [304-frameworks-spring-boot-hikari](.cursor/rules/304-frameworks-spring-boot-hikari.mdc) | HikariCP Connection Pool Configuration | `Review my HikariCP configuration using the cursor rule @304-frameworks-spring-boot-hikari` | Add in the context your application properties files | -| [311-frameworks-spring-boot-slice-testing](.cursor/rules/311-frameworks-spring-boot-slice-testing.mdc) | Spring Boot Slice Testing | `Improve my slice tests using the cursor rule @311-frameworks-spring-boot-slice-testing` | Add in the context the test classes to review | -| [312-frameworks-spring-boot-integration-testing](.cursor/rules/312-frameworks-spring-boot-integration-testing.mdc) | Integration Testing Guidelines | `Review my integration tests using the cursor rule @312-frameworks-spring-boot-integration-testing` | Add in the context the integration test classes | -| [313-frameworks-spring-boot-local-testing](.cursor/rules/313-frameworks-spring-boot-local-testing.mdc) | Local Testing with Docker Compose | `Improve my local testing setup using the cursor rule @313-frameworks-spring-boot-local-testing` | Add in the context your docker-compose.yaml and test configuration | -| [321-frameworks-spring-boot-native-compilation](.cursor/rules/321-frameworks-spring-boot-native-compilation.mdc) | Native Compilation | `Optimize my Spring Boot app for native compilation using the cursor rule @321-frameworks-spring-boot-native-compilation` | Add in the context your pom.xml and application configuration | - -### SQL rules - -| Cursor Rule | Description | Prompt | Notes | -|-------------|-------------|--------|-------| -| [500-sql](.cursor/rules/500-sql.mdc) | SQL Development Guidelines | `Review my SQL code and database design using the cursor rule @500-sql` | Add in the context your SQL files, database schema, or migration scripts | - ## Getting started If you are interested in getting the benefits from these cursor rules, you can manually download this repository and copy the './cursor' folder and paste it into your repository, or delegate this task to a specific command-line tool based on **Jbang**: