A versioned data migration records which data transformation was applied and rejects a changed definition for an already recorded version.
Java versioned data changes: checksums and a committed ledger
Java 8+. The downloadable Maven fixture supplies H2 2.3.232.
Make the replay rule explicit
This fixture migrates seed data, not arbitrary schema DDL. The schema is bootstrapped separately because DDL transaction behavior differs by database. The migration’s inserted row and its version/checksum record share one DML transaction.
A second launch with the same version and checksum is a no-op. A different checksum for an applied version is rejected before another data change. That protects the meaning of the recorded history; it does not decide how an operator should repair an already deployed mistake.
The checksum is derived from a declared UTF-8 migration string. A real migration tool also needs ordered discovery, locking, state validation and a recovery procedure. This program intentionally has one migration and one owner, so it does not claim to coordinate two simultaneous deployers.
Handle rollback without hiding the primary failure
The migration owner commits after both writes and rolls back after failure. If rollback itself fails, attach that failure to the original exception rather than replacing the cause of the failed change. A migration that sends an external message needs a separate publication rule because a JDBC rollback cannot retract it.
Working program
import java.sql.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class SeedMigrationLedger {
static String checksum(String definition)throws Exception{
byte[] digest=MessageDigest.getInstance("SHA-256").digest(definition.getBytes(StandardCharsets.UTF_8));StringBuilder value=new StringBuilder();for(byte part:digest)value.append(String.format("%02x",part&255));return value.toString();
}
static boolean apply(Connection connection,String definition)throws Exception{
String digest=checksum(definition);connection.setAutoCommit(false);
try{
try(PreparedStatement query=connection.prepareStatement("SELECT checksum FROM versions WHERE version=1");ResultSet rows=query.executeQuery()){
if(rows.next()){if(!digest.equals(rows.getString(1)))throw new IllegalStateException("Migration definition changed");connection.commit();return false;}
}
try(PreparedStatement seed=connection.prepareStatement("INSERT INTO labels VALUES(?,?)")){seed.setInt(1,31);seed.setString(2,"Ready");seed.executeUpdate();}
try(PreparedStatement record=connection.prepareStatement("INSERT INTO versions VALUES(1,?)")){record.setString(1,digest);record.executeUpdate();}connection.commit();return true;
}catch(Exception failure){try{connection.rollback();}catch(SQLException rollback){failure.addSuppressed(rollback);}throw failure;}
}
public static void main(String[] args)throws Exception{
try(Connection database=DriverManager.getConnection("jdbc:h2:mem:migration_"+java.util.UUID.randomUUID())){
try(Statement schema=database.createStatement()){schema.execute("CREATE TABLE versions(version INT PRIMARY KEY, checksum VARCHAR(64))");schema.execute("CREATE TABLE labels(id INT PRIMARY KEY, label VARCHAR(40))");}
System.out.println(apply(database,"seed-label-31-v1"));System.out.println(apply(database,"seed-label-31-v1"));
try{apply(database,"seed-label-31-edited");}catch(IllegalStateException changed){System.out.println(changed.getMessage());}
}
}
}Output
true
false
Migration definition changedCosts and boundaries
Hashing costs O(m) for a definition of m bytes; the database operations add transaction and I/O costs. The single-version fixture is not a replacement for a migration system’s ordered history or deployment locking. Schema bootstrap is outside the shown data-migration transaction.
Common Mistakes
- Do not edit an applied migration and pretend its history is unchanged.
- DDL and DML transaction behavior require engine-specific verification.
- A version table alone does not coordinate concurrent deployers.
Read next
Transaction ownership, Engine-specific behavior.
Continue with checked settings and schema rollout
Continue with Spring database rollout: add a nullable column before changing every writer.
Checked file and migration continuation
Continue with Flyway SQL migrations: record ordered schema changes against a real database.
Continue with checked Boot startup
Continue with Spring Boot Flyway startup: apply versioned SQL before querying a receipt.
Release boundary continuation
Continue with Spring Boot project: define the receipt service release boundary.
Checked outbound HTTP continuation
Continue with Spring HTTP timeout after POST: the write may already exist.
