A Hundred and Eight Files, Not a Hundred and Sixty-One
Ask git what an extraction cost and it will confidently give you the wrong number. Inflow's microservices branch diffs at 161 files against master - the honest figure is 108, and the fifty-three-file gap is a lesson in how migration retrospectives get fabricated.
Part 1 ended on a number, so let us start by getting it right. The question every architect asks about an extraction is “what did it cost”, and the tool everybody reaches for is a diff stat against the branch you came from. On Inflow that reads:
$ git diff master origin/microservices --stat
161 files changed, 2840 insertions(+), 148 deletions(-)
One hundred and sixty-one files. It is a clean, quotable number, it would go straight into a slide, and it is wrong — not slightly wrong, but wrong by a third, and wrong in the direction that makes extraction look more expensive and messier than it was. The honest number is on the commit, not the branch:
$ git show 35a9859 --stat
108 files changed, 2754 insertions(+), 54 deletions(-)
The fifty-three-file gap is worth an article because it is made of exactly the three kinds of noise that contaminate every real migration retrospective.
Two numbers for one question
Recall the graph. 35a9859 Microservices transition branches off 34f3f02 init and is the whole extraction, authored 31 October 2021 on .NET 5. Then two things happened that have nothing to do with microservices:
95fe438ported master to .NET 6.4724753ported the branch to .NET 6, separately.39e96dabumped packages on master only.
A diff between the tips of master and origin/microservices therefore answers “how far apart are these two trees today”, which folds three unrelated changes into the extraction's bill. origin/net5-microservices — the branch's parent, init plus the transition commit and nothing else — is the control, and it is why the 108 figure is checkable rather than asserted.
The forensics on the two ports are worth a paragraph, because they explain the whole gap. Their author dates are 30 November 2021, three minutes twenty-five seconds apart. Their committer dates are both 1 January 2022, thirty-seven seconds apart. Somebody sat down on New Year's Day, upgraded two long-lived branches in one sitting, and backdated the commits. Two independently-authored .NET 6 ports of substantially the same 500-file codebase now sit on either side of your diff, and every place they made a different arbitrary choice shows up as extraction work.
Fifty-one files that change by exactly one line
Filter the diff down to files where git reports a single insertion and a single deletion:
$ git diff master origin/microservices --numstat | awk '$1==1 && $2==1' | wc -l
51
Fifty-one of the 161 files are one-line changes. Nearly a third of the “cost” of extracting a module is single-line churn, and not one of those lines is in the transition commit. Open any of them and you get something like SecurityProvider.cs:
-internal sealed class SecurityProvider : ISecurityProvider
+public sealed class SecurityProvider : ISecurityProvider
Which brings us to the trap.
The visibility churn runs the other way
The naive reading of that hunk is that the microservices branch widened the shared framework's API surface — that extracting a module forced dozens of internal types public, which would be a genuinely interesting architectural finding about what a process boundary costs you in encapsulation.
It is the opposite. At 34f3f02 init, SecurityProvider is already public sealed. Master's .NET 6 port is the commit that tightened it: 95fe438 adds 88 internal sealed class declarations across 519 files, a deliberate encapsulation pass bundled into a framework upgrade. The branch's own .NET 6 port did not do that pass, so it kept what init had.
So the diff shows internal → public because master moved and the branch stood still. Attribute those 51 files to the extraction and you have invented an architectural consequence out of a branch that was never rebased. The transition commit itself adds 55 public class or record declarations and four internal ones — but every one of them is in a new file, in the extracted service, and there is a specific reason for it that has nothing to do with encapsulation philosophy. The modules keep their types internal by way of [assembly: InternalsVisibleTo("Inflow.Modules.Payments.Api")] in each Core project. The copied service's Extensions.cs does not carry those lines forward, and Startup.cs calls SubscribeEvent<SignedUp>() from the Api project — so SignedUp had to be public. One dropped attribute line propagated a visibility change across fifty files of copied domain code, which is a real finding, and a much smaller and more interesting one than the fake version.
The rule that falls out of this: internal → public on this branch is only evidence of a decision if the hunk is inside 35a9859. Everywhere else it is drift.
Eight migration files that only moved
The second noise source is more comic. Both .NET 6 ports regenerated the EF migrations, so the diff contains this:
{20211229212455_Customers_Init.Designer.cs => 20211229214321_Customers_Init.Designer.cs} | 2 +-
{20211229212455_Customers_Init.cs => 20211229214321_Customers_Init.cs} | 0
Four modules, two files each: eight files of the 161. Master's migrations were scaffolded at 21:24:55 on 29 December 2021; the branch's at 21:43:21 the same evening. Eighteen minutes apart, identical schema, different filenames, because the migration id is a wall-clock timestamp. Four of the eight are pure renames with zero changed lines; the other four differ only in the [Migration("...")] attribute string.
None of that is extraction cost. All of it is the cost of running dotnet ef migrations add twice on two branches.
The third source is smaller and one-directional: master took 39e96da packages update in July 2022 and the branch never did. origin/microservices still pins Microsoft.EntityFrameworkCore.Design at 6.0.1 where master has 6.0.7, and is behind on Npgsql.EntityFrameworkCore.PostgreSQL, Humanizer and Serilog.Sinks.Seq too. That is worth noticing for its own sake — the branch that documents how the estate does microservices has been quietly rotting relative to the branch people actually read — but it is a maintenance fact, not a design one.
What the 108 actually buys
Strip all of that away and the transition commit is remarkably legible. Grouped by area:
| Area | Files | What it is |
|---|---|---|
src/Services/Customers |
64 | The extracted service - Api host, Core copy, migration, .rest file |
src/Shared |
13 | 3 new abstractions, 5 RabbitMQ files, 5 edited |
src/APIGateway |
6 | The YARP project, entire |
src/Modules/Payments |
7 | Attributes on four records, one Use block, migration noise |
src/Modules/Wallets |
5 | Two contracts deleted, one Use block |
src/Modules/Users |
3 | One new endpoint |
src/Modules/Saga |
3 | Two message copies, one Use block |
src/Modules/Customers |
3 | One boolean, two migration designer touches |
src/Bootstrapper |
2 | appsettings.json, launchSettings.json |
| root | 2 | Inflow.sln, docker-compose.yml |
Sixty-four of 108 files are the copied service. Six are the gateway. Thirteen files of shared framework and twenty-three lines of module-side edits are the entire integration cost — and of those thirteen, five are brand-new files in a folder nothing else references. That is the shape of the argument this series makes, and you can only see it once you stop diffing branches.
Measure the commit, not the branch
The durable lesson is procedural, and it is one I would apply to any migration you intend to write up afterwards.
A migration is a commit, not a branch — so tag it. The moment your extraction lands, tag it. Every framework upgrade, dependency bump and encapsulation pass that follows will otherwise silently join your cost estimate, and worse, will do so asymmetrically: changes made on the branch you left look like changes made by the branch you are on. Nobody reading git diff main feature/extract-customers a year later can tell the difference, and the diff is confident either way.
Three checks before you quote a migration diff at anyone:
- Find the parent. If the branch has commits after the migration, diff the migration commit against its own parent, not against the other branch's tip.
- Filter one-line hunks. A third of Inflow's apparent cost is
$1==1 && $2==1. If yours has a comparable tail, it is drift, and you should read a sample before attributing any of it. - Check the direction of every “consequence”. The most seductive finding in this diff — a process boundary forcing types public — was master tightening while the branch stood still.
That leaves us with 108 honest files, thirteen of them in the shared framework. Next, the interface that did not move: the one file in that framework that this whole exercise was a test of, and the hash that proves it passed.