Skip to main content

Three Production Postmortems: Flyway, JAR Deploy, and Duplicate Beans

A comment in an applied Flyway migration breaks the checksum. A non-atomic cp during deploy causes an intermittent NoClassDefFoundError. Two beans with the same simple name crash the boot. Three real incidents, one root cause: violated immutability.

What do you think about this article?

Three Production Postmortems: Flyway, JAR Deploy, and Duplicate Beans
Platform Engineeringfedericocalo.dev

At a Glance

A comment in an applied Flyway migration breaks the checksum. A non-atomic cp during deploy causes an intermittent NoClassDefFoundError. Two beans with the same simple name crash the boot. Three real incidents, one root cause: violated immutability.

Direct answer

Applied Flyway migrations are immutable by contract, not by suggestion: changing even a single comment in a file already applied in production makes the boot fail with a checksum mismatch. I broke it myself once, with a cosmetic edit. Across 189 production migrations, that was the most expensive lesson: a text editor can crash a Spring Boot backend faster than an application bug can.

Database migration discipline is the most boring, most underrated part of backend engineering, until it crashes production. On this portfolio, 189 Flyway migrations (up to V199) govern the PostgreSQL schema of a backend with over 3,100 Java classes. Across three distinct production incidents, each traceable to a violation of the same implicit rule, "never touch something the system already considers written and verified", I learned how thin the margin is between boring discipline and a production boot crash.

Postmortem 1: a comment broke the checksum

Flyway computes a checksum of the full text content of every already applied migration file, and compares it on every startup against the value stored in the flyway_schema_history table. If they do not match, Flyway refuses to start the application. On 2026-06-20 I edited a single SQL comment in an already applied migration (V152), thinking a comment could not have functional effects, to clarify an intent that had become unclear months later. The backend did not restart: FlywayException: Migration checksum mismatch. No column, no table, no logic had changed: only text after a -- double-dash. Flyway does not distinguish between a comment and a statement, it treats the file as a byte blob with a checksum, period.

The operational rule I now apply, with no exceptions: an applied migration is frozen forever, including every comment. If an intent needs clarifying, write a new migration that documents the why in the commit message or in a comment on the new migration, never by editing the old one.

Postmortem 2: the JAR deploy race

Manual backend deploy follows the pattern git pull + mvn package + systemctl restart. Before the fix, the deploy script copied the new JAR over the running one (cp) while the previous JVM was still running and had the file memory-mapped. The observed production result: NoClassDefFoundError on core Spring classes, because the classloader was reading a file partially overwritten mid-way through a non-atomic cp, while the old process was still alive and serving traffic. The symptom is particularly insidious because it is neither a compile error nor an application bug, it is a binary file corrupted at runtime that looks like a missing-dependency problem.

The fix has three parts, and all three are necessary, none alone is enough: stop the service before copying (not after), copy the new JAR with an atomic mv instead of an in-place cp, and reset systemd's failed state if the previous restart went wrong, before restarting again. Skipping any one of the three reproduces the race intermittently, which is the most expensive kind of bug to diagnose because it does not recur on every deploy.

Postmortem 3: two beans, same simple name, boot crash

Two @Service classes in different Java packages but with the same simple name (e.g. ExportService in two separate business verticals) blow up the Spring boot with ConflictingBeanDefinitionException, because Spring, without an explicit name, derives the bean name from the simple class name, not the fully qualified name. The genuinely uncomfortable part: unit tests do not catch this, because no single test loads the full application context with both verticals together, only a full-context integration test reveals it, and in practice that full-context test runs rarely during routine development, often only before deploy or never.

With over 25 separate business verticals added over time, the risk of a simple-name collision grows combinatorially, not linearly, with the number of verticals. The rule I adopted: every new @Service in a specific business vertical gets an explicit, prefixed name (@Service("phExportService")), never left to the default, and at least one full-context integration test runs before every deploy.

Three production postmortems and the derived anti-repetition rule
Incident Observed symptom Anti-repetition rule
Comment edit on applied migration (V152) Boot crash, FlywayException checksum mismatch Applied migrations immutable, always, even for a comment
In-place JAR deploy race Intermittent NoClassDefFoundError Stop-before-copy + atomic mv + reset-failed
Duplicate beans, same simple name Boot-time ConflictingBeanDefinitionException Explicit vertical-prefixed bean name + pre-deploy full-context test
Three real production incidents on the Spring Boot 3.4 backend, 189 Flyway migrations (max V199), manual VPS Hetzner deploy.

The common pattern: immutability as a boundary, not a style

The three postmortems look independent, but they share the same root cause: something the system treats as already written, verified, immutable, gets modified or overwritten at a moment the system does not expect it. An applied migration, a running binary, a simple name Spring assumes is unique. Immutability is not an elegant coding style to follow when convenient, it is a system boundary that, when violated, produces a deterministic crash or an intermittent race, never a gentle warning.

When this discipline is overkill

On a single-person side project with one environment and infrequent manual restarts, the risk of a JAR deploy race is low and the cost of a sophisticated stop-before-copy deploy script is not justified. The real breaking point is production with continuous traffic: there, even a few-second race window becomes an error window visible to users, and the discipline stops being bureaucracy and becomes necessary protection.


Frequently asked questions

Why does editing a comment in an already applied Flyway migration break the boot?

Flyway computes a checksum over the full text content of every already applied migration and compares it on every startup against the value stored in the flyway_schema_history table. It does not distinguish between a comment and a SQL statement: any text change, even cosmetic, changes the checksum and makes startup fail with a mismatch.

What causes a NoClassDefFoundError during a Spring Boot JAR deploy?

If the new JAR is copied in-place over the running one while the previous JVM is still running and has the file memory-mapped, the classloader can read a file partially overwritten mid-way through a non-atomic copy. The symptom looks like a missing dependency, but the real cause is a binary file corrupted at runtime during deploy.

How do you avoid the race condition in a Spring Boot JAR deploy?

By stopping the service before copying the new JAR (not after), using an atomic mv instead of an in-place cp, and resetting systemd's failed state if a previous restart went wrong. All three steps are necessary together; skipping any one reproduces the race intermittently.

Why do two Spring beans with the same simple name crash the boot?

Without an explicit name, Spring derives an @Service bean's name from the simple class name, not the fully qualified name. Two classes in different packages but with the same simple name trigger a ConflictingBeanDefinitionException at boot, a problem isolated unit tests do not catch because no single test loads both verticals into the same application context.

What do the three production postmortems described here have in common?

They share the same root cause: something the system treats as already written, verified, or immutable (an applied migration, a running binary, a simple name assumed unique) gets modified or overwritten at a moment the system does not expect it, producing a deterministic crash or an intermittent race.

Sources and further reading

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects