Skip to content
kc@kumarChandrachooda.com:~$ cd /blog/the-road-out-of-the-monolith && read --section="top" 0%
Architecture

The Road Out of the Monolith

Which of ModularMonolith's three modules could become a service tomorrow, what blocks the other two, and the hardening bill the sample never pays - a retrospective on the modular monolith as a station, not a destination.

By Kumar Chandrachooda 15 Nov 2025 5 min read
One block leaving the outer rectangle along a dotted road

The test of a modular monolith is a question you should be able to answer without opening an editor: if this module had to become a service by Friday, what would it cost? Thirteen parts into reading the DevMentors ModularMonolith estate, that question finally has evidence-backed answers — different for each module, and revealing in each case. This closing part prices the extractions, names what blocks them, tallies the hardening bill, and weighs the estate honestly as the teaching artefact it is.

Extraction readiness, module by module

Speakers is nearest the door. It publishes nothing, subscribes to nothing, and owns its data outright in the speakers schema (part 9). Extraction is almost mechanical: give it a host with its own Startup, dump and restore one schema, keep the speakers-module URL prefix behind a gateway and no client notices. The bill is a bootstrapper, a pipeline, and fixing the return Ok(); bug it would otherwise carry into production. That a module can be this liftable is the architecture working — the walls of part 2 held.

Conferences needs its messaging debts paid first. It publishes the estate's one event on a save-then-publish with no outbox (part 8); in-process, that loses events rarely and silently — over a real network, routinely and silently. Leaving home means a durable broker behind IMessageBroker, an outbox beside its tables, and a decision about the broken contract it publishes into. The abstraction seam is right, which is why the swap is a bounded job rather than a rewrite; the estate's kin shows both halves done properly — Convey's outbox and RabbitMQ by convention.

Tickets cannot be extracted, because it barely exists. Four files, no entity, no schema, an AddCore that returns the collection unchanged (part 3). There is nothing to lift. Its role was always to demonstrate consumption, and its unfinished state accidentally demonstrates something better: in this architecture, a module can be a stub for seven months without destabilising anything around it. Boundaries that tolerate unevenness are good boundaries.

The coupling that blocks all three

One dependency edge undermines every extraction, and it is visible in plain .csproj XML: each module's .Core references Shared.Infrastructure — not just Shared.Abstractions — because that is where AddPostgres<T> lives. Infrastructure bleed: the moment a module leaves the repo, it drags along the mini-framework — controller discovery, the error middleware, the in-memory broker, the module registry — a framework reference for the sake of one database helper. Every path out runs through the same fix: ship Shared.Infrastructure as a NuGet package (the road the DevMentors ecosystem itself took — Convey is this project's shared folder grown up), or invert the dependency so .Core needs only abstractions.

The reference topology also drifts, quietly confirming part 13's fossil reading: Tickets.Core references only Infrastructure, the other two .Cores reference both shared projects, and Conferences.Api references Infrastructure directly while its siblings get it transitively. Same question, three answers, no ADR.

The hardening bill

Absences catalogued across the series, priced as the invoice any production adoption pays: message ids, correlation and versioning on the empty IMessage marker; retries, dead-lettering and persistence for the drop-on-error pump; an outbox under the dual write; contract tests over every copied record; health checks — currently unbuildable anyway, since Database.Migrate() runs inside ConfigureServices and the app dies before any probe could answer; pagination and ProblemDetails on the API surface (part 11); real schema constraints under the empty entity configurations; tests, starting with the empty scaffold; and a CI pipeline plus an app Dockerfile — compose runs only Postgres. Under operations posture, file the trust-auth container and empty database password: fine on a teaching laptop, and precisely the kind of default that outlives its context; the first hardening commit changes both. And everywhere: authentication, of which the estate has none — the vestigial @accessToken in Speakers.rest the only evidence anyone ever thought about it.

That list is long, and it is also the point. The sample implements the architecture completely and the operations not at all — a clean separation that makes it readable, provided somebody tells the learner which half they are looking at. Nobody does; there is no README. That marking gap, more than any defect, is what I would change about this estate.

The retrospective proper

What it gets right. The cheapest real module isolation in .NET — internal controllers, one feature provider, InternalsVisibleTo chains. A module recipe consistent enough that deviations are diagnostic. Boundary decisions that all point the same direction: URL prefixes, schema-per-module, contracts-by-copy, each one a rehearsal for a service split. And the JSON-round-trip translator — the single best teaching device in the estate, because it makes in-process modules fail exactly the way distributed services fail, on the same structural contracts, for the same reasons.

Its sharp edges. Everything load-bearing is implicit: startup order, delivery semantics, contract compatibility, the last-registration-wins repository swap. The estate runs on conventions no compiler checks and no test pins, and parts 4, 7, 8 and 10 each caught one of those conventions failing or fossilising. The pattern behind the pattern: a modular monolith replaces enforced coupling with promised discipline, and every promise here that lacked a checker eventually broke.

When not to take this road. Skip the modular monolith when the modules would share one aggregate anyway — walls through the middle of a transaction just relocate pain; when you already need independent scaling or deployment cadence today, in which case take the microservices lane directly; and at the other extreme, when a small CRUD app would wear the per-module ceremony as dead weight. The station serves travellers whose domain has real seams but whose team and traffic do not yet justify distribution — which is more teams than currently believe it.

What I would do differently. Three additions, none large: a module template plus architecture tests, making the implicit rules mechanical; one contract test per copied event, which converts part 7 from a cautionary tale into a non-event; and honest delivery semantics — an outbox table and at-least-once, or a loud comment declaring at-most-once on purpose.

DevMentors built three stations on one line: the shop as services, the chassis those services stand on, and this — the middle station, one process pretending hard enough to be three that the pretence becomes a migration plan. Eighty-four files, two shipped bugs, one broken contract, and the clearest demonstration I have read that the road out of a monolith is built inside it, wall by wall, while everything still runs.