What a Rails upgrade actually breaks
An upgrade is four different risks wearing the same version number. They fail at different times, in different places, with different blast radii, and treating them as one thing is why upgrades feel unpredictable.
The four kinds of breakage
Load-time breakage. A constant moved, a method was removed, an initializer calls something that no longer exists, and the application refuses to boot. This is the friendliest kind: it's loud, immediate and impossible to deploy by accident.
Test-time breakage. The application boots, but behavior changed. A default flipped, a callback order shifted, a serializer writes a different string. Your suite catches it if the behavior is covered.
Runtime breakage. The application boots, the tests pass, and then a real request takes a path no test takes. This is the expensive kind, because users discover it.
Data breakage. The new version writes something the old version can't read: a session cookie, a cache entry, an enqueued job payload. It appears only while both versions run at once, which is what a rolling deploy guarantees.
flowchart TB
U["Rails upgrade"] --> L["Load-time: won't boot"]
U --> T["Test-time: suite goes red"]
U --> R["Runtime: users find it"]
U --> D["Data: old and new disagree"]
L --> C1["Cheapest. Found in seconds"]
T --> C2["Cheap, if covered"]
R --> C3["Expensive. Needs a rollback"]
D --> C4["Expensive and sticky. Survives the rollback"]
Why the last one is different
The first three are fixed by fixing the code. Roll back and the problem goes with you.
Data breakage doesn't roll back. If the new version wrote a cache entry in a format the old version can't parse, rolling back leaves that entry sitting there, and the old code now crashes on data it didn't write. The rollback made things worse.
Every mitigation later in this course exists because of that asymmetry. Dual-booting, flipping defaults one at a time and the two-version window are all ways of never letting the fourth category happen, because it's the only one you can't undo by redeploying.
What this application looks like today
Rails 8.1.3.1 on Ruby 4.0.1, 31 gems in the Gemfile, config.load_defaults 8.1 in config/application.rb, and no config/initializers/new_framework_defaults file at all, which means every default for this version is already on. Nothing is pending.
That's the good position to be in, and it isn't the normal one. Many applications carry a defaults file with half its lines still commented out: a list of upgrades finished on paper and not in practice.
flowchart LR
A["Bundle updated"] --> B["Boots"]
B --> C["Suite green"]
C --> D["Defaults flipped"]
D --> E["Actually upgraded"]
A -.->|"where most<br/>applications stop"| C
The order that follows from this
Boot first, then tests, then defaults, then production, and never two of them in the same deploy. Each step tells you something the next step needs, and merging two of them means a failure no longer points at a cause.
Keep going with lesson 2
Reading the release notes as a work plan
Turns the upgrade guide into greps for removed names and tests for changed behavior, reading every skipped version.
A free account also gives you the quizzes, your progress and notes, and the discussion.