The Monolith and the Swarm
DShop exists as one deployable and as nine services built from the same requirements. Read side by side, the two builds show which decisions were decisions and which were defaults.
The monolith came first. DShop.Monolith is fifteen commits, dead by 25 June 2018 — it predates most of the microservices work, and reading it feels like reading the estate's own first draft of itself. It is the same shop the nine services build: sign up, complete a customer profile, browse products, fill a cart, place an order. But because it is one process with one database, every decision the distributed system had to make about distribution is here made trivially, or not made at all. That contrast is the most useful thing in the whole estate, so this part lays the two builds side by side, axis by axis, from the comparison sketched in part 1.
Structure: four layers versus eleven repos
The monolith is a clean four-project layering: Core (the domain — aggregates, entities, value objects, factories, specifications), Services (the CQRS application layer — commands, handlers, dispatchers), Infrastructure (Mongo, JWT, MailKit), and Api (controllers and middleware). It is netcoreapp2.1, references Microsoft.AspNetCore.App 2.1.1, and boots through the standard host builder with one custom link in the chain:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseLockbox();
The distributed build inverts every dimension of that. There is no Core/Services/Infrastructure/Api layering; there are eleven-plus repositories, each a thin vertical service — Customers, Discounts, Identity, Notifications, Operations, Orders, Products, Signalr, Storage — over the shared DShop.Common kernel. The gateway's boot is a fluent chain of platform concerns instead of one UseLockbox(): .UseLogging().UseVault().UseAppMetrics(). Where the monolith's four layers are compile-time boundaries you cross with a method call, the swarm's boundaries are network boundaries you cross with a message. The monolith's dependencies are references; the swarm's are subscriptions.
Dispatch: a method call versus a message
Inside the monolith, CQRS is real but local. A controller resolves an ICommandDispatcher and hands it a command; the dispatcher resolves the one handler that matches and invokes it — in-process, on the same thread, inside the same request:
public async Task DispatchAsync<T>(T command) where T : ICommand
{
var handler = _context.Resolve<ICommandHandler<T>>();
await handler.HandleAsync(command);
}
The distributed build takes the same command classes — CreateOrder, SignUp, AddProductToCart — and instead of resolving a local handler, publishes them to RabbitMQ, where a topic exchange routes each one to whichever service owns that message type. The command's journey changes completely; its name and shape do not. That substitution is important enough that the next part is entirely about it: decomposition, in this estate, was literally swapping the dispatcher for a bus.
The order path: 200 now versus 202 later
This is where the two builds diverge most visibly to a caller. In the monolith, placing an order is synchronous. The controller dispatches CreateOrder, the handler runs to completion inside the request, and the base controller returns a bare Ok():
protected async Task<IActionResult> DispatchAsync<T>(T command,
Guid? resourceId = null, string resource = "") where T : ICommand
{
await _commandDispatcher.DispatchAsync(command);
return Ok();
}
Look closely at that signature. It already accepts a resourceId and a resource — and it ignores both, returning a naked 200. Those two parameters are a fossil: the monolith's author had begun reaching for the contract the distributed gateway would later formalise — 202 Accepted with an X-Operation header naming the async work — but here the pipeline is synchronous, so there is nothing to name and the parameters go unused. The distributed gateway fills them in, mints the resource id at the edge, and returns 202 with headers pointing at an operation the caller can poll. That whole story is part 8; for now, note that the monolith's controller carries the shape of the async contract without the async.
Data and consistency: one Mongo versus many
The monolith's entire persistence story is ten legible lines — one Mongo database, six collections, registered in one Autofac module:
builder.AddMongoDB();
builder.AddMongoDBRepository<Cart>("Carts");
builder.AddMongoDBRepository<Customer>("Customers");
builder.AddMongoDBRepository<Order>("Orders");
builder.AddMongoDBRepository<Product>("Products");
builder.AddMongoDBRepository<RefreshToken>("RefreshTokens");
builder.AddMongoDBRepository<User>("Users");
That is the who-writes-what map for the entire shop, and because it is one database, a customer and their cart and their orders are always consistent with each other: there is no moment where the customer exists but the cart does not. The distributed build shatters this into a database per service plus Redis, and buys eventual consistency in exchange. When you sign up in the swarm, the customer record is created by a different service reacting to an event some milliseconds later — and the frontends, as we will see, literally Task.Delay(1000) to let that catch up. One Mongo made consistency free; nine Mongos made it a feature you have to engineer, and DShop engineers it incompletely.
Auth and errors: the same code, twice
Some things are identical across the two builds, and the identity is itself a finding. Both use the same HS256 JWT scheme with the same committed secret — a detail that becomes the security inversion in part 9. Both ship an ErrorHandlerMiddleware that funnels every exception to HTTP 400 and serialises the raw exception message. The monolith's version even computes a sanitised message and then throws it away:
var message = "There was an error.";
switch (exception)
{
case ServiceException e:
message = e.Message;
break;
}
var response = new { code = errorCode, message = exception.Message };
The local message variable is set, considered, and then never used — the response serialises exception.Message regardless. This is the same defect the distributed DShop.Common ships, which Series 1 reads in detail. Seeing it in both codebases tells you it was not a slip in one file; it was a habit the authors carried from the monolith into the framework.
The honest summary
Here is the whole comparison, and every row is a link to somewhere this series goes deeper:
| Axis | Monolith | Nine services |
|---|---|---|
| Boundaries | four project layers | eleven-plus repos |
| Dispatch | in-memory Autofac | RabbitMQ topic exchange |
| Write result | 200, synchronous | 202 + operation id |
| Data | one Mongo, six collections | DB-per-service + Redis |
| Consistency | immediate | eventual, UI sleeps for it |
| Customer creation | in-process on sign-up | a client responsibility |
| Auth | shared HS256 secret | same shared secret |
| Errors | everything is 400 | everything is 400 |
| Deploy | image-per-environment | image-per-service, mutable tags |
| Fate | abandoned, buy path broken | incomplete at the edges |
To be fair to both builds: this is teaching code, and a teaching estate that shows the same requirements decided two ways is worth more than a polished example that shows one. The monolith is, if anything, more rigorously domain-driven than the services — it has aggregates with uncommitted events, a real UniqueEmailSpecification, a UserFactory. The distributed build traded some of that domain richness for the operational machinery of distribution. Which trade was right depends entirely on questions the estate never had to answer, because it was a course, not a company.
Next, the substitution at the heart of the decomposition: swap the dispatcher for a bus, where the same CreateOrder command travels two completely different roads.