<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Node.js Backend Refactor Case Studies · Remote Since Forever</title>
  <subtitle>Case studies of refactoring a Node.js backend without downtime — mega-service split, TypeORM 1.0 upgrade, and migrations that keep production quiet.</subtitle>
  <link href="https://blog.clearview.team/tags/nodejs-backend-refactors/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://blog.clearview.team/tags/nodejs-backend-refactors/" />
  <updated>2026-08-20T17:39:48+02:00</updated>
  <id>https://blog.clearview.team/tags/nodejs-backend-refactors/feed.xml</id>
  <author>
    <name>Clearview Team</name>
  </author>
  <entry>
    <title>Upgrading TypeORM 0.3 to 1.0 in Production: A NestJS Case Study</title>
    <link href="https://blog.clearview.team/2026/the-typeorm-1-0-upgrade-that-did-not-block-production/" />
    <id>https://blog.clearview.team/2026/the-typeorm-1-0-upgrade-that-did-not-block-production/</id>
    <published>2026-08-17T11:00:00+02:00</published>
    <updated>2026-08-17T11:00:00+02:00</updated>
    <author>
      <name>Taufan Fadhilah</name>
    </author>
    <summary>How we took TypeORM 0.3 → 1.0 to a live NestJS API without a headache.</summary>
    <content type="html">&lt;p&gt;A backend service I work on runs on TypeORM. Earlier this year we jumped from 0.x to the new 1.0 release. The old line had gone almost five years without a breaking change, and a class of quietly-wrong-answer bugs had piled up in it: queries that returned bad data instead of raising an error. The 1.0 release fixes them by making the bad queries fail loudly instead. It also forced an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@nestjs/typeorm&lt;/code&gt; upgrade at the same time, since the NestJS glue layer for TypeORM crashes at startup on the new TypeORM if you are still on the old version. But the catch with any major ORM upgrade is that every entity, every query builder, every database factory in the test suite gets touched.&lt;/p&gt;

&lt;p&gt;The team had two constraints. &lt;em&gt;&quot;Do not block the rest of the work&quot;&lt;/em&gt; and &lt;em&gt;&quot;do not break production.&quot;&lt;/em&gt; On a service that deploys every day, with a long backlog of feature work that the product team had committed to, an upgrade that needed the whole team to stop and migrate together would have been the wrong shape. We had to find the order that let the migration happen alongside the feature work, with the test suite green at every commit and production unaware that anything had changed.&lt;/p&gt;

&lt;p&gt;The order we ended up with: a staging branch that absorbed the breaking changes, the test-harness migration ahead of the runtime, the Docker image pinned in parallel, and the eight or so smaller follow-ups that landed after the main upgrade so nobody had to read a five-thousand-line PR.&lt;/p&gt;

&lt;h2 id=&quot;two-terms-before-we-go-further&quot;&gt;Two terms before we go further&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;TypeORM&lt;/strong&gt; is a TypeScript ORM for SQL databases. It maps decorated TypeScript classes to tables and gives you a query builder, an entity manager, and a migration runner. The 0.x to 1.0 upgrade is the kind of release that touches every file in the codebase that talks to the database.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test harness&lt;/strong&gt;, in this post, is the infrastructure around the test suite: the factories that build entities, the per-test database setup and teardown, the mocked services, the rate-limit mocks, the fixtures. None of it ships to production. All of it has to be green for CI to pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;four-places-the-upgrade-broke&quot;&gt;Four places the upgrade broke&lt;/h2&gt;

