Rich Rules, Demo-Grade Data
The series retrospective - Pacco's domain teaches real rules (state machines, priority expropriation, loyalty ladders, compensations) on top of cardboard data (bare-decimal prices, one vehicle per order, size-cube parcels, no payment, and a capacity check that ships in two halves nobody ever connects). What to steal, what to distrust, and what fourteen parts of source-reading add up to.
Part 14 closed the last individual reading in this series. This part is the ledger. Fourteen articles ago I claimed that Pacco's order approves itself — that the estate's domain is a web of reactions worth reading closely, bugs included. Having now read it closely, I owe you the settled accounts: what this estate is genuinely excellent at teaching, where its edges cut, and the one deep pattern that runs through everything — the rules are real, and the data is cardboard. Both halves of that sentence are findings, and the gap between them is the most honest thing a sample system has ever shown me about what samples can and cannot teach.
What the estate does well
Credit first, because it is substantial and specific.
The invariants have teeth. Resource.AddReservation (part 3) is the best invariant-in-an-aggregate teaching example I know in public .NET code: a real business rule — higher priority expropriates the incumbent — enforced in one method, emitting a compensating ReservationCanceled as a first-class domain event. Cancellation as a normal business outcome, not an error path, and the whole estate downstream is shaped to absorb it.
The state machines are honest. Order guards every transition and re-approval clears the cancellation reason (part 4); Customer walks Incomplete → Valid/Locked/Suspicious with an event carrying both previous and new state (part 9). And the boundary translation — domain fact CustomerRegistrationCompleted becoming integration fact CustomerCreated, because to other contexts a customer only exists once registered — is ubiquitous-language discipline in three lines of EventMapper.
The choreography actually choreographs. The reservation conversation (part 5) runs end to end — assign, reserve, react, approve; expropriate, cancel, compensate — with no process manager anywhere in the domain services. Fourteen parts in, I can report the thesis held: the order really does choreograph itself.
The estate teaches by contrast. The same framework hosts three maturities of domain model (part 2); one upstream feeds three borrowing strategies (part 10); the CQRS split is legible at the transport layer (part 12); and Pricing is a standing argument for when not to use the bus. Whether or not every contrast is intentional, a reader who diffs Parcels against Availability learns more about DDD's cost-benefit curve than most books manage.
The sharp edges, collected
The other column of the ledger, one line per scar, each verified in source in its own part:
| Edge | Where it lives | Part |
|---|---|---|
DeleteParcel publishes a fact the aggregate never performed; Parcels detaches one parcel of many |
Order.DeleteParcel, OrderCanceledHandler |
8 |
Failed reserve announced as ReleaseResourceReservationRejected; dead and mismatched rejection mappings |
both ExceptionToMessageMappers |
7 |
Choreography correlated by (vehicleId, date) natural key — the key is the contract |
ResourceReservedHandler |
6 |
| Outbox atomicity switched off estate-wide; three handlers throw on duplicate delivery | seven appsettings.json, three handlers |
11 |
| Version-filtered replace with the result discarded; versioning absent two folders away | ResourcesMongoRepository, three AggregateRoots |
13 |
| A four-service synchronous chain on the write path; retries that ignore 500s; the null that NREs | AssignVehicleToOrderHandler, Convey HTTP |
14 |
Reading them as a set, a pattern emerges that no single part could show: almost every defect lives at a seam. Aggregate to document, domain event to integration event, exception to rejected event, service to service. The aggregates themselves are largely correct; it is the translations that rot. That is worth generalising, because it matches my production experience precisely — model code gets reviewed as a unit, mappers get reviewed as plumbing, and plumbing is where facts quietly stop being facts.
Demo-grade data, itemised
Now the other half of the thesis. Underneath rules realer than many production systems, every actual value in this estate is a placeholder, and the gaps compose into a shadow inventory of what a real parcel marketplace would need:
- Money is a bare
decimal.TotalPrice,PricePerService, the discount ladder — no currency, no rounding policy, noMoneytype, and a log line that labels a fraction as dollars. The first international vehicle breaks every price in the system silently. - An address is one string.
Customer.Address, validated as non-whitespace and never parsed — in a delivery domain, where the address is arguably the most load-bearing datum in the business. - A parcel is a cube. Six sizes, side lengths 10 cm to 2 m, volume as side³ — no weight, no dimensions, no irregular shapes, in a domain whose vehicles declare
PayloadCapacity. - An order holds one vehicle.
Guid? VehicleId— no multi-leg deliveries, no vehicle changes mid-flight, and the reservation is date-granular, so a van is booked by the day. - Nobody ever pays. There is no payment service among the thirteen repos, no payment step in the saga, no invoice, no refund to compensate. In a series where compensation logic featured in five parts, the money-shaped reason compensation exists in real systems is entirely absent.
- The capacity check ships in two halves that never meet. This one is my favourite, because it is a defect you can only see from the whole-estate altitude: Vehicles models
PayloadCapacityandLoadingCapacityand validates them at construction; Parcels exposesGET parcels/volume, a purpose-built endpoint computing total volume for a parcel set — and grep the estate: no service calls it. Both halves of “will these parcels fit in this van?” were built; the sentence was never assembled. An order for six Exclusive cubes assigns a scooter without complaint.
And two absences the honest-lens rule obliges me to record even though there is nothing to read: there is no analytics anywhere — not a report, not an aggregate query, in a marketplace that would live or die by utilisation rates — and nothing resembling machine learning in the domain services (the estate's one self-styled “AI” component lives in the saga client, outside this series' borders, and is best enjoyed unspoiled).
None of this is a scandal — it is a choice, and I think it is the right one for a teaching estate. Rich data multiplies fixture noise and buries the patterns; Pacco spends its complexity budget on rules, which is where the transferable lessons are. But the choice has a consequence for the reader: a sample system teaches its rules and hides its data problems, and data problems — currency, addresses, partial capacity, refunds — are half of what makes real domain modelling hard. Steal Pacco's shapes; do not mistake its silence about data for evidence that data is easy.
What I would do differently
If I were maintaining this estate — the standing thought experiment of every source-reading series — four changes, in priority order:
- Make events facts again. Fix
DeleteParcel's missingRemove, make Parcels detach all parcels of a dead order, and check theReplaceOneAsyncresult before theEventProcessorruns. Three small diffs; they close the estate's three ways of announcing things that did not happen. - Decide what a duplicate means. Either the inbox is the idempotency story (then handlers no-op on repeats) or the handlers are (then they must be tolerant); today each half assumes the other (part 11).
- Name the rejection contract. A rejected event per command, ids mandatory, mapper coverage tested — part 7 showed how cheaply the vocabulary decays without a rule.
- Assemble the capacity sentence. One call from
AssignVehicleToOrderHandlertoparcels/volumeagainst the vehicle's capacity would turn two ornamental features into the estate's best cross-service invariant — and would justify the synchronous hop far better than the price refresh does.
When not to read Pacco this way
Fairness demands the counter-frame. This estate is a circa-2020 netcoreapp3.1 teaching artefact by two educators, published under MIT, floating on 0.4.* wildcards, and never meant to survive a code review conducted seven years later at article length. Every defect this series documented is the kind that ships when your deadline is a workshop, not a marketplace. What earns DevMentors genuine credit is that the estate is good enough to be worth auditing — anaemic samples generate no findings because they attempt nothing. Pacco attempts real invariants, real compensations, real consistency trade-offs, and lands most of them. The bugs are the tuition, and it is the cheapest tuition in distributed systems: reading this estate with a debugger costs a weekend and teaches failure modes that cost production teams quarters.
So: read it. Run it. Break the reservation flow on purpose. And read its two companion stories, because the domain is only a third of what these thirteen repos teach — Two Gateways and a Nervous System reads the estate as a system of systems, and The Convey Consumer Tax reads it as a framework consumer's bill of costs.
The order that choreographs itself turned out to be exactly that — created by a command, grown by reactions, approved by a reservation, cancelled by a priority it never saw coming, and occasionally lied to by its own outbox. If your services react to each other's facts, somewhere in your estate there is a mapper turning a thing that happened into a thing that didn't. Pacco's gift is showing you, in thirteen public repos, every place to look.