Cartography of an Estate
The retrospective that closes three series on DShop - the whole map drawn once, the services wired into nothing, the skew across seventeen repos, and what reading someone else's estate end to end actually teaches.
Three series and forty-seven articles ago, this corpus set out to read one estate completely: the DevMentors DShop, the same e-commerce shop built as a monolith and as nine microservices, over a shared kernel its authors later shipped as Convey. The Framework Before the Framework read the kernel. Nine Services and a Message Bus read the services. This series read everything else — the two builds, the two gateways, the two frontends, the platform. This is where the three maps get laid on top of each other, because the most useful view of DShop is the one no single repository can show you: the estate as one object, with its roads and its dead ends drawn honestly.
The map, drawn once
Here is the whole topology in a paragraph. A request enters through DShop.Api, the gateway. Reads are forwarded over HTTP to the service that owns the data; writes are published to RabbitMQ as commands, and the caller gets a 202 and an operation id. The bus routes each command by namespace to its service. Placing an order kicks off a saga in Operations, which orchestrates a reservation across Orders and Products and, when it finishes, emits an operation event. A SignalR service subscribes to those events to push results to the browser — a channel no frontend connects to. Each service owns its own Mongo; Storage keeps a denormalised read copy of nearly everything; Redis holds the token deny-list and the SignalR backplane. Consul does discovery, Fabio load-balances, Vault holds secrets, Jaeger traces, Serilog logs, App.Metrics counts. Two frontends — an Angular SPA and a Blazor WASM app — sit over the gateway. And off to one side, complete and abandoned, the monolith that does all of it in one process.
That is a genuinely rich distributed system. Drawn on a whiteboard it would look like a reference architecture. The value of having read it is knowing where the whiteboard lies.
The roads that dead-end
The estate's most honest lesson is in its absences, and absences are only visible from the estate view. Three stand out.
Two services are wired into nothing. Storage and Discounts are real, working services — Storage is the read-model sink for the whole estate; Discounts holds the one genuine piece of integration-tested logic in the services. Yet neither appears in the umbrella's clone script, and neither appears in any compose file. The clone-all script names eleven repositories; docker-compose.yml starts eight; the estate has seventeen. Storage and Discounts were added after the umbrella's picture of the world was frozen, and nobody went back to update it. If you followed the run-book faithfully, you would never start them — the documentation describes a smaller estate than the one that exists.
The push channel has no client. A whole SignalR service — JWT handshake, per-user groups, Redis backplane, bus-to-socket bridge — broadcasts to browsers that never connect, while both frontends Task.Delay(1000) through eventual consistency instead. One of the three operation events it fans out is never even published. Every component works; the wiring between them doesn't exist.
The contracts are copied, not shared. There is a DShop.Messages package that could be the single shared source of every message contract. It exists, it's referenced by exactly one service (Storage), and everyone else re-declares each event locally — which is how OrderCreated ends up with three different shapes across the estate, silently dropping fields at the boundaries. The shared-contract solution was built, frozen mid-2018, and abandoned in favour of copy-paste. The package is a tombstone marking where a decision was made and reversed.
The skew nobody reconciled
Read the seventeen repositories' target frameworks and build configs side by side and the estate never once agreed with itself about when it was:
| Unit | Target | Era |
|---|---|---|
DShop.Api (gateway) |
netcoreapp2.2 |
Feb 2018 → May 2019 |
DShop.Api.Next (Ntrada) |
2.1 (runtime image 2.2) |
Nov 2018 → Jun 2019 |
DShop.Monolith |
netcoreapp2.1 |
died Jun 2018 |
DShop.Web (Angular) |
Angular 6 | frozen Jun 2018 |
DShop.Blazor |
Blazor 0.7.0 / netstandard2.0 |
Dec 2018 → Apr 2019 |
The base images straddle the microsoft/dotnet → mcr.microsoft.com registry migration mid-estate. The Ntrada gateway disagrees with itself inside one small repo — a 2.1 project targeted by a 2.2 runtime image, a Travis config on an older SDK than its sibling. And the deepest finding: DShop never had a single moment when all seventeen repositories were known to work together. There is no tag, no compose file, no commit that says “as of this date, the whole estate builds and runs.” It is a polyrepo estate assembled over fifteen months, each repo moving on its own clock, and the version skew is not a bug someone introduced — it is the default state of many repositories with no orchestrating manifest. That is the honest, useful thing DShop teaches about polyrepo microservices, and you can only learn it by trying to see all of it at once.
The compliance and onboarding footnotes
Two lenses close with a sentence each, which is what they earned. On governance: DShop is scrupulously MIT — a LICENSE in every repository, consistent 2018 DevMentors copyright, and the Blazor app even vendors open-iconic with its licences intact; the only unclear-rights assets are the hotlinked product photos (the Angular app sells hotlinked iPhones, the Blazor app sells hotlinked papayas, off the same Products service). On developer experience: the estate's onboarding is genuinely good where it exists — the README run-book, the .rest files that double as the only end-to-end workflow docs, git-clone-all.sh, the 400-line docker-images.txt ops cheat-sheet — and genuinely thin where it doesn't, with single-line READMEs on the Ntrada gateway, the Blazor app, and the monolith.
What reading an estate teaches
Across three series, the recurring lesson was never about a single clever line. It was about the seams. The bugs that mattered in DShop almost all lived between components, where no single repository's tests could see them — the contract that drops a field at the boundary, the push channel with no client, the compose file that defeats the gateway's auth, the metrics setting that silently deregisters a service, the two gateways that disagree about whether a price can have cents. Each component, read alone, looks fine. The estate, read whole, is a catalogue of things that are individually correct and collectively broken.
To be fair to DShop, finally and completely: it is a teaching estate, and it succeeded at teaching — including, richly, by leaving its scars on. It is more honest than most production code precisely because it is unfinished: the NotImplementedException in the monolith's buy path, the design debate committed as an exception message, the auto-signup, the Task.Delay — these are not embarrassments to hide but the exact artefacts that make the estate worth reading. Its authors went on to take the best ideas here — the AddX/UseX chassis, the 202-operation contract, the saga orchestration — refine them, name them, fix the reflection and the lifetimes, and ship them as Convey. DShop is the sketchbook, and you can watch it become the painting.
The last thing the estate teaches is a habit, not a fact. When you inherit a system built by people who have moved on, do not trust the diagram and do not trust the README — both describe the system someone intended. Read the code, run the paths, follow one feature end to end, and pay closest attention to the wires between the boxes, because that is where the intention and the reality come apart. DShop was built twice, documented once, and finished never, and reading all of it is the closest thing I know to a field guide for the estates you will actually be handed. The map is only as true as the roads you walk.