&lt;p&gt;The breaking surface on the upgrade landed in four places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Factory APIs.&lt;/strong&gt; TypeORM&apos;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setSeederFactory&lt;/code&gt; shape changed. Every factory we had (about thirty of them, one per major entity) needed a new signature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository methods.&lt;/strong&gt; A handful of methods that had a &lt;em&gt;&quot;first one wins&quot;&lt;/em&gt; behaviour in 0.x became &lt;em&gt;&quot;explicit single or throw&quot;&lt;/em&gt; in 1.0. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOne()&lt;/code&gt; without an options argument is no longer valid; you have to pass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOne({ where: { id } })&lt;/code&gt; or use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOneBy({ id })&lt;/code&gt;. This was a global codemod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entity manager transactions.&lt;/strong&gt; The transaction API tightened. Some callsites that had been relying on implicit transaction propagation needed explicit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;manager.withRepository(...)&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decorator metadata.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PrimaryGeneratedColumn&lt;/code&gt; got stricter about its options. A handful of entities with custom configurations needed a small rewrite.&lt;/p&gt;

&lt;p&gt;None of those were hard individually. All four at once, on a codebase with hundreds of entities, was the upgrade.&lt;/p&gt;

&lt;h2 id=&quot;the-order-we-ran-it-in&quot;&gt;The order we ran it in&lt;/h2&gt;

&lt;aside class=&quot;callout&quot;&gt;
  &lt;p&gt;&lt;strong&gt;The rule that ordered everything else.&lt;/strong&gt; If the test harness is broken, every open PR is blocked. If the runtime is broken, only the upgrade PR is blocked. Test harness goes first.&lt;/p&gt;
&lt;/aside&gt;

&lt;figure&gt;
  &lt;img src=&quot;/assets/images/posts/the-typeorm-1-0-upgrade-that-did-not-block-production/staging-timeline.svg&quot; alt=&quot;A three-lane timeline across two weeks. Lane 1: the Docker base image pinned to node:22.17.1 on a parallel branch in week 0, then left inert on the staging branch through weeks 1 and 2. Lane 2: the test-harness migration (setSeederFactory shape change, about thirty factories) landed in week 1 while the runtime was still on 0.x. Lane 3: the runtime upgrade (typeorm@1.0.0, findOne to findOneBy codemod, transaction-API tightening) landed on the staging branch in week 1, absorbed daily feature-PR rebases across week 2, and merged to main at the end of week 2.&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;720&quot; /&gt;
  &lt;figcaption&gt;Three pieces of work, three landing windows, one merge to main. The Docker base spent a week proving itself inert, the test harness landed while the runtime was still on 0.x, and the runtime upgrade lived on a staging branch long enough for every feature PR to rebase onto it.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Each step below has its own commit (or branch) and was reviewable on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One. Pin the new Docker image and Node version on a parallel branch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TypeORM 1.0 requires Node 22. Our production image was on Node 20. Before any code changed, the new image had to be built and tested. We landed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node 22.17.1 + production stage yarn setup for typeorm@1.0.0&lt;/code&gt; as the first commit on the upgrade branch:&lt;/p&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Dockerfile (shortened)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:22.17.1-bookworm-slim&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;builder&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /app&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; package.json yarn.lock .yarnrc.yml ./&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; .yarn .yarn&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;yarn &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--immutable&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;node:22.17.1-bookworm-slim&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;production&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /app&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; . .&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;yarn &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--immutable&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--production&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;node&quot;, &quot;dist/server.js&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The build target uses the production-stage yarn setup because the new TypeORM brings transitive dependencies that the old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--immutable&lt;/code&gt; flag was tolerant of and the new one is not. We ran the new image against the old code for a week in staging to make sure the runtime change itself was inert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two. Migrate the test harness &lt;em&gt;before&lt;/em&gt; the runtime.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the step that decided whether the upgrade would block the team or not. The test harness is what runs in CI on &lt;em&gt;every&lt;/em&gt; PR. If the test harness is half-migrated, nobody can ship anything. So the test harness goes first, in its own PR, with the old TypeORM still in production:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// database/factories/address.factory.ts (before)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;typeorm-seeding&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../../modules/address/address.entity&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;define&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;street&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// database/factories/address.factory.ts (after)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setSeederFactory&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;typeorm-extension&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../../modules/address/address.entity&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setSeederFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;street&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;faker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;About thirty factories, all the same shape, all migrated in one commit. We landed the test harness work as a series of &lt;em&gt;&quot;complete TypeORM 1.0 test-harness migration&quot;&lt;/em&gt; commits. Runtime was still on 0.x, test setup was ready for 1.0. The two coexisted because the factories are dev-time only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three. Bump the runtime on a staging branch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The actual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; change. Roughly fifteen lines changed in the manifest, a hundred or so callsites changed across the source for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOne&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOneBy&lt;/code&gt; codemod and the transaction-API tightening. We landed it on a staging branch that lived for about a week.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;typeorm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;^1.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;transitive&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;updates&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;things&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;that&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;wanted&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Node&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;22&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During that week, every other PR rebased onto the staging branch as it was merged. Conflicts happened (entity decorators and query builders both got touched), but they stayed bounded. We had a Slack channel for &lt;em&gt;&quot;my PR is breaking on the staging rebase, who else has touched this file&quot;&lt;/em&gt; and it never had more than one or two messages a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four. Fix the small tests that quietly broke.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the runtime was on 1.0, a small set of tests broke for reasons the code diff itself did not show. The framework was behaving slightly differently now, and the tests were catching it. A rate-limit mock had assumed the old library called it at one specific moment; the new library called it a moment later. A recaptcha mock had relied on a method name that had been renamed. Some test suites we had not touched in a long time were quietly assuming old data-object shapes and stopped compiling once the underlying types changed.&lt;/p&gt;

