Case study · 2024
Company-wide Node.js platform migration
Sole technical owner
Sole technical owner of a Node v18 → v22 upgrade across every backend service and microservice, delivered with zero customer-facing downtime.
- Node.js
- TypeScript
- AWS SDK v3
- Prisma
- Microservices
- CI/CD
Context
The platform is a stakeholder-engagement product used by enterprise and government clients — a set of Node.js backend services and microservices sharing databases, build tooling, and deployment pipelines. The runtime had drifted to Node 18, which was approaching end-of-life. Staying put meant losing security patches and falling behind on language and dependency support; several libraries we wanted to adopt already required newer runtimes.
I owned the upgrade end to end — scoping, sequencing, execution, and rollout — across all services.
The problem, precisely
A runtime bump sounds like changing one number in a Dockerfile. In a shared multi-service codebase it isn’t. The real problem was coordinated change under a zero-downtime constraint:
- Multiple services shared libraries and a common build/tooling layer, so they couldn’t all be upgraded independently, but they also couldn’t all be upgraded at once without unacceptable blast radius.
- Node 18 → 22 crossed breaking changes in native modules and several transitive dependencies, so “it compiles” was not the same as “it’s safe.”
- The same effort was the right moment to retire accumulated tooling debt — otherwise we’d pay the migration tax twice.
Constraints I designed around
- No customer-facing downtime. Production had to stay stable throughout.
- Reversibility. Every step needed a clean rollback path; a big-bang cutover was off the table.
- One migration, not many. Bundle the runtime bump with the dependency and tooling upgrades it forced, rather than doing three disruptive passes.
Approach
Sequenced, not simultaneous. I ordered services by risk and coupling — leaf services and internal tooling first to validate the upgrade path, then the higher-traffic core services once the pattern was proven. Each service moved behind its own reviewable change so a regression could be isolated to one rollout, not the whole fleet.
Bundled the forced upgrades. The runtime jump pulled in matching work, which I folded into the same migration instead of deferring:
- AWS SDK v2 → v3 — moving to the modular clients, which also trimmed cold bundle size.
- Prisma ORM upgrade — aligning the data layer with the new runtime.
- Tooling modernization — replacing legacy Gulp tasks with plain npm scripts for builds, schema updates, and automation, removing a brittle layer that new contributors kept tripping over.
Made types do the verification. Alongside the runtime work I pushed strict TypeScript across the services, converting previously loosely-typed code into fully strongly-typed modules. That turned a class of runtime-only migration risks into compile-time errors — the compiler caught breakage the runtime bump would otherwise have surfaced in production.
Phased rollout with a rollback path. Services shipped in planned waves. Each wave was observed in production before the next started, so problems stayed contained and every step could be reverted independently.
Outcome
- Every backend service and microservice migrated from Node 18 to Node 22, with production stability maintained throughout and no customer-facing downtime.
- AWS SDK v3, an upgraded Prisma, and an npm-script-based build pipeline landed in the same effort — the migration tax was paid once.
- The services came out fully strongly-typed, improving reliability and raising day-to-day developer velocity for the whole team.
What I’d carry forward
The leverage was in sequencing and bundling, not in any single code change. Ordering by risk bought a safe place to learn the upgrade path before touching the services that mattered most; bundling the forced upgrades avoided repeating a disruptive migration three times; and leaning on the type system converted “hope it still works” into “the compiler says it does.” The upgrade itself was routine — owning the plan is what kept it boring, which for a production migration is exactly the goal.