FlywayConfiguration.java

package com.stocks.stockease.config;

import javax.sql.DataSource;

import org.flywaydb.core.Flyway;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * Database migration configuration using Flyway.
 * 
 * Problem solved:
 * - Spring Boot 3.5.x with Spring Data JPA causes circular dependency:
 *   EntityManagerFactory wants to validate schema, but migrations haven't run yet
 * - Solution: Manually execute migrations via initMethod before JPA initialization
 * 
 * Migration discovery:
 * - Location: classpath:db/migration/
 * - Naming: V{VERSION}__{DESCRIPTION}.sql (e.g., V1__init_schema.sql)
 * - Execution: Sorted by version number, executed once per database
 * 
 * Features configured:
 * - baselineOnMigrate: Creates baseline if flyway_schema_history table doesn't exist
 * - cleanDisabled: Prevents accidental database wipe (safety for production)
 * - outOfOrder: Allows applying migrations older than current baseline (recovery scenarios)
 * - connectRetries: Retry failed connections (handles slow database startup)
 * 
 * Initialization sequence:
 * 1. Spring creates FlywayConfiguration bean
 * 2. flyway() method invoked (creates Flyway instance)
 * 3. initMethod="migrate" runs immediately (before @RestController beans)
 * 4. Migrations execute in order (V1__, V2__, etc.)
 * 5. EntityManagerFactory initializes with migrated schema
 * 6. Application ready for requests
 * 
 * @author Team StockEase
 * @version 1.0
 * @since 2025-01-01
 */
@Configuration
@ConditionalOnClass(Flyway.class)
public class FlywayConfiguration {

    /**
     * Property to enable/disable Flyway migrations.
     * Default: true (enabled)
     * Override: spring.flyway.enabled=false (disable in test profile)
     */
    @Value("${spring.flyway.enabled:true}")
    private boolean flywayEnabled;

    /**
     * Creates and executes Flyway migrations immediately during startup.
     * 
     * Why @Primary and initMethod="migrate":
     * - Prevents Spring Data JPA from initializing EntityManagerFactory before migrations run
     * - Executes migrations in constructor (implicit dependency)
     * - Alternative approaches (like @DependsOn) don't guarantee migration order in Spring 3.5.x
     * 
     * Configuration details:
     * - locations: Source migrations from classpath:db/migration/ (V*__*.sql files)
     * - baselineOnMigrate: Create baseline if first migration run (idempotent)
     * - cleanDisabled: Prevent accidental truncate/drop (safety net for production)
     * - outOfOrder: Allow non-sequential migrations (handle backfill scenarios)
     * - connectRetries: Retry 20 times with 2-second intervals (handles serverless DB cold-start)
     * 
     * Flyway profiles:
     * - dev: In-memory H2 database (migrations ephemeral)
     * - test: TestConfig creates separate test database
     * - prod: PostgreSQL (migrations persisted)
     * 
     * @param dataSource Spring-managed DataSource (auto-wired from application.properties)
     * @return Configured Flyway instance (migrations already executed by initMethod)
     * @throws Exception if DataSource connection fails after retries
     */
    @Primary
    @Bean(initMethod = "migrate")
    public Flyway flyway(DataSource dataSource) throws Exception {
        
        // Honor explicit disable flag (useful for test profiles)
        if (!flywayEnabled) {
            return Flyway.configure().dataSource(dataSource).load();
        }

        // Configure Flyway with resilience and safety settings
        Flyway flyway = Flyway.configure()
                .dataSource(dataSource)
                .locations("classpath:db/migration") // Location of V*__*.sql files
                .baselineOnMigrate(true) // Create baseline if schema_history doesn't exist
                .cleanDisabled(true) // Prevent accidental database wipe
                .outOfOrder(true) // Allow non-sequential migrations
                .connectRetries(20) // Retry 20 times (handles slow DB startup)
                .connectRetriesInterval(2) // 2 seconds between retries
                .load();

        // Execute migrations immediately (before other beans initialize)
        // This ensures schema is ready before JPA EntityManagerFactory needs it
        flyway.migrate();
        return flyway;
    }
}