&lt;p&gt;We landed these as a series of small commits with names like &lt;em&gt;&quot;test: align auth assertions and complete rate-limit mock&quot;&lt;/em&gt; and &lt;em&gt;&quot;test: fix constructor drift in the compile-failing test suites.&quot;&lt;/em&gt; Each one was a few hours of work and a small PR. Nothing dramatic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five. Ship the security fixes we had queued up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one was a bonus. The TypeORM upgrade gave us a single PR window where every service was already being touched. We pulled in a handful of unrelated security findings (bugs where an attacker could swap in another user&apos;s ID, and write endpoints that let one user modify another user&apos;s data) and shipped them in the same staging branch. &lt;em&gt;&quot;While we are in here&quot;&lt;/em&gt; is a real shipping pattern; we used it once, deliberately, on the boundary the upgrade had opened anyway.&lt;/p&gt;

&lt;h2 id=&quot;two-conflicts-we-planned-for-and-didnt-need&quot;&gt;Two conflicts we planned for and didn&apos;t need&lt;/h2&gt;

&lt;p&gt;Two conflicts I want to call out because they are the ones I was expecting and did not get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production rollback.&lt;/strong&gt; We had a plan. The plan was &lt;em&gt;&quot;deploy the new image, watch the error rate, roll back if anything spikes.&quot;&lt;/em&gt; We did not need it. The week of staging burn-in caught everything that would have spiked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration runner.&lt;/strong&gt; TypeORM 1.0 has a different migrations API than 0.x. We were prepared to rewrite our migration files. We did not need to. The existing migrations are run-once historical records that the new runner reads cleanly without changes. The &lt;em&gt;new&lt;/em&gt; migrations going forward use the new API, but the catalogue of historical migrations is untouched.&lt;/p&gt;

