The Full Picture — Was the Machinery Worth Owning?
Photo by Sonny Sixteen on Pexels
Part 1 opened with a change request: show a document’s revision number next to its title. In a dual-rendering application that took six coordinated edits, not one of which was checked by anything. Eighteen months of migration work later, the honest question isn’t whether the new architecture is more modern. It’s whether that change got cheaper by enough to justify everything now sitting in the repository.
So this article answers that, collects up the lessons which turned out to be transferable, and says where I wouldn’t build this again.
Table of Contents
- The Revision Label Eighteen Months Later
- A Logical View of the System
- Four Decisions That Got an Owner
- Three Rules That Kept Reappearing
- What It Costs to Own
- What Can Be Claimed Responsibly
- When to Adopt Simplify or Decline
- The Series in Reading Order
- Closing Thoughts
The Revision Label Eighteen Months Later
Same request, current architecture. The page’s GraphQL operation selects revision alongside title, and the component renders it. Generation updates the operation’s result type, and the component either type-checks against it or the build fails. If the field doesn’t exist in the schema, operation validation fails. If it’s nullable and the component treats it as a number, the type check fails right at the usage.
Part 1 proposed counting two things for a change like this: how many places have to agree, and how many of those agreements are verified automatically. The count is now two edits with both agreements checked, against six edits with none at all. It’s the ratio that moved, and that framing has held up rather better than any of the technology choices in this series, because it tells you what to look at in a codebase you’ve never seen before.
What didn’t get cheaper deserves the same precision. The field still has to exist in the .NET API and be exposed through the schema, which is a backend change with a review of its own. The checks only tell you the truth when generation runs against the current schema in CI. Nullability, authorization and validation all remain the API’s job, because a generated type enforces nothing at run time. So the change became local and checkable. It didn’t become free.
A Logical View of the System
Those are logical responsibilities rather than a container inventory. The cache may well be a container in an isolated preview and a managed service in production. The proxy forwards requests and sets cache policy without holding any responses itself. And whether the CDN caches anything at all depends on its configuration and on the response semantics.
Four Decisions That Got an Owner
Client data contracts are derived rather than transcribed. The gateway composes one schema, and the client’s types come out of it. The general principle here is schema-derived contracts in preference to manually synchronized ones, and not GraphQL in preference to REST, since OpenAPI-generated clients give you the same property for REST APIs. And here’s the qualification once more, because it’s the one everybody drops: generated code is only trustworthy when it gets regenerated against the current schema and the type check actually runs.
Rendering has one owner. One component tree renders on the server and hydrates in the browser, so there’s no second template language and no second data representation. The cost is a contract: the browser has to start from the state the server used, and that’s a discipline rather than a feature.
Module boundaries are declared and checkable. Each module states what it registers, owns its own configuration, and depends only in the permitted directions. Nuxt modules don’t make files private, so a boundary is exactly as strong as the lint rule, package export or review that enforces it. Knowing which of those you’ve actually got is more useful than believing you have encapsulation.
Releases are verified before they get traffic. Configuration is generated from one source per environment, a candidate deployment revision is exercised at its own address, and then traffic switches at the proxy. None of which isolates shared data, cache formats, external side effects or browser clients from the previous release. That’s why the compatibility rules matter considerably more than the switching mechanism does.
Three Rules That Kept Reappearing
Writing these twenty articles turned up three rules that each showed up in four or five completely different contexts. They’re the part I’d carry with me to a different stack entirely.
Anything reused has to be keyed by everything that varies it. Cache identity in the gateway, the per-URL result set for CMS pages, the useAsyncData key, the experiment assignment in a page cache, a DataLoader’s scope. Every single one of those is the same rule, and every violation produces the same category of incident: one visitor’s content served to another, or one variant’s page served to everybody.
Any value the server decided has to be transferred, not recomputed. Experiment assignment, async condition results, validity flags, locale, authorization outcomes. Recomputing in the browser gives you hydration mismatches when it disagrees and wasted work when it agrees. And the corollary is just as useful: a value that genuinely can’t be known on the server belongs behind an explicit client-only boundary, not in shared code written on the hope that both sides will happen to agree.
A boundary only exists if something checks it. Module imports, contract validation in CI, release gates, cache-key correctness, hydration warnings failing a browser test. Every boundary in this architecture that lacked a check eventually drifted, and every one that had a mechanical check stayed exactly where it was put. The uncomfortable implication is that what actually holds this architecture in place is the lint rules, the generators and the release gates, rather than the document describing it.
What It Costs to Own
- Generators need tests, ownership and an upgrade path, and they fail in one specifically nasty way: silently producing stale output that compiles beautifully.
- A stitched gateway is a component with a schema lifecycle, caching semantics, a failure policy and diagnostics of its own.
- SSR needs memory budgeting and request-scoped state discipline, and it punishes violations under concurrency rather than in development.
- Preview environments need reconciliation-based cleanup, access restriction, and awareness of the shared dependencies they aren’t actually isolating.
- Candidate releases consume capacity, and retained revisions constrain both secret removal and cache-format changes.
- Observability needs bounded label cardinality, redaction, a sampling policy and retention rules, or it turns into a cost centre that leaks data as well.
And there’s one cost that’s very easy to leave off that list: all of this needs people who understand it. Every mechanism above replaced repetitive work with a system that has failure modes of its own, and a team inheriting those systems without knowing why they were built can’t tell a deliberate decision from an accident. So they’ll treat the constraints as arbitrary and work around them.
What Can Be Claimed Responsibly
| Area | Defensible benefit | Necessary qualification |
|---|---|---|
| Data access | Less handwritten client plumbing, earlier contract errors | Requires current schemas, regeneration and a real type check |
| Rendering | Useful HTML before client startup | Adds server work and a hydration contract |
| Caching | Fewer repeated upstream operations | Keys and invalidation must respect user, locale and variant identity |
| Deployment | Candidate verification before cutover | Shared state and older clients still need compatibility rules |
| Recovery | Previous release available without rebuilding | Readiness and routing propagation affect actual recovery time |
| Diagnostics | Better evidence for a root-cause hypothesis | Sampling, overhead, permissions and retention all constrain it |
On performance, the position from Part 19 stands. The observations span seconds-scale response summaries for the legacy stack and hundreds of milliseconds for many requests in the revised one, and the revised application handled several multiples of a chosen baseline under declared conditions. Those are ranges and tested behaviour. They are not a like-for-like whole-system ratio, because the historical aggregates and the new measurements were taken at different boundaries over different populations. And inventing a ratio out of them would undermine everything else in this series.
When to Adopt Simplify or Decline
The architecture itself isn’t the recommendation. The recommendation is to match the machinery to the problem, and Part 1’s diagnostic is the test for that.
Adopt something like this when a small change touches many unchecked agreements, when your pages genuinely combine several independently owned data sources, when server rendering is a requirement rather than a preference, and when the team is big enough that boundaries need enforcing by tools rather than by conversation. Those four conditions together are what make generators and a gateway cheaper than the coordination they replace.
Simplify it when one of those conditions falls away. A single backend API needs no stitching gateway at all, so generate a client from its specification and keep the composition in the frontend. A site with no editorial content needs no catch-all route, no content generators and no conditional-visibility system. And a team of three can enforce module boundaries in review and skip the packaging entirely.
Decline it when the application is small, its content is largely static, or its change volume is low. Most of this series’ mechanisms are amortized costs: they pay off across many changes and they’re pure overhead across few. A brochure site with a contact form and one integration is far better served by the simplest thing that renders it, and choosing that is not a failure of ambition.
The Series in Reading Order
Foundations and data contracts
- Duplicated Contracts
- The Target Architecture
- GraphQL Schema Stitching
- The Custom Delegate Directive
- GraphQL-Based Code Generation
Application structure and content
- Nuxt Module Boundaries
- Two Toolkit Modules
- The Compose Pattern
- Nuxt and a Headless CMS
- Visibility Rules and Editor Preview
- A/B Testing at the SSR Level
Rendering and browser performance
Security, operations, and verification
- Security in a Nuxt SSR App
- Configuration Generation and Safe Releases
- Memory, Stability, and PM2
- From a Latency Alert to a Root Cause
- Load Testing an SSR Migration
- The Full Picture — this article.
Closing Thoughts
If I had to compress eighteen months into a single sentence, it’d be that the valuable architectural changes were the ones which turned an agreement somebody had to remember into an agreement a machine could check. Not the frameworks, and not the generators as such. The checks.
And that’s also the limit of it. The checks reduce repeated work and make failures easier to locate. They don’t remove drift, outages or operational cost, and every one of them is now a thing you own. So keep your claims narrow enough to test, your examples free of anything that identifies a customer, and your measurements comparable enough to be worth quoting. If you can state an architecture’s costs and limits plainly, then you can also argue about them and change your mind later on. An architecture defended with claims nobody can check tends to stay exactly as it is.
Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.
Category: Nuxt in Production