We put the monolith back together
P6X4 reduced operational complexity by restoring boundaries inside one deployable system instead of defending service count as progress.
P6X4's architecture diagram looked mature. It had fourteen boxes.
I had drawn every box myself.
P6X4 was a personal laboratory assembled from the difficult parts of my earlier projects: documents from M31V, preview-like rendering from R7K1, privacy policy from Q2F8, incident evidence from W93H, and durable effects from Z29C. I separated identity, catalogue, workflow, rendering, delivery, notification, audit, and search into services because I wanted to understand the costs and benefits of distribution from the inside.
The system worked. A document could be created, rendered, indexed, delivered, and traced across asynchronous boundaries. Each service had a small repository, a deployment definition, health checks, structured logs, a database or projection, and versioned messages.
Then I added an optional subtitle.
The change crossed four repositories, three event contracts, a render schema, a search projection, and a coordinated sequence of releases. Nothing about the subtitle needed independent scale or authority. The diagram had made one product idea travel as a distributed protocol.
I did not need a better service mesh. I needed to admit that many boxes had no independent life.
I had optimized for extraction
The design began from a fashionable and partly sensible premise: boundaries are easier to enforce when the network makes violations impossible.
A renderer could not import private document tables if it lived in another repository. A search index could evolve independently. A delivery worker could isolate credentials and retry behavior. Small codebases appeared easier to understand. Each new service created a clean place to begin.
Extraction relieved local discomfort. When a module became awkward, moving it behind HTTP or a queue replaced tangled calls with a documented payload. The source repository became smaller. The new service acquired a name and an obvious owner—me, wearing another hat in the lab.
The cost arrived later:
- The payload froze assumptions that were still changing.
- One local invariant became several eventual states.
- Every deployment needed configuration, secrets, health, logs, metrics, backup, and restoration.
- A local function failure became latency, timeout, retry, duplicate delivery, or unknown outcome.
- A feature needed compatibility across active versions.
- Development required an environment that approximated the distributed topology.
The network had made some dependencies visible. It had also made ordinary coordination expensive before the concepts were stable.
The project gave me permission to reverse myself
No employer, client, or real users depended on P6X4. The scenarios were synthetic and the external systems were deterministic adapters. That did not make the engineering questions imaginary. It let me interrupt processes, corrupt projections, delay messages, revoke credentials, and repeat migrations without inventing a business story.
It also removed a common excuse for preserving an architecture: organizational ownership.
There were no teams whose identities depended on service names. There was only accumulated code and my reluctance to admit that a sophisticated experiment had taught me the opposite lesson from the one I expected.
I wrote the consolidation hypothesis plainly:
If capabilities change together, share one transactional decision, and recover on the same schedule, consolidating their authority and release should reduce failure states without erasing useful internal boundaries.
The hypothesis could fail. Perhaps the services would reveal independence under load or failure. Perhaps combining them would produce an untestable codebase. I designed P6X4's second phase to collect that evidence.
I reconstructed focused feature episodes from the repository history. Generated clients, lockfiles, broad formatting, and dependency sweeps were excluded. Tests remained because they showed which behavior a change affected.
For each episode I recorded:
- Capabilities changed.
- Contracts versioned.
- Releases that needed ordering.
- Data copied or migrated.
- Failure and recovery paths added.
- Whether either side could ship meaningfully alone.
The graph did not generate a target architecture. It challenged the current one.
Document authority, rendering requests, template selection, and audit facts changed together repeatedly. Search followed source changes but could rebuild and tolerate bounded lag. Delivery evolved more independently because adapter rules, rate limits, receipts, and ambiguous outcomes formed a distinct operational problem.
Notification existed as a separate service because it was a noun. Its scale, authority, and release cadence were not independent. The network boundary bought almost nothing.
I tested each split against change, resource shape, authority, security, recovery, and consistency. Several services passed only the circular test “it is already deployed separately.”
A modular monolith was not a pile of files
The target was not one directory where any handler could reach any table.
P6X4 used packages with explicit public entry points:
apps/
web/
render-worker/
outbox-relay/
search-indexer/
packages/
identity/
documents/
rendering/
delivery/
audit/
search/
platform/Packages could expose commands, queries, domain facts, and narrow ports. Internal files were not importable outside their package. The dependency graph was acyclic except for a small platform layer with no domain knowledge.
Architecture tests parsed imports and failed on forbidden directions. TypeScript path aliases did not grant access to private modules. A build check confirmed that public contracts did not leak persistence types or framework request objects.
The rules sometimes felt artificial. That was their purpose. A useful boundary should interrupt casual dependency. It did not need a network timeout to do so.
The consolidated PostgreSQL database used schemas and roles to retain authority boundaries. The document package governed document revisions and accepted intents. Rendering governed job and artifact receipts. Delivery governed effects, attempts, and evidence. Search remained a separate store because its data was rebuildable and its query shape differed.
Application code could not issue arbitrary cross-schema queries through a universal database client. Each repository exposed domain operations and received a restricted connection interface. Reporting paths were declared and reviewed separately.
I considered a separate database per module within one deployment. It preserved the appearance of ownership and retained the same cross-database transaction problem. For capabilities whose invariants belonged together, that was the cost I wanted to remove.
One database allowed a document revision, its policy context, audit fact, and outbox record to commit atomically. It did not make search or external delivery synchronous. Consistency became a product decision rather than a topology default.
Transactions replaced choreography where the decision was one
The old document workflow created metadata in one service, requested policy attachment in another, emitted an audit event, and asked the renderer to start. Partial completion produced intermediate states and compensation.
Some intermediate states were genuinely useful. Rendering can take time and fail independently. But document revision, chosen template revision, authority decision, and durable render intent described one accepted request. There was no product value in allowing only some of them to commit.
The new transaction:
- Validated the expected document revision and current authority.
- Appended the immutable document revision.
- Recorded the render intent with a stable identity.
- Appended an audit fact.
- Inserted an outbox record for asynchronous execution.
After commit, a renderer could fail, restart, or fall behind without making the accepted document revision disappear. Before commit, nothing external was allowed to act.
This was not a return to one enormous transaction around every workflow. It was a decision that these local invariants should fail together.
I removed queues that merely simulated module calls. I kept asynchronous boundaries for work that could legitimately complete later:
- Rendering consumed significant CPU and produced an immutable artifact.
- Search maintained a rebuildable projection.
- Delivery crossed an external-effect boundary with rate limits and uncertain outcomes.
- Notifications followed completed facts and could recover independently.
The document transaction inserted outbox records in the same commit as authoritative state. A relay published them with stable message IDs. Consumers recorded their projection or effect receipt transactionally where possible.
Inside P6X4, some outbox work used the same database instead of a broker. Workers leased rows with expiration and applied versioned handlers. External or independently retained consumers still received published facts through a broker fixture.
I did not preserve asynchronous transport because it looked modern. Every retained queue had to answer what delay, buffering, isolation, or recovery property it bought.
One release still had several runtime shapes
The modular monolith built one versioned artifact with explicit entry points for web, render worker, outbox relay, indexer, and reconciler.
The roles shared source provenance, domain contracts, migration expectations, and workflow registries. They did not share every resource or privilege.
The renderer had CPU and memory limits, asset access, and no ability to modify document authority. The web role could record intents but not perform external delivery. The outbox relay could read pending records and publish but held no adapter credentials. Database roles and architecture tests enforced those claims.
This separated three decisions I had previously bundled into “make it a service”:
- Code modularity.
- Process and resource isolation.
- Independent versioning and deployment.
P6X4 needed the first two in several places. It needed the third in fewer.
The migration moved behavior before authority
I used a strangler path rather than combining all repositories at once.
For one capability, P6X4 first implemented the existing public contract while calling the old service. Routes could enter the new application composition without changing data ownership. Next, a shadow path calculated results from imported data and compared them with normalized old responses. It performed no effects.
Only after comparison and failure scenarios passed did I move authority. A write fence closed a deterministic cohort, drained accepted old intents to a known sequence, verified P6X4's import, advanced the authority epoch, and reopened writes against P6X4.
There was always one writer. I rejected permanent dual writes after a prototype demonstrated the ambiguous states they create.
During a bounded rollback window, P6X4 exported compatible committed revisions to a read-only old view. Rollback itself required another fenced transition. After the window, a forward repair or P6X4 restore replaced the comforting fiction that the obsolete service could remain authoritative forever.
The service estate contained repeated fields. I did not merge tables by matching names.
Every duplicate was classified as authority, snapshot, projection, cache, or accidental second authority. Current document metadata moved as authority with revisions and stable identities. Render manifests remained immutable snapshots tied to source and template revisions. Search was rebuilt as a projection. Caches were discarded. Writable convenience copies required an explicit resolution before cutover.
The migration compared invariants rather than only counts:
- Stable IDs were unique.
- Current records referenced their highest committed revisions.
- Accepted intents mapped to one payload digest and result.
- Artifacts referenced existing immutable inputs.
- Outbox sequences had no unexplained gaps.
- Authorization relationships remained valid.
Rows that could not be classified or reconciled entered quarantine. A timestamp did not select truth across different authorities and snapshots.
This work took longer than copying tables. It removed uncertainty that would otherwise have survived inside the monolith.
Contracts became narrower after the network disappeared
Several service APIs passed broad objects because serialization made them look official.
The renderer accepted a mutable document shape containing fields unrelated to rendering. The notification service consumed an event with document, user, template, and delivery data because each new listener had added another need. The search client returned persistence-shaped records that callers used for authoritative decisions.
In-process calls made the coupling embarrassingly easy. I used that discomfort to shrink the contracts.
RenderRequest contained an immutable document revision reference, template revision, locale, asset digests, and stable intent. DocumentRendered named the artifact and exact source digests. Search returned discovery references that required authoritative loading before mutation. Notifications consumed a specific completed fact and constructed their own bounded effect request.
Contract tests remained even when transport disappeared. The test fixtures could later support extraction if a boundary earned it. The interface did not assume extraction in advance.
The distributed version required many containers, schema ordering, generated clients, seed coordination, and routing. To work quickly I mocked services, which made the local happy path smooth and hid integration failures.
After consolidation, one database and the role processes could run a complete document scenario. External adapters remained deterministic fixtures. Search could use a local index or an in-memory contract test depending on the scenario. Failure injection targeted durable workflow points rather than shutting down an arbitrary mock.
The improvement was not merely startup time. A keyboard flow, accessibility test, schema migration, worker interruption, and recovery action could operate against the same domain implementation used in the assembled build.
The build initially slowed as repositories combined. I added package-level caching and affected-test selection for fast feedback while keeping full integration and architecture checks. Consolidation moved some cost into a deterministic build. I preferred that to intermittent coordination hidden across deployments.
Observability followed product state
The old tracing view was a tour of service calls. A green trace could still leave the user's document pending, and a failed internal span could belong to successfully retried work.
P6X4 logged transitions around stable intent, document revision, render job, artifact, delivery effect, and projection sequence. Runtime role and release identity remained visible, but the primary narrative followed the work.
Dashboards answered:
- How old is the oldest accepted render without a terminal result?
- Which outbox sequence has each projection applied?
- Which external effects have unknown outcomes?
- Which authority epoch accepted this revision?
- Are all runtime roles on a compatible release?
I could still inspect process latency and errors. They became evidence for product state rather than the only definition of health.
This drew directly from W93H and Z29C. Consolidation did not mean fewer failure modes by decree. It made the remaining states easier to connect to one accepted intent.
I disabled each boundary under load and during migration.
Search could stop while documents continued to commit. Its projection lag increased, the interface explained bounded staleness, and a rebuild caught up. Search retained a distinct store and runtime.
Rendering could stop while accepted jobs remained durable. Interactive traffic continued, and resource isolation prevented CPU pressure from exhausting request capacity. Rendering kept a worker shape but not an independently versioned domain service.
Delivery adapters could time out after acceptance. The effect ledger entered an unknown state and required query or review according to adapter capability. Delivery retained its strong package, credential, and recovery boundary.
Audit facts could not fail independently of the document decision without weakening the record. They moved into the same transaction.
The survival criterion was not whether I liked a technology. It was whether a boundary protected a failure or recovery property the project actually exercised.
Security did not collapse into one trusted process
Combining code increased the blast radius of an accidental import. I treated that as a requirement to enforce capability, not a reason to keep ornamental services.
Each runtime role received a restricted database identity, allowed outbound destinations, and only the secrets declared in its manifest. Package tests enforced import direction. Negative authorization tests tried to read and mutate across module boundaries. Sensitive policy decisions returned bounded results rather than exposing raw tables.
The old internal network had conveyed an undeserved sense of safety. Services frequently trusted calls because they came from another private address. P6X4's in-process code had no network identity to hide behind, so authority became explicit in the command and repository interfaces.
The renderer could process approved immutable input and store artifacts. It could not decide who may render a document. The web handler could accept current authority but could not use delivery credentials. Consolidation reduced network surface while preserving least privilege at execution boundaries.
Deletion was part of the migration
Stopping traffic to a service did not finish consolidation.
For every retired box I inventoried:
- Repository and build pipeline.
- Images and deployment definitions.
- DNS and routes.
- Service identities and credentials.
- Database users, backups, and retention.
- Topics, subscriptions, retry policies, and dead-letter queues.
- Alerts, dashboards, log sinks, and runbooks.
- Generated clients and compatibility packages.
- Local-development fixtures.
The migration remained incomplete while obsolete infrastructure could receive a request, wake a consumer, retain sensitive data, or page an old alert.
A topology snapshot test listed every expected publisher, consumer, process role, and authority. Removing a service changed the test only when its operational residue was gone.
This was where subtraction became measurable. The code move was often the smallest part.
I preserved extraction paths, not extracted services
Consolidation could have made a future split harder. I accepted that trade where tighter invariants were the point, but preserved useful seams.
Public package APIs remained narrow. Durable external facts used versioned schemas. Resource-heavy work consumed explicit job contracts. Database access stayed behind module repositories. Scenario tests exercised capability boundaries through their public surface.
If search, delivery, or rendering later needed independent versioning, the contract and evidence would already exist. Extraction would still require operational work and likely data migration. I did not claim it would be effortless.
The distinction was important: designing a module that could be extracted is reasonable. Paying the network and deployment costs before extraction is justified is not automatically prudent.
I kept the option through clear meaning, not through fourteen permanently active services.
The result was not one number
I avoided a vanity metric such as “complexity reduced by 60 percent.” P6X4 had no real organizational throughput or customer load to measure.
The outcomes were concrete and observable in the lab:
- The subtitle scenario changed one authoritative model and one render contract rather than four service schemas.
- A document revision and its audit and outbox records committed atomically.
- Search could rebuild from a named source sequence.
- Web and worker roles shared one release while retaining separate resource and privilege limits.
- A clean environment could run an end-to-end failure scenario without coordinating fourteen service versions.
- Retired queues, credentials, databases, and dashboards no longer existed.
- Every remaining network or asynchronous boundary had a written reason and failure test.
Some costs grew. The main repository and test suite became larger. Database migration discipline mattered more. Architecture checks had to prevent convenient cross-module access. A bad release could affect several capabilities at once, so staged activation and role-specific health remained necessary.
The result was not simplicity without responsibility. It was responsibility placed closer to the decisions it protected.
The diagram became less impressive
P6X4 ended with fewer independently deployed units, a handful of explicit runtime shapes, one authoritative application database, a search projection, and durable adapters at the external edge.
The new diagram did not advertise sophistication through box count. It made document authority, asynchronous rendering, projection lag, and external effects easier to point at. A line existed because delay, isolation, or independent recovery mattered—not because a noun had once deserved a repository.
I still value distributed systems. Z29C's external-effect protocol, W93H's incident evidence, and P6X4's projections all depended on treating time and failure seriously. The lesson was not that networks are bad or monoliths are good.
Distribution is a purchase. It buys particular forms of independence and pays in latency, compatibility, partial failure, observability, security surface, and recovery. P6X4 had purchased independence it did not use.
Putting the monolith back together was not a retreat from difficult engineering. It required stronger judgment about which difficulty belonged to the problem.
The architecture became less visible. The boundaries that remained became more believable.