Spring Boot Loading Initial Data: data.sql, schema.sql, and Flyway Guide
Learn how to load initial data in Spring Boot using data.sql, schema.sql, and Flyway.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17+ and Maven 3.8+
- ✓Spring Boot 3.2+ project with spring-boot-starter-data-jpa
- ✓MySQL 8.0 or PostgreSQL 15 running locally or via Docker
- ✓Basic understanding of JPA entities and SQL
• Use schema.sql for DDL and data.sql for DML when JPA auto-DDL is disabled • Flyway is the production-grade choice for versioned migrations with rollback support • Naming conflicts and execution order between JPA and script-based approaches cause silent failures • Always set spring.jpa.defer-datasource-initialization=true when using JPA with data.sql • For large datasets, use Flyway callbacks or batch inserts to avoid memory issues
Think of setting up a new restaurant kitchen. schema.sql is like installing the stoves and counters (the structure), data.sql is like stocking the fridge with initial ingredients (the data). Flyway is like a recipe book with versioned steps — you can't skip making the dough before baking the bread, and you can always revert to a previous recipe if something burns. Without Flyway, you're just throwing ingredients at the kitchen hoping it works.
Every Spring Boot application eventually needs to start with some data — reference tables, admin users, or test fixtures. The framework offers three main approaches: raw SQL scripts (schema.sql/data.sql), JPA auto-DDL, and Flyway migrations. While the official docs make each sound straightforward, the real world is full of landmines: execution order conflicts, silent failures when JPA drops tables, and migration hell when you outgrow scripts. I've seen a SaaS billing platform go down because a data.sql ran before Flyway, wiping out customer subscription data. This article walks through each approach with production-hardened patterns, using a payment-processing domain. You'll learn when to use scripts for prototypes, when to switch to Flyway, and how to avoid the gotchas that cost real money. By the end, you'll have a decision framework for any Spring Boot version from 2.x to 3.x.
Setting Up the Project for Data Initialization
Start with a Spring Boot 3.2 project using Spring Initializr. Add dependencies: spring-boot-starter-data-jpa, spring-boot-starter-web, and your database driver (mysql-connector-j or postgresql). For Flyway, add flyway-core and flyway-mysql or flyway-postgresql. Configure application.yml with your datasource. Disable JPA auto-DDL for script-based approaches: spring.jpa.hibernate.ddl-auto=none. Enable SQL logging to debug execution: spring.jpa.show-sql=true and logging.level.org.hibernate.SQL=DEBUG. For MySQL, add spring.datasource.url=jdbc:mysql://localhost:3306/payments?useSSL=false&allowPublicKeyRetrieval=true. For PostgreSQL: jdbc:postgresql://localhost:5432/payments. Set the database user and password. This setup is crucial — many developers skip logging and then wonder why their scripts silently fail.
What the Official Docs Won't Tell You
The Spring Boot documentation says data.sql and schema.sql work seamlessly with JPA. What they don't mention: if you have spring.jpa.hibernate.ddl-auto set to create or update, Hibernate will create tables first, then schema.sql will fail because the tables already exist. The fix is to set ddl-auto=none or use spring.jpa.defer-datasource-initialization=true (available since Spring Boot 2.5). Another hidden issue: data.sql runs in the same transaction as your application's initialization, so if a JPA entity listener fails, the entire data.sql may roll back — but only if your database supports DDL transactions (MySQL does not for DDL). Also, the script execution order is: schema.sql, then data.sql, then Flyway (unless deferred). This means Flyway migrations run after your seed data, which can cause version conflicts. In a payment system, I saw Flyway fail because a migration added a NOT NULL column that data.sql didn't populate. The official docs also skip the fact that script-based initialization is only intended for small datasets — if you have 100,000 rows, you'll hit memory limits because Spring loads the entire script into memory.
Using schema.sql and data.sql for Development
For local development, schema.sql and data.sql are convenient. Create schema.sql with CREATE TABLE statements. Use IF NOT EXISTS to avoid errors on restarts. For a payment system, define tables like payment_transactions, merchant_accounts, and refunds. In data.sql, insert reference data: payment statuses, currency codes, and test merchants. Use INSERT IGNORE or MERGE to make scripts idempotent. Example: INSERT IGNORE INTO payment_status (id, name) VALUES (1, 'PENDING'), (2, 'COMPLETED'). For MySQL, use ON DUPLICATE KEY UPDATE. For PostgreSQL, use ON CONFLICT DO NOTHING. Never use DELETE FROM without a WHERE clause in data.sql — that's how production data gets wiped. In development, you can use TRUNCATE in a separate reset script, but never in the main data.sql. Set spring.sql.init.continue-on-error=true to allow scripts to fail without crashing the app (useful for development). But in production, set it to false and rely on Flyway.
Flyway: Production-Grade Database Migrations
Flyway is the industry standard for managing database schema and data changes in production. It uses versioned SQL files: V1__init_schema.sql, V2__add_index.sql, etc. Each migration runs exactly once, tracked in the flyway_schema_history table. For a payment system, start with V1__create_tables.sql containing your DDL. Then V2__seed_reference_data.sql for static data like payment statuses. Use repeatable migrations (R__views.sql) for views and functions that change often. Configure Flyway in application.yml: spring.flyway.enabled=true, spring.flyway.locations=classpath:db/migration. Flyway checksums each migration — if you modify a file after it ran, Flyway throws an error. This is a feature, not a bug: it prevents silent schema drift. For production, use Flyway's undo migrations (V1__U__drop_column.sql) for rollbacks, but only if you have the Flyway Teams edition. For community edition, roll forward with a new migration. Always test migrations against a copy of production data before deploying.
Hybrid Approach: Combining Flyway with JPA Entities
In real projects, you'll use JPA entities for ORM but Flyway for schema management. This hybrid approach gives you the best of both: type-safe queries with JPA and version-controlled schema changes. Configure JPA with ddl-auto=validate to ensure entities match the database schema. Flyway runs before Hibernate validation, so any mismatch is caught at startup. For entity classes, use @Table and @Column annotations to map to the schema created by Flyway. Example: @Entity @Table(name = "payment_transaction") and @Column(name = "merchant_id"). Use @Column(nullable = false) to enforce NOT NULL constraints. JPA's schema validation can catch issues like missing columns or wrong data types. This is especially important when multiple developers work on the same database. I've seen a team spend hours debugging a 'column not found' error because someone added a column in Flyway but forgot to update the entity. With ddl-auto=validate, the error is immediate and clear.
Advanced Flyway Patterns: Callbacks and Undo
Flyway callbacks allow you to execute custom logic before or after migrations. Common use cases: logging migration start/end, sending notifications, or cleaning up temporary data. Create a Java class implementing Callback interface from Flyway. Use @Component to register it. For example, log migration duration to Prometheus metrics. Another pattern: use Flyway's undo migrations (V1__U__description.sql) for rollbacks. Note: undo is only available in Flyway Teams edition. For community edition, create a new migration to reverse changes. Example: V3__add_merchant_email_index.sql, then V4__drop_merchant_email_index.sql if needed. Always test rollbacks against a production-like database. In a payment system, a rollback of a column addition must also handle data that was populated in that column. Use Flyway's validateOnMigrate to check for checksum changes before running migrations. This prevents accidental edits. Also, use flyway.clean-disabled=true in production to prevent accidental database wipe.
Testing Initial Data Loading
Testing data initialization is critical but often overlooked. Use @DataJpaTest with Testcontainers for integration tests. Start a MySQL or PostgreSQL container, then verify that Flyway migrations and seed data are applied correctly. Use @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) to use the Testcontainers database. Write tests that query the database after initialization to confirm expected rows exist. For example, assert that payment_status has 4 rows. Test both happy path and failure scenarios: what happens if a migration fails? Use @Test(expected = ...) or assertThrows for negative tests. Also test idempotency: restart the application and verify that data isn't duplicated. For Flyway, test that running migrations on an already-migrated database doesn't fail. Use @FlywayTest annotation from the flyway-test-extensions library to clean and reapply migrations between tests. This ensures test isolation. In a payment system, we once had a test that passed because H2 didn't enforce constraints, but failed on MySQL. Always test against your production database type.
Performance Considerations and Batch Loading
Loading large datasets with data.sql or Flyway can cause performance issues. Spring Boot's default script execution loads the entire SQL file into memory. For files over 10MB, this can cause OOM errors. Solution: use Flyway with batch inserts. In Flyway, you can use executeBatch in Java-based migrations. For SQL-based migrations, split large inserts into chunks of 1000 rows. Use Flyway's placeholder replacement to inject batch sizes. Another approach: use @PostConstruct in a Spring bean to load data programmatically with JdbcTemplate batchUpdate. For example, read a CSV file and insert in batches of 500. This also allows error handling per batch. For truly large datasets (millions of rows), consider using Spring Batch with chunk-oriented processing. In a payment system, we loaded 2 million transaction records using Spring Batch with a 1000-item chunk size and a skip policy for duplicates. The entire process took 12 minutes instead of 45 with raw SQL scripts. Always measure and tune batch sizes based on your database's max_allowed_packet (MySQL) or similar settings.
The Midnight Billing Meltdown: When data.sql Overwrote Customer Subscriptions
- Never use data.sql in production — use Flyway or Liquibase for all data changes
- Always verify execution order in your target database, not just H2
- Add a validation step in CI that fails if data.sql contains DELETE or TRUNCATE
- Use Flyway repeatable migrations (V__) for reference data, versioned migrations for schema
Check application logs for 'Initializing database schema' vs 'Flyway migration' orderSet spring.jpa.hibernate.ddl-auto=validate| File | Command / Code | Purpose |
|---|---|---|
| application.yml | spring: | Setting Up the Project for Data Initialization |
| data.sql | INSERT IGNORE INTO payment_status (id, name) VALUES | Using schema.sql and data.sql for Development |
| V2__seed_reference_data.sql | INSERT INTO payment_status (id, name) VALUES | Flyway |
| MigrationCallback.java | @Component | Advanced Flyway Patterns |
| DataInitializationTest.java | @SpringBootTest | Testing Initial Data Loading |
| BatchDataLoader.java | @Component | Performance Considerations and Batch Loading |
Key takeaways
Interview Questions on This Topic
Explain the execution order of schema.sql, data.sql, and Flyway in Spring Boot. How do you control it?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't