TL;DR: Treat the controller as a production system. Back up JENKINS_HOME, restore it onto a staging controller, upgrade there first with the plugin set pinned in a file, run real jobs against it, then do production in a window with a tested rollback. Pinned versions and a rehearsal are what make it uneventful.
How to approach it
Frame Jenkins honestly: a Java application whose behaviour is mostly supplied by a hundred-odd community plugins with their own dependency graph and their own release cadence. Upgrades go wrong at the plugin layer far more often than the core. Then give the sequence, and make the restore rehearsal the centre of it.
A strong answer
Know what state is, because backup is the whole safety net. JENKINS_HOME holds everything: job configurations, build history, credentials, plugin jars, and the secrets used to encrypt them. Back up the directory, and specifically include secrets/ and credentials.xml together, because credentials restored without the matching secret key are unreadable. Job history is the bulk of the size, and excluding old build artifacts keeps the backup a sensible size.
Pin plugin versions in a file rather than clicking "update" in the UI. A plugins.txt with explicit versions, installed by the plugin installation manager at image build time, means the controller is reproducible and a rollback is a previous image rather than an archaeology exercise. Running Jenkins as a container built from a Dockerfile that installs a pinned plugin set turns the whole controller into something you can rebuild from git.
The upgrade sequence:
Read the changelogs, specifically for plugins marked as having breaking changes, and for the core's minimum Java version, which has moved several times and is the single most common way an upgrade fails on start.
Restore the backup onto a separate controller. This is the step people skip and it is the one that finds the problems. A staging controller with real job configurations, running a handful of real pipelines, will surface a plugin that changed a step signature, which a fresh empty Jenkins will not.
Upgrade the staging controller: core first, then plugins, then run the jobs again. Compare the results.
For production, take a fresh backup immediately before, put the controller in shutdown mode so running builds finish and no new ones start, upgrade, restart, and watch the log for plugins that failed to load. The "Manage Jenkins" screen lists failed plugins after a restart, and that list is where the damage shows.
Rollback is restoring the previous JENKINS_HOME and the previous plugin set. Both, together: an old home directory with new plugins is not a state anyone has tested.
Two structural improvements worth raising, because they are what reduces the risk permanently. Move build execution off the controller entirely, so agents run everything and the controller only schedules; a controller running builds is one bad job away from being down for everyone. And configure it as code (the JCasC plugin) so controller settings are a YAML file in git rather than accumulated UI state.
What interviewers probe next
"How often should you upgrade?" Regularly and in small steps. A controller two years behind cannot be moved in one jump, because plugin dependency ranges will not resolve, and that is how teams end up frozen on an unsupported version with known vulnerabilities.
"How do you handle a plugin that is abandoned?" Find out what it actually does for you, which is usually less than assumed, and replace it or absorb the behaviour into a shared library step. An unmaintained plugin blocks core upgrades and accumulates CVEs.
"What if you cannot take an outage?" Run a second controller, migrate jobs across in batches, and switch teams over gradually. It costs more and it is the only real answer when one window is not available.
Common mistakes
Clicking update-all in the UI on the production controller, which is an unpinned, unrehearsed, unrollbackable change to everyone's build system.
Backing up job configuration without secrets/, so the restored controller has credentials it cannot decrypt.
Running builds on the controller, so agent load and plugin behaviour can take down scheduling for every team.
Leaving an upgrade for two years, after which the dependency graph makes a single-step upgrade impossible.