&lt;h2 id=&quot;what-wed-do-again-on-the-next-one&quot;&gt;What we&apos;d do again on the next one&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Test harness first, runtime second.&lt;/strong&gt; If the harness is broken, every other PR is blocked. If the runtime is broken, only the upgrade PR is blocked.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pin the new base image on a parallel branch.&lt;/strong&gt; Node version upgrades that come bundled with the main one should be tested as their own change before the code change lands on top.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Codemod the global API changes in one commit.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOne()&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findOneBy()&lt;/code&gt; is hundreds of callsites. One mechanical commit beats fifty thoughtful ones.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Keep the staging branch alive for a week.&lt;/strong&gt; Let other PRs rebase onto it. The conflicts are real but they are bounded if the staging branch is fresh.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pull in the &lt;em&gt;&quot;while we are in here&quot;&lt;/em&gt; fixes deliberately.&lt;/strong&gt; Big PRs are scary, but a major version upgrade is the one PR window where every file gets touched anyway. The marginal cost of an extra security fix is much lower in that window than in any other.&lt;/li&gt;
&lt;/ol&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-run-your-major-dependency-upgrade&quot;&gt;We Could Run Your Major Dependency Upgrade&lt;/h2&gt;

  &lt;p&gt;If your team has a major-version dependency upgrade (TypeORM, Prisma, Next.js, React, NestJS, a Node base-image jump) that has been on the backlog for a quarter because nobody has had the bandwidth to plan it without stalling the sprint, &lt;strong&gt;Clearview Team&lt;/strong&gt; has shipped a few of those. Your feature team keeps shipping while the upgrade lands on a parallel staging branch, the test harness moves ahead of the runtime so nobody&apos;s CI is broken mid-week, and production sees the new version without a single dropped request. Send us the version-from and the version-to; we&apos;ll scope it.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;mailto:info@clearview.team?subject=Major%20dependency%20upgrade%20enquiry&quot;&gt;Brief us on your dependency upgrade →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="backend" />
    <category term="typescript" />
    <category term="nestjs" />
    <category term="typeorm" />
    <category term="postgres" />
    <category term="dependency-upgrade" />
    <category term="case-study" />
    <category term="nodejs-backend-refactors" />
    
  </entry>
  
  <entry>
    <title>Splitting a Mega-Service Into Four: The Service-Facade Refactor (Plus a Reusable Skill)</title>
    <link href="https://blog.clearview.team/2026/splitting-a-mega-service-into-four/" />
    <id>https://blog.clearview.team/2026/splitting-a-mega-service-into-four/</id>
    <published>2026-08-05T11:00:00+02:00</published>
    <updated>2026-08-05T11:00:00+02:00</updated>
    <author>
      <name>Taufan Fadhilah</name>
    </author>
    <summary>How we split a 1,307-line `user.service.ts` into four responsibility-tagged NestJS sub-services (query, mutation, membership, stats) behind a stable facade — no caller broke, no test changed, one PR, and a reusable Claude skill file at the end.</summary>
    <content type="html">&lt;p&gt;On a backend I work on, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.service.ts&lt;/code&gt; had crossed 1,307 lines. Nobody on the team could hold the whole file in their head. New hires bounced off it on day one. Reviews took twice as long because the reviewer did not remember the surrounding code the diff was sitting in, and the blame view was a quilt of contributors going back years.&lt;/p&gt;

&lt;p&gt;We split the file into four sub-services behind a stable facade, landed the whole thing in one PR without changing a test, and wrote the procedure up as a reusable Claude skill for the next time.&lt;/p&gt;

