Skip to content
Kumar Chandrachooda
Microservices

Rich Domain, Honest Stubs

A retrospective on nine DShop services - the order aggregate and checkout saga that genuinely work, sitting next to the discount that only prints, the emails that never send, and a currency guard that guards nothing. Part 15 of Nine Services and a Message Bus.

By Kumar Chandrachooda 24 Oct 2025 6 min read
A solid core ringed by placeholders left honestly unfinished

Fourteen parts of reading nine services leaves you with a clear double impression, and the two halves are worth holding together rather than averaging into a grade. DShop has a genuinely rich core — a real domain aggregate, a real saga, a real event-driven topology — surrounded by placeholders that were left honestly unfinished rather than faked. This retrospective is about the seam between the two, because the seam is where the estate is most useful as a teacher: it shows you what “done” looks like next to what “stubbed” looks like, in the same codebase, with nothing pretending otherwise.

What the estate genuinely gets right

Start with the credit, because it is real. The Order aggregate is a proper domain object, not an anaemic bag of setters. It enforces its invariants in its constructor and its transitions:

public Order(Guid id, Guid customerId, IEnumerable<OrderItem> items, string currency) : base(id)
{
    if (items == null || !items.Any())
        throw new DShopException("cannot_create_empty_order", /* ... */);
    // ...
    Status = OrderStatus.Created;
    TotalAmount = Items.Sum(i => i.TotalPrice);
}

Approve, Complete, Cancel, and Revoke are guarded state transitions — you cannot complete an order that was never approved, cannot cancel one already completed. The status is a real state machine with the illegal moves closed off by exceptions. This is the one place in the estate that models a domain the way domain-driven design intends, and it holds up.

The checkout saga (Part 6) is a clean process manager: reserve, approve, and compensate in reverse when a step fails, with the workflow legible in one file. The reflection orchestrator (Part 8) reduces “react to every message in the estate” to two handler classes and a reflective subscribe — the right tool used well. The event-driven topology itself works: namespace-per-service exchanges, competing consumers, the routing convention that turns a class name into an address. And the estate is a faithful teaching artifact — the ECST fan-out, the CQRS read side, the stateful discount saga are all recognisable, well-shaped versions of patterns you meet in production. As a machine for showing you what these patterns are, DShop is excellent.

The honest ledger, collected

Now the other half, gathered in one place, because a retrospective earns its keep by refusing to soften the list.

  • The discount that only prints. The stateful saga's whole reward, CreateOrderDiscount, is handled by a Console.WriteLine (Part 7). No discount is ever persisted or applied. The coordination is beautiful; the effect is a print statement.
  • The email that never sends — but only sometimes. Notifications has two email paths, and they diverge. OrderCreatedHandler genuinely builds a message and sends it over SMTP. SendEmailNotificationHandler — the command the discount saga would trigger — only logs, with the real work left as comments (// Publish: EmailSent). So the order-confirmation email works and the saga-driven email is a stub, in the same service.
  • The currency guard that guards nothing. Order's constructor rejects an invalid currency — good defensive design — but every order is constructed with a hardcoded "USD": new Order(command.Id, command.CustomerId, items, "USD"). The guard can never fire, because the only value that reaches it is always valid. A validated field with a single hardcoded source is validation theatre.
  • The dead branch inside a working method. Order.Complete() guards if (Status != Approved) throw, then runs a switch whose cases are Canceled, Revoked, Completed — every one of which the guard has already excluded. The switch is unreachable; the method works only because its live path is the default.
  • The operation that is never pending. The OperationPending event is defined, deliberately excluded from self-subscription, subscribed to by Signalr, handled by the browser — and never published, because the one line that would publish it was never written (Part 8).
  • The read model that was planned, not built. Storage subscribes to SignedUp with an empty handler (Part 10) — a user projection that got a live subscription and no body.

Line them up and they share a character. Every stub in DShop is honest — a Console.WriteLine, a TODO, an empty handler — never a fake that pretends to work. That is genuinely to the estate's credit as a teaching tool: it leaves the seams visible. The failure mode it models is not deception; it is the very common one where the interesting problem (the saga, the coordination, the topology) gets lavish attention and the boring problem (the effect, the persistence, the send) gets a placeholder — and then the placeholder ships, because nothing downstream complains about a stub that returns cleanly.

The two honest absences

Two disciplines are simply not here, and each deserves exactly one sentence. Analytics is a single counter: Discounts increments an App.Metrics find-discounts counter in FindDiscountsHandler, and that is the entire analytics surface of a nine-service e-commerce estate — there is no serving layer, no metric that a business would read. Machine learning is absent entirely: the closest thing to a model is the discount saga's hand-coded “within 24 hours” rule, which is a constant and an if, not a learned anything. Both absences are fine — an e-commerce teaching estate owes you neither — but they are worth naming so no reader imagines a data layer that was never written.

When this shape helps, and when it doesn't

The architecture DShop demonstrates — services per bounded context, a bus, sagas for cross-service workflows, a read side of its own — is the right shape when you genuinely have independent teams, independent scaling needs, and workflows that span services. It is emphatically the wrong shape for a shop this size run by one team: nine deployables, nine databases, contract copies to keep in sync, and an eventual-consistency window on every read, to sell products a single Postgres and a monolith would serve with less ceremony and more safety. The estate half-admits this by also shipping the same shop as a monolith — the two builds side by side are the subject of the companion series One Shop, Every Shape, and the comparison is the most honest argument in the whole corpus about when microservices pay for themselves.

And the framework underneath it all — DShop.Common — was itself a draft. The same authors rewrote it as Convey, and reading DShop is partly reading Convey's rough draft: the copy-vs-share contract question DShop left unsettled, the idempotent receiver it never grew, the outbox it never had. What the successor kept, renamed, and finally fixed is the companion series' The Rough Draft of Convey.

What I carry away

Three durable lessons, distilled from fifteen parts:

  • A convention is not a contract. The routing key, the copied event, the four representations of a product — all held together by names people typed the same, with nothing enforcing agreement. Everything that drifted, drifted through a convention that no compiler, broker, or test could check.
  • The interesting problem gets built; the boring problem gets stubbed — so guard the boring one. The saga is real and the discount prints; the aggregate is rich and the email logs. Coordination is where attention goes and effects are where value lives, and the estate is a standing reminder to finish the unglamorous half.
  • Honest stubs are a gift; unfinished machinery that pretends to work is a trap. DShop chose the gift every time — a Console.WriteLine you can see is worth more than a fake that hides the gap. Leave your placeholders honest.

That closes the nine-service reading. The estate has one more story to tell — the same shop built as a monolith, fronted by two gateways and two frontends, deployed by a ladder of Dockerfiles — and that is the next series, One Shop, Every Shape, which reads DShop not service by service but as a single system built several ways at once.