How would you upgrade a Ruby payroll service from 3.1 to 4.0, and which headline changes from 3.2 through 4.0 would you expect to break it?
answer
- deprecations on first, on 3.1
- 3.2 removed File.exists? and Fixnum
- 3.4 made bigdecimal a bundled gem
- 3.4 changed Hash#inspect and messages
- 4.0 bundled logger, removed pipe open
basics
~20 sFix deprecations on 3.1 first, then move through the versions with CI on each: expect 3.2's removed methods, 3.4's bundled bigdecimal and csv plus new Hash#inspect and error-message formats, and 4.0's bundled logger and removed APIs.
solid answer
~40 sI would start on 3.1: run the suite with `-W:deprecated`, fix what it reports, and upgrade gems to releases that support 4.0. Then I would move in steps, at least landing on 3.4 before 4.0, because each release warns about libraries leaving the default gems one version early. The breaks I expect: **3.2** removed `File.exists?`, `Dir.exists?`, `Fixnum`/`Bignum`, `Kernel#=~` and the taint methods; **3.4** made `bigdecimal`, `csv` and `base64` bundled gems (a payroll app almost certainly uses BigDecimal), changed `Hash#inspect` to `{amount: 1}` and error messages to `'Object#foo'`, which breaks tests that compare strings; **4.0** made `logger`, `ostruct` and `benchmark` bundled, removed `Kernel#open("|cmd")`, `Ractor.yield`, CGI outside `cgi/escape`, and Net::HTTP's automatic form Content-Type. Native gems must be reinstalled for each new Ruby.
code
ruby · 8 lines# Ruby 3.1 code that breaks on the way to 4.0
File.exists?("payroll.csv") # NoMethodError since 3.2; use File.exist?
amount.is_a?(Fixnum) # NameError since 3.2; use Integer
require "bigdecimal" # LoadError under Bundler since 3.4 unless in Gemfile
require "logger" # LoadError under Bundler since 4.0 unless in Gemfile
expect(totals.inspect).to eq("{:gross=>100}") # fails on 3.4+: "{gross: 100}"go deeper
Recall that methods such as File.exists? were removed in 3.2 and that some libraries now need Gemfile entries.
Explain the steps: deprecations on the old version, gem updates, CI on each target, reinstalling native gems, and which releases moved which libraries.
Run the upgrade as a staged rollout: boot every process type, watch the real payroll runs, and know the 3.4 format changes and the 4.0 Net::HTTP change that tests may not cover.
Use the upgrade to fix the process: add the next Ruby to CI early and fail on deprecations, so the next jump is one release, not four.
## The starting point A payroll service on Ruby 3.1 is several yearly releases behind 4.0. It already survived the big 3.0 change, keyword arguments separated from positional hashes, so what remains is a series of smaller removals and format changes spread over four releases. Payroll makes some of them sharper than usual: money is usually `BigDecimal`, exports are often CSV, audit trails go through `Logger`, and payouts call bank or tax APIs over HTTP. ## The upgrade plan 1. **Surface deprecations on 3.1.** Run the full suite with `RUBYOPT="-W:deprecated"` or `Warning[:deprecated] = true` in the test helper, and fix everything. Most 3.2 to 4.0 removals were deprecated first, and this is the cheapest time to see them. 2. **Update dependencies first.** Move gems to versions whose `required_ruby_version` includes 4.0 while still on 3.1, one reviewable change at a time. 3. **Step through the versions in CI.** Add each target version to the CI matrix. Landing on 3.3 or 3.4 before 4.0 matters because those releases warn about libraries that the next release moves out of the default gems. 4. **Rebuild native extensions.** Gems are installed per Ruby API version (a separate `gems/4.0.0` directory), so after switching Ruby you reinstall the bundle and every C extension compiles against the new Ruby. 5. **Boot every process type.** Web, background workers, console, cron and rake tasks each `require` different things; a missing bundled gem fails only in the process that loads it. 6. **Roll out gradually** and keep the previous image ready, watching error rates on the payroll runs themselves, not just health checks. ## Changes that are likely to break it - **3.2 removals:** `File.exists?` and `Dir.exists?` (use `exist?`), the `Fixnum` and `Bignum` constants (use `Integer`), `Kernel#=~` on arbitrary objects, and `taint`/`untaint`/`trust`. Delegating keywords through `*args` now always needs `ruby2_keywords`. - **3.3:** `Kernel#lambda` with a non-lambda, non-literal block raises `ArgumentError`; subprocess creation through `Kernel#open("|cmd")` is deprecated. - **3.4 library moves:** `bigdecimal`, `csv`, `base64`, `observer`, `drb`, `mutex_m` and others became bundled gems, so under Bundler they need Gemfile entries. For payroll, `require "bigdecimal"` is the one that stops the service. - **3.4 output formats:** `Hash#inspect` prints `{amount: 1}` instead of `{:amount=>1}`, and `{"k" => 1}` with spaces; error messages and backtraces use `'Object#foo'` instead of a backtick and include the class name. Tests and log parsers that compare these strings break. - **3.4 chilled strings:** mutating a string literal in a file without the `frozen_string_literal` comment warns when deprecation warnings are on; it still works. - **4.0 library moves:** `logger`, `ostruct`, `benchmark`, `pstore`, `irb`, `reline` and `rdoc` became bundled gems. CGI was removed except `cgi/escape`. - **4.0 removals:** process creation through a leading `|` in `Kernel#open` and `IO` class methods, `Ractor.yield` and `Ractor#take`, `Process::Status#&` and `#>>`. - **4.0 behaviour:** `Net::HTTP` no longer sets `Content-Type: application/x-www-form-urlencoded` automatically on requests with a body. A payout call that relied on it now sends no Content-Type at all. | Release | Most likely payroll break | |---|---| | 3.2 | `File.exists?` or `Fixnum` in old code | | 3.3 | lambda with a non-literal block | | 3.4 | undeclared `bigdecimal` and `csv`; string-matching tests | | 4.0 | undeclared `logger`; missing Content-Type on POSTs | ## Verifying the result Green unit tests are necessary but not sufficient for a payroll service. Before switching production traffic, compare a full payroll run on the old and new Ruby with the same inputs: gross and net totals, tax lines and export files should be byte-for-byte identical, except where a known format change (such as `Hash#inspect` in a log line) is expected. Differences in rounding or formatting show up here, not in unit tests that mock the data. Keep the old image deployable until at least one real pay cycle has completed on the new version. ## What is not a break `Set` and `Pathname` became core classes in 4.0, so existing `require "set"` lines are harmless. Prism became the default parser in 3.4 without changing the language. The major number itself means nothing special: 4.0 is the yearly release, and Ruby treats a major bump like any minor one for compatibility.
- Why reinstall gems after switching Ruby even if the Gemfile.lock is unchanged?RubyGems installs gems per Ruby API version, into a separate `gems/4.0.0` directory, and C extensions are compiled against a specific Ruby. The 3.1 installation's gems are neither visible to 4.0 nor binary-compatible with it, so `bundle install` must run again on the new Ruby and rebuild every native extension.
- Is it safe to jump from 3.1 straight to 4.0 in one change?It can work for a small service, but it hides where each failure comes from and skips the releases that warn about libraries leaving the default gems. Running CI on 3.4 before 4.0 turns silent future LoadErrors into warnings you can fix first, and makes each failure attributable to one release's changes.
saying these in an interview costs you the question
- Ruby 4.0 is a rewrite, so everything must be retested from scratch
- Bundled gems like bigdecimal load under Bundler without a Gemfile entry
- Gems installed for Ruby 3.1 keep working after switching to 4.0
- Hash#inspect output is stable, so tests can compare it across versions
- Ruby 4.0 froze all string literals, so every literal mutation raises