Skip to content
Kumar Chandrachooda
.NET

The Price of No Shared Kernel

The retrospective, with the ledger totalled - seven byte-identical MessageBrokers, one AggregateRoot with three meanings, what Convey genuinely optimises, where a Convey.Bootstrap meta-layer should have stood, and what a consumer should copy from this estate.

By Kumar Chandrachooda 28 Feb 2026 9 min read
One class name, three hash values

You can measure a philosophy with a hash function. Take every copy of a file the estate distributes by paste, normalise the service name out of each one, normalise the line endings, and hash what remains. I ran that experiment across the Pacco repositories to close this series, and it returns two results that together are the whole retrospective. MessageBroker.cs — the 87 lines of publish-glue that part 2 met — exists in seven repositories and produces one hash, seven times: after name normalisation the copies are byte-identical. AggregateRoot.cs — the base class of the domain itself — exists in five repositories and produces three different hashes. The glue never diverged; the domain foundation did. Byte-identical where nothing was at stake, silently divergent where everything was — that is the copy-paste stance, measured.

This closing part reads the divergence, totals the arc's ledger, gives the framework its honest due, and answers the question the whole series has been circling: should a shared kernel have existed, and what should a consumer of any chassis copy from this estate — and refuse to.

One name, three meanings

Here is what the three AggregateRoot hashes correspond to. Availability's AddEvent, from Pacco.Services.Availability.Core/Entities/AggregateRoot.cs:

protected void AddEvent(IDomainEvent @event)
{
    if (!_events.Any())
    {
        Version++;
    }

    _events.Add(@event);
}

One version bump per unit of work, on the first event — and the version matters: Availability's repository updates with ReplaceOneAsync(r => r.Id == resource.Id && r.Version < resource.Version, ...), a genuine optimistic-concurrency filter (whose returned result is discarded, so a lost update is indistinguishable from success — but the intent is wired end to end).

Orders, Customers, and Identity share the second hash — an 18-line variant that is byte-identical across the three:

protected void AddEvent(IDomainEvent @event)
{
    _events.Add(@event);
}

Version exists as a property and is never incremented by anything, anywhere, in any of the three services. The field persists as scaffolding; the semantics were left behind in the repo the file was copied from.

Deliveries holds the third hash: a 34-line dialect that adds an IAggregateRoot interface with an explicit IncrementVersion() — which nothing in the repository calls — and a HasChanged property, which exactly one of the service's four handlers consults before persisting. A third design, half-adopted even locally.

Now the point. public class Order : AggregateRoot and public class Resource : AggregateRoot read as the same commitment in every repo — same class name, same folder convention, same shape at the call site. But in one service that base class is a live concurrency token; in three it is an inert field; in one it is an interface contract nobody invokes. The same class name means three different things three folders apart, and no compiler, no test, and no reviewer will ever surface it, because divergence between repositories is invisible to every tool that operates within one. That is the real price of the stance. Not the copying — copying is cheap. The silent divergence of things that still look identical: that is expensive, unbounded, and undetectable by default. It took a hash function and a deliberate audit to see it at all.

What Convey actually optimises

The critique only lands if the credit does, so let me be precise about what building on Convey bought this estate, because the arc verified it repeatedly. The surface compression is real: ten services, and every Program.cs is a legible ~30-line fluent composition — part 3 walked it without reservation. The conventions are real: snake_case wire naming, {service}/{exchange}.{message} queue templates, one options section per package, [Message] and [Contract] attributes — consistent and guessable across every repository, which is worth more at 2 a.m. than any single feature. And the composition is honest: nothing wraps or hides ASP.NET Core; every AddX is an ordinary registration you can read. When I read the framework itself from the inside, I concluded it was the chassis you were going to write anyway. Its era and its trade-offs got their own honest closing in what Convey got right and what modern .NET ate.

Pacco is simultaneously that framework's best demo and its best counter-exhibit. The demo: ten services stood up on the same chassis, each one readable in an afternoon. The counter-exhibit is this series' ledger.

The tax, totalled

Twelve parts of verified findings, in one place:

  • The same 87 lines of publish-glue in seven repositories, hash-identical — the estate's real framework, outside the framework (part 2).
  • Exactly one MongoDB index across ten databases, created fire-and-forget in a disposed scope, while natural-key scans ride the message hot path (part 4).
  • Nine CI pipelines vacuously green over zero test projects; the one real pyramid is the one CI skips (part 5).
  • Contracts exchanged through a filesystem path six directories up (part 6) and a hand-maintained registry of eighty message names that nothing validates (part 7).
  • Two event-design philosophies in one estate, neither named, one of them syncing deletes but never creates (part 8).
  • An orchestrator whose saga state lives in RAM, whose compensation is one joke string, and whose progress rides a magic header agreed by copy-paste (part 9).
  • Eleven lines of FirstOrDefault() where the AI was advertised (part 10); a tray of smells clustered in the hand-rolled glue (part 11); and a graveyard of registrations nobody uses, on versions that float by construction (part 12).