&lt;h2 id=&quot;two-terms-before-we-go-further&quot;&gt;Two terms before we go further&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Facade&lt;/strong&gt;, in this post, is a class that exposes the same public methods as the old service did, but delegates each method to the right one of the new sub-services. The old import path keeps working; the internal shape moves.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sub-service&lt;/strong&gt; is one of the new files the mega-service is split into. Each sub-service owns one slice of responsibility (&lt;em&gt;querying&lt;/em&gt;, &lt;em&gt;mutating&lt;/em&gt;, &lt;em&gt;membership-state&lt;/em&gt;, &lt;em&gt;stats&lt;/em&gt;) and is named after that slice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-split-we-shipped&quot;&gt;The split we shipped&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;src/modules/user/
├── user.service.ts                   #  facade — was 1,307 lines, now ~120
├── user-query.service.ts             #  349 lines — read paths
├── user-mutation.service.ts          #  460 lines — write paths
├── user-membership.service.ts        #  288 lines — membership-state changes
├── user-stats.service.ts             #  307 lines — derived counts and aggregates
└── user.service.password-reset.spec.ts
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The split ran along responsibility inside the service layer, not up through the controller and model layers. Reading a user and updating a user are different jobs on the same domain object, and they grow at different rates. On this service, the read methods stayed small and stable while the write methods kept picking up business rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query&lt;/strong&gt; — anything that returned a user (or a list of users) without mutating state. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findById&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findByEmail&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findByCompany&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;searchByKeyword&lt;/code&gt;. The query service does not write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutation&lt;/strong&gt; — anything that wrote a user row (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updatePreferences&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;softDelete&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore&lt;/code&gt;). The mutation service loads a row, mutates it, saves it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Membership&lt;/strong&gt; — methods that flip the user&apos;s membership state: upgrade tier, downgrade tier, transfer membership, attach to company, detach from company. The state transitions are tangled enough to justify their own file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stats&lt;/strong&gt; — derived counts and aggregates. &lt;em&gt;&quot;How many users in this company are active,&quot;&lt;/em&gt; &lt;em&gt;&quot;how many memberships expire this month,&quot;&lt;/em&gt; &lt;em&gt;&quot;how many users by role.&quot;&lt;/em&gt; Reading across many rows is a different job from finding a single user.&lt;/p&gt;

&lt;p&gt;A few methods did not fit any of the four. Some were doing two things and got split before they moved. Some were orchestration — they touched two or more sub-services in a single flow — and stayed on the facade.&lt;/p&gt;

&lt;h2 id=&quot;the-facade--keep-the-public-api-stable&quot;&gt;The facade — keep the public API stable&lt;/h2&gt;

&lt;p&gt;The facade is the file every other module imports. Its surface area is identical to the old service: every method other modules called still exists, with the same signature.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// user.service.ts — the facade, after the split&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserService&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserQueryService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserMutationService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserMembershipService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;UserStatsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ─── Read methods delegate to query ─────────────────────────────────&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;findById&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;findByEmail&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;findByEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;findByCompany&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;findByCompany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;searchByKeyword&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;searchByKeyword&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ─── Write methods delegate to mutation ─────────────────────────────&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;updatePreferences&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updatePreferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;softDelete&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;softDelete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;restore&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;restore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ─── Membership ─────────────────────────────────────────────────────&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;upgradeMembership&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;upgrade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;downgradeMembership&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;downgrade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;attachMemberToCompany&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;attachToCompany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;membership&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ─── Stats ──────────────────────────────────────────────────────────&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;countActiveByCompany&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;countActiveByCompany&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;countExpiringMemberships&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;countExpiringMemberships&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// ─── Orchestration methods stay here ────────────────────────────────&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Methods that genuinely coordinate across two or more sub-services.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;passwordResetFlow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;newPassword&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;findByEmail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;mutation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updatePassword&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;newPassword&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;recordSecurityEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;password-reset&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bind&lt;/code&gt;-and-assign shape looks unusual on first read, but each method on the facade is the same function object as the one on the sub-service — no wrapper indirection, no extra logging. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userService.findById(42)&lt;/code&gt; runs with the same overhead it always had.&lt;/p&gt;

&lt;p&gt;Orchestration methods stay on the facade because they touch several sub-services in one flow. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;passwordResetFlow&lt;/code&gt; above hits the query, mutation, and stats services in sequence, so it belongs to none of them alone.&lt;/p&gt;

&lt;h2 id=&quot;how-the-split-landed-in-one-pr&quot;&gt;How the split landed in one PR&lt;/h2&gt;

&lt;p&gt;The PR added around sixteen hundred lines and deleted around twelve hundred. Most of it was file moves. The tests did not need to change: because the facade&apos;s public API is byte-identical to the old service, every test that imported &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UserService&lt;/code&gt; kept passing untouched. That is the safety net the whole refactor rides on.&lt;/p&gt;

