The Convey Consumer Tax
Convey sells a microservice in thirty lines, and Pacco builds ten of them - this series reads the whole estate to price what the framework compresses and what every consumer pays for by hand. Part 1 of The Convey Consumer Tax.
Every microservices framework demos the same way. Someone puts a whole service on one slide — routing, messaging, persistence, tracing, a database, all composed in thirty lines — and the room nods, because thirty lines is a number you can believe in. What the slide never shows is the eleventh service: the one where you discover which parts of the framework are genuinely shared and which parts you have been quietly rewriting, by hand, in every repository since the second one.
Pacco is the best place I know to see both halves at once. It is a parcel-delivery marketplace built by DevMentors — Piotr Gankiewicz and Dariusz Pawlukiewicz, MIT-licensed at github.com/devmentors — as thirteen sibling repositories: ten backend services, two API gateways, and an umbrella repo that runs the lot. Every service is a consumer of Convey, the framework the same authors distilled out of their earlier sample, DShop, precisely so that nobody would have to copy DShop.Common between solutions again. Pacco is the successor estate: what happens when the extraction has been done, the framework exists, and you build ten real services on top of it.
To be clear up front: I did not write Pacco or Convey. I am a source-reader who has spent weeks inside all thirteen repositories, and this series quotes the actual code, attributed, wherever the implementation is the story; everything else is fresh example code. The estate is frozen circa 2020 — everything targets netcoreapp3.1, hosts on the legacy WebHost, serialises with Newtonsoft — and every Convey package reference floats at 0.4.*, a wildcard, never a pinned version. That era framing matters: this is not a critique of what Convey became, it is a reading of what building on it cost at the moment the sample was written.
This series is the third of three arcs over the same estate — The Order That Choreographs Itself reads the domain and Two Gateways and a Nervous System reads the system of systems; this one reads the bill.
The thirty lines, as actually shipped
Here is the entire Program.cs of Pacco.Services.Vehicles — not a slide, the file from the repository, minus its using directives:
public class Program
{
public static async Task Main(string[] args)
=> await WebHost.CreateDefaultBuilder(args)
.ConfigureServices(services => services
.AddConvey()
.AddWebApi()
.AddApplication()
.AddInfrastructure()
.Build())
.Configure(app => app
.UseInfrastructure()
.UseDispatcherEndpoints(endpoints => endpoints
.Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService<AppOptions>().Name))
.Get<GetVehicle, VehicleDto>("vehicles/{vehicleId}")
.Get<SearchVehicles, PagedResult<VehicleDto>>("vehicles")
.Post<AddVehicle>("vehicles",
afterDispatch: (cmd, ctx) => ctx.Response.Created($"vehicles/{cmd.VehicleId}"))
.Put<UpdateVehicle>("vehicles/{vehicleId}")
.Delete<DeleteVehicle>("vehicles/{vehicleId}")
))
.UseLogging()
.UseVault()
.Build()
.RunAsync();
}
There are no controllers anywhere in the estate. Routes bind directly to command and query types; a lambda names the status code for the one route that needs a non-default one; the rest is convention. When I wrote about the framework itself I called this the chassis you were going to write anyway — and on this evidence the chassis works: ten services, and every one of their Program.cs files reads like this.
That is the pitch, and I want to be plain that the pitch is honest. The surface compression is real. A new Pacco service earns HTTP endpoints, RabbitMQ subscriptions, Mongo persistence, Consul registration, Jaeger tracing, Prometheus metrics, Swagger, and Vault wiring from a fluent chain a junior developer can read top to bottom. Part 3 of this series is an unreserved tour of that surface, because critique that skips the good parts is just complaint.
The tail the slide never shows
But I read all ten services, not one, and the estate keeps a second ledger. A sample of what the following twelve parts verify in source, file by file:
- A class called
MessageBroker.cs— 87 lines of glue between Convey's bus, outbox, correlation context, and tracing — exists in seven repositories, and once you substitute the service name the seven copies hash identically. It is the estate's real framework; it just isn't in the framework. - The
AddInfrastructure()call hiding behind line 9 above expands, in every service, into a chain of seventeen-plus registrations — long enough that Vehicles calls.AddMongo()twice and Operations calls.AddRedis()twice, and nobody ever noticed, because nobody reads boilerplate. - Across ten databases there is exactly one MongoDB index in the entire estate, and it is created fire-and-forget inside a dependency scope that has already been disposed — while cross-service correlation queries scan collections by natural keys on the message hot path.
- Twelve repositories carry Travis CI configuration. Nine of them run
dotnet testagainst zero test projects and stay vacuously green; the one repo with a genuine unit/integration/e2e/performance pyramid is the only one whose CI omits the test step. - The contract story is a folder: consumer-driven Pact files are exchanged through a relative path six directories up, and the operations service subscribes to eighty messages via a hand-maintained JSON manifest that nothing validates.
The thesis of this series is that a framework's real cost lives in what it leaves for every consumer to repeat — Convey optimises the first thirty lines of a service brilliantly and leaves the next three hundred to be copied ten times. None of the items above is a bug in Convey. Every one of them is a decision each consumer had to make alone, ten times, because the framework's surface ends exactly where the glue begins.
To be fair to the authors: the copy-paste is a stance, not an accident. Pacco deliberately keeps each service in its own repository with no shared kernel, because the alternative — a common library that every service references — is precisely the coupling that made DShop's DShop.Common a bottleneck, and extracting that into Convey is how Convey came to exist. The estate chooses repo autonomy and pays for it in duplication, and it makes the price observable, which is exactly what a teaching estate should do. My job in this series is to measure the price, not to sneer at it.
Where the series goes
- The Convey Consumer Tax — this post.
- The Same File in Seven Repos — the copy-paste inventory, measured in md5.
- A Service in Thirty Lines — the framework's genuine win, taken seriously.
- One Index Across Ten Services — honest document modelling meets a missing database discipline.
- The Test Pyramid CI Never Runs — nine green badges over zero tests, and the real pyramid Travis skips.
- Pacts Through a Folder Six Levels Up — consumer-driven contracts without a broker.
- A Shadow Registry of Every Message —
messages.json, genius and trap. - Two Ways to Tell a Service Something Changed — state-carrying events versus id-only notifications.
- The Saga That Rides a Header — OrderMaker's Chronicle orchestration and the header that crosses the estate.
- AI in a Startup — what the “uber AI order maker” actually computes.
- A Smell Audit With Prescriptions — named smells, named refactorings, real lines.
- The Graveyard of Registered Dependencies — Redis in eight services, used in one.
- The Price of No Shared Kernel — the retrospective, with the ledger totalled.
Who this is for
If you have ever adopted a framework because the demo service fitted on a slide, and then found yourself six months later diffing two hand-copied EventBusExtensions.cs files to work out which one has the fix — this estate is your story with the serial numbers left on. Pacco's authors published, under an MIT licence, the thing most organisations hide: the full, honest consequence of a framework decision carried across ten services. Reading it beats any style guide I could write.
Next, the measurement that anchors everything else: the same file in seven repos, where we hash the copies and meet the 87 lines of glue that every Pacco service secretly agrees on.