CREATE TABLE country (
code CHAR(2) PRIMARY KEY,
name VARCHAR(128) NOT NULL,
currency CHAR(3) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_country_currency ON country (currency);
package com.example.migration;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
public class ReferenceDataCallback implements Callback {
private static final String UPSERT =
"INSERT INTO country (code, name, currency) VALUES (?, ?, ?) " +
"ON CONFLICT (code) DO UPDATE SET " +
"name = EXCLUDED.name, currency = EXCLUDED.currency, updated_at = now()";
private static final List<String[]> SEED = List.of(
new String[]{"US", "United States", "USD"},
new String[]{"GB", "United Kingdom", "GBP"},
new String[]{"DE", "Germany", "EUR"},
new String[]{"JP", "Japan", "JPY"}
);
@Override
public boolean supports(Event event, Context context) {
return event == Event.AFTER_MIGRATE;
}
@Override
public boolean canHandleInTransaction(Event event, Context context) {
return true;
}
@Override
public void handle(Event event, Context context) {
Connection connection = context.getConnection();
try (PreparedStatement ps = connection.prepareStatement(UPSERT)) {
for (String[] row : SEED) {
ps.setString(1, row[0]);
ps.setString(2, row[1]);
ps.setString(3, row[2]);
ps.addBatch();
}
ps.executeBatch();
} catch (SQLException e) {
throw new IllegalStateException("Failed to seed reference data", e);
}
}
@Override
public String getCallbackName() {
return "ReferenceDataCallback";
}
}
package com.example.migration;
import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FlywayConfig {
@Bean
public FlywayConfigurationCustomizer referenceDataCustomizer() {
return configuration -> configuration.callbacks(new ReferenceDataCallback());
}
}
This snippet shows how Flyway's Callback extension point is used to seed slowly-changing reference data (country codes, currency lookups, and similar) right after a versioned migration completes, without embedding large INSERT scripts inside the migration files themselves. Reference data tends to evolve independently of schema; keeping it in a callback lets it be re-applied idempotently on every deploy while the schema migration stays a one-time, immutable event.
The V3__create_reference_tables.sql tab is a normal Flyway versioned migration. It only defines structure — the country table with a natural primary key on the ISO code — and deliberately contains no seed rows. Because versioned migrations are checksummed and never re-run once applied, mixing volatile data into them would force a new V4, V5, and so on every time a row changes. Separating the two concerns keeps the version history clean.
The ReferenceDataCallback tab implements Flyway's Callback interface. supports narrows execution to the AFTER_MIGRATE event so the seeder runs once the schema is guaranteed to be in place, and canHandleInTransaction returns true so the seed shares Flyway's transaction and rolls back cleanly on failure. The real work in handle uses the Context's live Connection — reusing Flyway's own connection avoids opening a second one and keeps everything atomic. Each row is written with an INSERT ... ON CONFLICT (code) DO UPDATE, which makes the seed idempotent: running it repeatedly converges the table to the desired state instead of throwing duplicate-key errors or drifting.
The FlywayConfig tab registers the callback with Spring Boot. FlywayConfigurationCustomizer is the idiomatic hook — it lets the callback participate in the auto-configured Flyway bean rather than constructing Flyway manually, so Spring still manages the DataSource and migration locations. The trade-off of this pattern is that callback code runs on every startup, so the seed logic must stay fast and strictly idempotent; heavy or non-deterministic work does not belong here. It is the right tool when reference data must track application code closely and be present in every environment, including fresh test databases, without a manual data-loading step.
Related snips
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]
sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first
SQL injection prevention with unsafe and safe query patterns
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
Share this code
Here's the card — post it anywhere.