&lt;p&gt;Three disciplines made the PR reviewable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One sub-service per commit.&lt;/strong&gt; Four code commits, one per sub-service, plus a fifth that thins the facade. The reviewer reads one sub-service at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method-by-method migration.&lt;/strong&gt; Each method moved with its tests. If a test in the suite was targeting a single method, that test stayed near the method, in the same commit as the move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No behavioural changes.&lt;/strong&gt; Every method moved is byte-identical to what was in the old service. Behavioural refactors — N+1 fixes, added logging, tighter authorization — go in follow-up PRs after the split lands.&lt;/p&gt;

&lt;h2 id=&quot;the-reusable-skill--service-facade-refactor&quot;&gt;The reusable skill — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;service-facade-refactor&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;The skill file lives at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/service-facade-refactor/SKILL.md&lt;/code&gt; in this codebase and in our internal skill library. The next mega-service does not need to re-derive any of this.&lt;/p&gt;

&lt;p&gt;A Claude skill is a small Markdown file documenting a reusable procedure in enough detail that an LLM-assisted contributor can apply it without having lived through the original work.&lt;/p&gt;

&lt;p&gt;The skill in full:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;# Service Facade Refactor&lt;/span&gt;

A pattern for splitting a single mega-service file into multiple
responsibility-tagged sub-services behind a stable public facade,
without breaking any caller.

&lt;span class=&quot;gu&quot;&gt;## When to use this skill&lt;/span&gt;

A single service file has crossed roughly 800 lines, the team is
avoiding it on PRs, and the file mixes more than one of:
read paths, write paths, state-machine transitions, derived stats.

If the file is large but coherent (one responsibility, just verbose),
this is not the right skill — use a method-extraction refactor instead.

&lt;span class=&quot;gu&quot;&gt;## Inputs&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; The mega-service file (path).
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; The full test suite (must be green before starting).
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; One hour of uninterrupted reading time, before any code moves.

&lt;span class=&quot;gu&quot;&gt;## Steps&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Tag every method on the existing service.**&lt;/span&gt; Read top-to-bottom.
   Each method gets exactly one tag:
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Read**&lt;/span&gt; — returns the entity, no writes.
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Write**&lt;/span&gt; — mutates the entity, may read first.
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**State machine**&lt;/span&gt; — tier upgrades, status transitions, lifecycle.
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Stats**&lt;/span&gt; — counts, aggregates, derived values across many entities.
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Orchestration**&lt;/span&gt; — coordinates across two or more of the above.

   If a method does not fit one tag, it is doing two things; split
   the method &lt;span class=&quot;ge&quot;&gt;*before*&lt;/span&gt; you split the service.
&lt;span class=&quot;p&quot;&gt;
2.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Each non-orchestration tag becomes a sub-service.**&lt;/span&gt;
   File naming: &lt;span class=&quot;sb&quot;&gt;`&amp;lt;domain&amp;gt;-&amp;lt;tag&amp;gt;.service.ts`&lt;/span&gt;. Example: &lt;span class=&quot;sb&quot;&gt;`user-query.service.ts`&lt;/span&gt;.
&lt;span class=&quot;p&quot;&gt;
3.&lt;/span&gt; &lt;span class=&quot;ge&quot;&gt;**&lt;/span&gt;Move methods one tag at a time, in their own commit, with
   their tests.&lt;span class=&quot;ge&quot;&gt;**&lt;/span&gt; The test file moves with the method, or, if the
   tests are integration-style, stay in place and reference the
   sub-service through the facade.
&lt;span class=&quot;p&quot;&gt;
4.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Rewrite the original service file as a facade.**&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; For each method on a sub-service, &lt;span class=&quot;sb&quot;&gt;`bind`&lt;/span&gt;-and-assign on the facade:
     &lt;span class=&quot;sb&quot;&gt;`findById = this.query.findById.bind(this.query);`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;   -&lt;/span&gt; For orchestration methods (touch two or more sub-services), keep
     the implementation on the facade.
