Boundaries the Compiler Enforces
Trill's monolith makes every module type internal and gives each one an explicit friend list, so a cross-module reach is not a code-review conversation - it is a build error.
Newest first, grouped by year. Search or filter by topic.
Trill's monolith makes every module type internal and gives each one an explicit friend list, so a cross-module reach is not a code-review conversation - it is a build error.
DevMentors shipped Trill as nine microservice repos, then rebuilt the identical product as a modular monolith three months later - giving us a controlled experiment in architecture that almost never exists in public.
Trill's Blazor WebAssembly client has twenty Razor components, thirty-seven inline style attributes, two authored class names and one stylesheet containing a single rule - and the thinness is not an oversight, it is the most precise measurement of what a teaching repository is.
An honest retrospective on Trill's five services and its client - twelve xunit scaffolds with no test files and no project references, a green test script over nothing, and a defect list where almost every entry would have died to a four-line assertion.
Trill's entire advertising product resolves to one boolean on somebody else's entity - a published ad is a Stories-service story with Highlighted set true - and the flag is priced at a hundred units a day while the same field is free on the public endpoint.
Trill's Timeline service stores each story body once and fans out only the identifier into every follower's sorted set - the right hybrid design, executed as a sequential foreach with one Redis round trip per follower and no batching anywhere.
Trill's entire ranking engine is thirty-nine lines of OrderByDescending().Take(10) over counters that only ever increase - no time window, no decay, no visibility filter - so once the top ten stabilises it can never change again.
The same .NET format specifier means thirty-two hex digits on a Guid and a grouped decimal on a long - and Trill's timeline writes its sorted-set members one way then reads them the other, so every non-empty timeline throws.
Trill's User constructor stores the name trimmed but not lower-cased, and the repository looks it up lower-cased. Any account with a capital letter in its name can never be found, which means it can never sign in - and the test fixture is all lowercase.
Trill's Stories service ships a complete caller-identity abstraction - context, factory, claims, an IsAdmin flag - registers it in the container, and injects it into nothing, while every endpoint reads the user id out of the request body instead.
Trill's browse endpoint loads every rating document for every story on the page into process memory and sums them in LINQ - while the same codebase already has a server-side SumAsync that returns the number without moving a single document.
Trill creates one compound index on its ratings collection, keyed userId then storyId, and every read query in the service filters on storyId alone - so the only index that exists cannot serve the only queries that run.
Trill wraps every command handler in logging, metrics, tracing and an outbox using Scrutor's TryDecorate - and the whole arrangement only works because a single attribute keeps the decorators out of the assembly scan that registers handlers.
Trill's last commit built a complete domain-event pipeline - dispatcher, mapper, handlers - and migrated one of the two command handlers onto it. The other still constructs its aggregate directly, so one domain event is never raised and a reputation gate can never fire.
Trill extracts story-text validation into an IStoryTextFactory and leaves the value object's constructor public - plus an implicit conversion from string, so any assignment builds an unvalidated StoryText in one keystroke.
StoryDocument.ToEntity passes the body where the title goes and the title where the body goes. It compiles because of one implicit conversion operator, and it has survived since the first commit because the only code path that would notice is dead.
Trill's Stories service is four projects with a dependency-free domain at the bottom. Its Users service is two projects with everything in one assembly - same author, same week, same estate, opposite answers to what a layer is.
Trill ships Jaeger, Prometheus, Grafana, Seq, Consul and Vault, and contains no circuit breaker, no dead-letter queue, no health check and no alert rule - the closing retrospective on what this estate gets right, what it does not, and when not to build a system this way.
Trill contains a complete synchronous integration architecture - typed gRPC and HTTP clients, a live gRPC server, generated stubs, an options class - that was built, wired and then disabled by commenting out three lines across two repositories.
Trill has no shared contracts package - every consumer re-declares the publisher's event class by hand. Diffing all fourteen duplicated contracts shows exactly two have drifted, and the ratio is what makes copy-paste governance feel safe until it isn't.
Trill's ad-publication saga implements Compensating Action in five overrides and can reach none of them - the rejection events it subscribes to carry no correlating id, their handlers return Task.CompletedTask, and no handler ever throws.
Trill's push service checks its cancellation token after dequeuing rather than before, so a disconnected client wins one more message from the shared queue and destroys it - and the Blazor client that consumes the stream never reconnects.
Trill's push service gates stories on a visibility window and silently discards anything scheduled for later - a Message Filter where a delay was intended, with no scheduler, no delay queue and no re-delivery anywhere in the estate.
Trill's push service fans stories out with a single System.Threading.Channels queue - which means every connected browser competes for the same message instead of all receiving it, and the class name is plural while the field is not.
Trill's gateway declares an authenticatedUser authorisation policy exactly once and attaches it to nothing - and its identity middleware returns early when the Authorization header is absent, so a caller with no token at all can publish commands onto the estate's message bus.