Nine Services and a Message Bus
DShop is one e-commerce shop stamped out as nine microservices over RabbitMQ - this series reads the real source to see how the topology holds together and where it drifts. Part 1 of Nine Services and a Message Bus.
Most microservice tutorials show you two services and a happy path. The interesting failures only appear at the seams — when the fourth service copies the third service's event contract slightly wrong, when the orchestrator subscribes to everything and one of the everythings is never published, when the read side targets a different framework version than the write side and only compiles against a frozen package feed. You need a whole estate before those failures have room to happen.
DShop — the open-source teaching estate by DevMentors (Piotr Gankiewicz and Dariusz Pawlukiewicz), MIT-licensed and © 2018, at github.com/devmentors — is that whole estate. It is a small e-commerce shop built twice: once as a monolith, once as nine microservices, over a hand-rolled shared framework the same authors later rewrote as Convey. This series reads the nine-service build. I did not write DShop; I am a source-reader, working through the actual repositories commit by commit. Where the implementation is the story I quote it, attributed and with the real class and file names — that is the point of reading real production-shaped code rather than a sketch. Everything else is fresh illustrative code.
Nine boxes and what they own
Here is the whole estate, one row per service. Each is its own repository, its own deployable, its own MongoDB database — the sharp version of “database per service.”
| Service | Owns | Storage | Talks by |
|---|---|---|---|
| Identity | sign-up, sign-in, JWT issue + refresh | Mongo + Redis | REST in, events out |
| Customers | customer profile, cart, a local product replica | Mongo | commands + events |
| Products | catalogue, stock reserve/release | Mongo | commands + events |
| Orders | the order aggregate and its state machine | Mongo | commands + events + REST |
| Discounts | per-customer discount codes | Mongo | commands + events + REST |
| Notifications | email side-effects | none (SMTP out) | command + one event |
| Operations | saga orchestration + operation status | Redis + saga store | everything |
| Signalr | real-time push to the browser | none (Redis backplane) | events in, WebSocket out |
| Storage | the read side — denormalised read models | Mongo + Redis | events in, REST out |
Read that column of “talks by” values top to bottom and the architecture announces itself. Most services both send and receive on the bus. Two are lopsided: Identity takes HTTP in and only emits events — it never subscribes to a single message. Storage is the mirror image — it subscribes to almost every event in the estate and emits nothing, because it is the query side of a CQRS split that runs across a service boundary. And Operations subscribes to literally everything, by reflection, because it is the orchestrator.
Three services define the shape of the whole system: Operations is the hub, Storage is the sink, Signalr is the edge. Everything else is a spoke that owns one bounded context and gossips about it.
The bus underneath
Between the boxes sits RabbitMQ, and the estate's whole routing topology is a naming convention rather than a configuration file. Each service is a RabbitMQ namespace with its own topic exchange; a command is routed point-to-point to the service that owns it; an event fans out to every service that has declared interest. Queue names are built from the assembly and the message type, so two instances of the same service compete for the same queue — competing consumers, for free, by naming.
I am not going to re-derive that mechanism here, because it lives in the shared framework rather than in any one service, and the companion series pulls it apart line by line: the convention that turns a class name into an exchange, a routing key, and a queue name is Queue Names Are the Whole Topology. What matters for this series is that the convention is the contract. There is no schema registry, no broker-side validation, nothing that checks that the OrderCreated one service publishes is the OrderCreated another service consumes. The routing key is derived from the class name and the namespace, and as long as those two strings match, RabbitMQ delivers. What is inside the envelope is a separate promise that nothing enforces — which is exactly where this series finds its best material.
One template, stamped nine times
Open any two of the nine repositories side by side and they are eerily similar. The same Program.cs with UseLogging().UseVault().UseAppMetrics(), the same Startup.cs registering Autofac and Mongo and RabbitMQ in the same order, the same Domain / Handlers / Messages / Repositories / Queries folders. This is a template that was stamped out nine times, and the uniformity is the point of the teaching estate — learn it once, read it nine times.
But a template stamped by hand drifts. One service scans a different assembly than the other eight. One seals its handler classes; the next never does. One builds its rejected events manually where the rest let the bus do it. The “advanced” Dockerfile has the same copy-paste bug in all seven services that carry it. The drift is not noise — it is a fossil record of nine slightly different afternoons, and reading it teaches you more about the framework than the uniform parts do. That catalogue is One Template, Nine Times.
The shared kernel, and its tombstone
All nine services sit on DShop.Common, the shared framework — messaging, dispatchers, Mongo helpers, service discovery, JWT. There is also a second shared package, DShop.Messages, that was supposed to be the canonical contract library: one place where OrderCreated is defined, so every service agrees on its shape. It did not take. Exactly one service — Storage — still references it. The other eight hand-copy every event contract they consume into their own Messages/Events folder. DShop.Messages is a tombstone: a shared-contracts package frozen mid-2018 when the estate quietly pivoted to per-service copies. Both answers to “who owns the contract” survive in the same estate, side by side, and you can read the disease and the abandoned cure in one afternoon.
Where the series goes
Fifteen parts, following the bus:
- Nine services and a message bus — this post: the topology and the players.
- One template, nine times — the uniform skeleton and its drift catalogue.
- OrderCreated in three shapes — the contract drift that silently drops a field.
- Copy the contract or share it — the two answers to the shared-kernel question.
- Two ways to get another service's data — state transfer vs notification-then-fetch.
- The saga that orchestrates a checkout — reserve, approve, and compensate.
- A saga with a memory — the stateful discount saga and its 24-hour rule.
- One orchestrator subscribes to everything — Operations by reflection.
- Idempotency aspired, not enforced — competing consumers with no dedup.
- CQRS across a service boundary — Storage as the read side.
- The version-skew time capsule — the one service that only builds in Release.
- Four products, one truth — product identity as un-mastered master data.
- Identity done mostly right, secrets done wrong — the JWT flow and the committed key.
- The test folder that lies — a coverage census across nine services.
- Rich domain, honest stubs — the retrospective.
If you have ever inherited a fleet of services that were “all the same” until the day one of them wasn't, the next fourteen parts are a guided tour of how that happens. We start where the estate starts: with one template, stamped nine times.