&lt;span class=&quot;p&quot;&gt;
5.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Run the full test suite. It should pass with no test changes.**&lt;/span&gt;
   The facade&apos;s public API is byte-identical to the original service.
&lt;span class=&quot;p&quot;&gt;
6.&lt;/span&gt; &lt;span class=&quot;ge&quot;&gt;**&lt;/span&gt;Land as a single PR with one commit per sub-service plus one
   for the facade.&lt;span class=&quot;ge&quot;&gt;**&lt;/span&gt; Behavioural changes (N+1 fixes, new logging,
   tightened authz) are &lt;span class=&quot;ge&quot;&gt;*follow-up PRs*&lt;/span&gt;, not part of the split.

&lt;span class=&quot;gu&quot;&gt;## Output shape&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; One facade file, exposing the same methods the original did.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Four (or fewer) sub-service files, each named by its tag.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Zero test changes.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; A single PR, multi-commit, readable one sub-service at a time.

&lt;span class=&quot;gu&quot;&gt;## What this is not&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; Not a layered split (controller / service / model). The split is
  by responsibility within the service layer, not by layer.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Not a behavioural change. Save the perf and authz fixes for after.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Not appropriate for files under ~500 lines. The overhead of the
  split is worse than the readability win at that size.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Step 1 is where the split gets decided. The tagging walks top-to-bottom on five yes/no questions, and the first &quot;yes&quot; wins.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/assets/images/posts/splitting-a-mega-service-into-four/tagging-decision.svg&quot; alt=&quot;Method-tagging decision flow. Five yes/no questions: does the method write to the database (Mutation), does it read the row and return it (Query), does it flip a status or lifecycle state (Membership), does it aggregate across many rows (Stats), or does it call two or more of the above (Orchestration — stays on the facade). If none of the above, split the method — it is doing two things.&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;900&quot; /&gt;
  &lt;figcaption&gt;First &quot;yes&quot; wins. A method that answers no to all five is doing two things and needs to be split before it is moved.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The skill works across codebases because it does not name a specific domain. Any TypeScript codebase with service classes and a mega-service problem can apply it the same way.&lt;/p&gt;

&lt;p&gt;The judgement calls in the skill took longer to write than the steps. The &lt;em&gt;&quot;if the file is large but coherent, this is not the right skill&quot;&lt;/em&gt; line at the top and the &lt;em&gt;&quot;what this is not&quot;&lt;/em&gt; section at the bottom are what keep the skill from getting misapplied. A skill that only describes the happy path gets used wrong the first time somebody reaches for it.&lt;/p&gt;

&lt;p&gt;The next mega-service on this codebase — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;finance.service.ts&lt;/code&gt;, around eleven hundred lines — is queued up for the same treatment.&lt;/p&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-split-your-mega-service&quot;&gt;We Could Split Your Mega-Service&lt;/h2&gt;

  &lt;p&gt;We have run this swap before, and we can run it on yours. If your team has a service file that has crossed a thousand lines and PRs against it have started slowing down, &lt;strong&gt;Clearview Team&lt;/strong&gt; takes it on as a one-week refactor: your team keeps shipping while we work, and when we hand it back, the file reads in one sitting, the callers are untouched, and the behavioural fixes you actually want are queued as separate follow-ups. Send us the file path and we will scope the split.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;mailto:info@clearview.team?subject=Service%20facade%20refactor%20enquiry&quot;&gt;Brief us on your mega-service →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="backend" />
    <category term="typescript" />
    <category term="nestjs" />
    <category term="refactor" />
    <category term="architecture" />
    <category term="ai" />
    <category term="claude" />
    <category term="claude-skill" />
    <category term="case-study" />
    <category term="nodejs-backend-refactors" />
    
  </entry>
  
</feed>
