What Strategy-First Actually Bought Them
An honest retrospective on fifteen parts of source-reading - what the decision records bought, what they could not buy, and the compliance gaps nobody wrote down.
Fifteen parts ago I said the thesis of this series was testable: if strategy really came first, the fingerprints of a written decision should be visible in the compiler-checked structure of the solution. Part 15 finished the last of the evidence. This part is the verdict, and it has two columns, both of them full.
The estate under review is DevMentors' GroupFlights at commit a19b337 — 439 C# files, 14,559 lines, thirty-five projects, four ADRs, three commits, and no licence file. The disclaimer from part 1 still stands: I did not write it, I have read it, and everything here comes from the source rather than the README.
What it bought
A traceable line from a decision to code, nine steps deep. ADR 04 extracted the reservation-change process into its own bounded context and enumerated ten steps of the resulting flow. Nine of them are real code you can put your finger on, and part 10 walks the trace. That is the strongest single piece of evidence for the thesis, and I have not seen it done better in any repository of this size. The value is not that the ADR was right; it is that a stranger can score it. Every no in that table is a specific conversation with a specific file attached.
Zero boundary violations across thirty-five projects. ADR 01 chose a verbal prohibition over an automated architecture test and named the condition for upgrading. It held perfectly: every cross-module ProjectReference in the estate terminates at a <Module>.Shared contract project. The agreement worked because a convention made the illegal path awkward rather than merely forbidden — the interface goes in .Shared, the implementation goes in ModuleApi/, and every sibling module does it that way. Part 6 has the detail.
A published surface you can read in one sitting. Seven contract projects, fifteen operations, thirteen event types, under four hundred lines total. Whatever else is wrong with that surface — and part 8 found seven of thirteen events talking only to themselves — its smallness is a real achievement and a direct consequence of deciding, up front, that shared contracts were the only legal edge.
Proportional architecture per module. Three application architectures chosen by problem complexity, so nobody paid Clean Architecture tax on a module that stores a payer's tax number. Part 2 also found the inversion: the module the README never singles out holds the estate's cleanest aggregate.
One relationship implemented exactly as drawn. Sales ↔ Postsale is textbook Supplier-Customer: ISalesApi exposes precisely one command and one query for Postsale's benefit, Sales replies with an integration event, and neither side knows the other's internals. And the two anti-corruption layers on the map are drawn only where the line leaves the process — which is exactly where an ACL earns its keep.
What it could not buy
A map that stays current. Three of ten boxes were never built (part 5), a Conformist edge is a cycle in the code, a Partnership is one-way RPC, and one Open Host edge is switched off at runtime behind var enabled = false; (part 14). Four of nine edges match. A drawing has no compile step, and nothing in the estate closes the loop between the drawing and the build. The technique in part 4 closes it in twenty minutes; nobody ran it.
An enforced schema boundary. Seven modules, seven Postgres schemas, no cross-schema joins — and one superuser connection string handed to all of them. The assembly boundary got an ADR with three enforcement options weighed against each other. The database boundary got no ADR and therefore no considered choice; it inherited a default. Discipline that is real at one layer and absent at another is the normal shape of these systems, and the reason is always the same: somebody thought hard about the layer they wrote a document for.
Consistency where no decision was written. Correlation identifiers appear three times in three forms, one of them a static Dictionary with a TODO (part 12). Selective-consumer guards appear on two of four broadcast handlers (part 13). A published contract type ends up as columns in another module's schema (part 9). None of these has an ADR, and that is the pattern: the four things the team wrote decisions about are the four things that came out consistent.
A mitigation for the one risk it named. ADR 04 identified “I change a reservation on the basis of stale information”, built a real snapshot table in response, and gave it no version, no timestamp and no concurrency check (part 11). A half-built mitigation is worse than none, because it retires the concern.
The compliance ledger
Four things belong here that no ADR mentions, and they are the ones I would raise first if this were a production system rather than a course companion.
There is no licence. No LICENSE, no SPDX header, no PackageLicenseExpression. Public on GitHub means viewable and forkable under the platform's terms; all other rights are reserved. Read it, cite it, learn from it — do not vendor it. This constrained the series too, which is why almost every code block here has been fresh illustrative C# rather than the estate's.
Personal data goes to the application log at Information level, on every dispatch. EventDispatcher.cs:75-76 serialises every event as indented JSON before dispatching it. ContractGenerationRequestedIntegrationEvent carries a signee's name and surname plus the full itinerary; DeadlineRequestedIntegrationEvent carries participant e-mail addresses. Client names and e-mails land in the log on every publish. In a teaching repository with invented data this is a wire tap and a good one — it is how a reader sees the choreography happen. In production it is a data-protection incident with a five-second heartbeat, and the fix is a redacting serialiser, not a lower log level.
There is no retention of anything. No soft delete, no TTL, no purge job, no anonymisation. Passenger identity-document details land in sales."Reservations" and stay there. The only deletion path in the estate is scripts/truncate-everything.sql, a developer convenience that truncates fifteen tables across seven schemas.
Documents.Owner is written and never checked. Backoffice.Core/Services/DocumentService.cs:42 stamps the signee's user id onto a generated contract, and line 69 stamps the uploader's onto the signed copy. The download path is three lines:
public async Task<DocumentFileDto> DownloadContractFile(Guid contractId, CancellationToken cancellationToken)
{
var document = await _fileRepository.GetFileByContractId(contractId, cancellationToken);
return new DocumentFileDto(document.Content, document.Name);
}
DocumentService.cs:46-50, GroupFlights at commit a19b337. No Owner comparison. Any authenticated client with a contract GUID can download any contract. The field exists, the intent is legible, and the check was never written — which is the same shape as OnPaymentOverdue() having zero callers. The domain model keeps getting this right and the layer above it keeps forgetting to ask.
To be fair, the README declares that authentication and authorisation are not implemented, and an ownership check on top of a header-based identity stub would be theatre. But the field is there, and one if would have made the intent checkable by the next reader.
The rule this series was really about
Nine bullets in the README declare what the system does not do. Every one of them turns out to be defensible — the console-logging email stub makes a mechanism visible, six test methods stage a contrast the README explains, the fake payment gateway with its five-second auto-pay is a better teaching device than a real integration would be. Not one of the declared omissions is a defect once you read the declaration.
And every serious problem in these fifteen parts is undeclared. The disabled decorator. The overdue handler calling OnChangePayed(). The in-memory payment registry. The missing snapshot version. The unchecked document owner. Nobody wrote any of them down, which is exactly why they read as bugs rather than as decisions.
A declared omission is a design decision; an undeclared one is a bug. That is the rule I would take from this estate into any codebase, and it cuts both ways — it is a defence of teaching code against unfair criticism, and it is an obligation on anyone shipping something incomplete. The cost of declaring is one line in a README. The cost of not declaring is that your reader, your reviewer and your future self cannot tell the difference between a choice and an oversight.
Who should read this repository
Read it if you are about to split a bounded context and want a worked example with the reasoning attached; if you want to see three application architectures justified per module rather than mandated estate-wide; if you have never seen an ADR with an approver's name on it; or if you want to run the derived-context-map technique on something before you run it on your own estate.
Do not read it as a template. It is net6.0, three commits old, has no CI, no Dockerfile, no health checks, no outbox and no transactions anywhere in thirty-five projects. It is not licensed for reuse. And its most-copied idiom — a static field bridging two instances of a hosted service — is load-bearing for reasons you would not want to inherit.
What to steal: the EndpointRegistration(Pattern, Verb, AccessControl, Handler) record, so a module cannot register a route without declaring who may call it. The EventHandlers/Internal versus External folder split. The <Module>.Shared-only rule, with the test part 7 writes for it. And the ADR template with five more fields on it.
The strategic half ends here. The tactical half — three persistence strategies for one domain, a fully encapsulated aggregate you cannot write a getter against, and a dispatcher whose delivery semantics are at-most-once and sometimes not at all — is the companion series, GroupFlights — Tactics in Three Dialects, and it starts at one domain, three dialects.