Read the list as a whole and one property jumps out: almost nothing on it is a defect in Convey. The framework's surface ends at the composition root, and nearly every entry lives in the territory past that edge — the glue, the discipline, the governance that each of ten consumers had to supply alone, ten times. The tax was never the thirty lines. The tax is the next three hundred, per service, forever.

Where Convey.Bootstrap should have existed

So: should there have been a shared kernel? The authors' answer is a deliberate no, and it deserves stating fairly, because it is not laziness — it is doctrine with a scar behind it. Their previous estate's common library, DShop.Common, became exactly the coupling bottleneck the microservices literature warns about, and extracting it into a versioned, published framework is how Convey came to exist. Having paid once to turn a shared kernel into a product, the authors chose repo autonomy for everything that remained — and, to their enormous credit, published the consequence in full rather than hiding it.

But the hash experiment gives the counter-argument its teeth. The estate did not actually avoid a shared kernel. It has one: MessageBroker.cs, the context classes, the outbox decorators, the exception-mapper skeletons, the forwarding helpers — seven-plus copies, coordinated by nothing. The choice was never “shared kernel or no shared kernel”; it was “versioned shared kernel or invisible one” — and the invisible kernel has every cost of coupling (a fix must visit every repo) with none of the benefits of versioning (no upgrade path, no diff, no release notes, no way to even know the copies diverged). A ten-repo Shotgun Surgery per bug fix is not autonomy; it is a distribution mechanism with the version control removed.

The middle path existed, even in 2020, and this is where a Convey.Bootstrap meta-layer should have stood — any one of three shapes would have done. A meta-package: one AddPaccoStack() owning the standard 18-step chain, the broker glue, and the decorators, versioned like any other dependency. Or a dotnet new template: the copies still happen, but they become deliberate, generated, and diffable against the template that produced them. Or, at minimum, a glue-only shared package — infrastructure classes exclusively, no domain types, which is precisely the boundary that keeps a shared kernel from becoming DShop.Common again. The estate's own evidence says the consumers always wanted the same eighteen things; a framework whose every consumer writes the same three hundred lines has drawn its API boundary one layer too low.

What a consumer should copy

The estate is MIT-licensed teaching material, so end where a reader should: with what to take. Copy these, with both hands:

  • The [Decorator] + Scrutor seam — open-generic decoration of every handler, cleanly excluded from the framework's own scan. The best composition-root hygiene in the estate.
  • Availability's EventProcessor — the one service that extracted the load-then-mutate-then-map-then-publish tail every other service repeats inline in every handler.
  • Exception-to-message mapping as declarative tables — errors-as-data on both the HTTP and bus channels, readable at a glance.
  • [Contract] + UsePublicContracts<> — a runtime contract endpoint for near-zero effort; underrated then, underrated now.
  • The .rest scenario files — executable onboarding that documents the estate's actual flows better than any README.
  • The saga-header idea — process state riding message headers is genuinely clever; steal it with the string promoted to a shared, documented constant rather than a seven-times-copied literal.

And refuse these, by name: floating wildcards as a coherence strategy; CI steps that test nothing and stay green; fire-and-forget index creation inside a disposed scope; base classes distributed by paste; and any method whose name promises more intelligence than its body contains.

The domain and estate arcs closed their own books in rich rules, demo-grade data and Cartography of a Successor; this one closes the consumer's. My verdict, for the record: I would build on Convey's 2020 self for a teaching estate without hesitation, and for a production estate I would adopt the chassis and budget, on day one, for the meta-layer it does not ship — because this series just watched ten well-built services pay for its absence in md5.

One rule of thumb to carry out of the whole arc: before you adopt any chassis, write down the files you expect to copy between your first two services. That list is your real framework — and someone must own it, version it, and ship it, or you will meet it again years later, seven copies deep, three meanings wide, in somebody's audit.

My thanks to DevMentors — Piotr Gankiewicz and Dariusz Pawlukiewicz — for publishing, under an MIT licence at github.com/devmentors, the thing most organisations hide: the full, honest, reproducible consequence of a framework decision carried across an entire estate. Reading it taught me more than any style guide could. If your team is one framework decision away from its own eleventh service, start your audit where this series started: hash your copies.