{"id":305,"date":"2026-06-04T15:33:01","date_gmt":"2026-06-04T15:33:01","guid":{"rendered":"https:\/\/softwareproduction.eu\/?p=305"},"modified":"2026-06-07T18:58:20","modified_gmt":"2026-06-07T18:58:20","slug":"the-legacy-problem-when-manual-synchronization-becomes-the-architecture","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=305","title":{"rendered":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>First in a series about migrating from legacy web architectures to a modern Nuxt 4 stack. The series covers architecture, code generation, performance, infrastructure, and the automation philosophy behind every decision.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Starting Point: Architectures That Work \u2014 But Barely<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Across multiple large, high-traffic, customer-facing web platforms \u2014 the kind where millions of visitors per year browse products, configure options, and complete purchases \u2014 two architectural patterns show up again and again:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Dual-rendering<\/strong>: ASP.NET MVC with Razor views and Vue.js components layered on top. The server renders HTML; the client re-renders parts of it.<\/li>\n<li><strong>SPA + BFF<\/strong>: A single-page application handles all rendering, calling a backend-for-frontend (BFF) layer for data. Types, REST clients, and mapping code are written by hand on both sides.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Both patterns work. Pages load, forms submit, orders go through. But both are collapsing under complexity that better code cannot fix. The problems are structural \u2014 rooted in <strong>manual synchronization between client and server code<\/strong> that compounds with every new feature.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This article walks through the patterns repeatedly seen in these systems, why they decay, and what finally pushes teams to break out.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Architecture That Grew Sideways<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Variant A: Dual Rendering (Server HTML + Client JS)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most visible version of this problem follows a pattern that sounds sensible at first: <strong>server-side HTML rendering through Razor templates, with client-side interactivity layered in via Vue.js components<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The server renders page structure and static content. Vue components handle the dynamic pieces \u2014 a product configurator, a checkout form, an interactive filter. The two worlds are stitched together with data attributes, JSON blobs injected into the Razor HTML, and an ever-growing pile of initialization scripts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a simplified view of how a single page request flows through such a system:<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart TB\n    subgraph B[\"Browser\"]\n        Breq[\"User requests page\"]\n    end\n\n    subgraph S[\"ASP.NET MVC Server\"]\n        C[\"Controller\"]\n        VM[\"C# ViewModel\"]\n        RZ[\"Razor View\"]\n        HTML[\"Server-rendered HTML&lt;br\/&gt;+ embedded JSON blobs&lt;br\/&gt;+ &amp;lt;script&amp;gt; tags mounting Vue\"]\n    end\n\n    subgraph CS[\"Client-Side (Browser)\"]\n        PHTML[\"Step 1: Parse large HTML document\"]\n        PJS[\"Step 2: Download all JS bundles (no splitting)\"]\n        PJSON[\"Step 3: Parse JSON blobs from data attributes\"]\n        MOUNT[\"Step 4: Mount Vue components onto DOM nodes\"]\n        RERENDER[\"Step 5: Re-render with client-side data\"]\n        CLS[\"\u2192 Content jumps (CLS)\"]\n        TBT[\"\u2192 Slow interaction (TBT)\"]\n    end\n\n    Breq --&gt; C --&gt; VM --&gt; RZ --&gt; HTML\n    HTML --&gt; PHTML --&gt; PJS --&gt; PJSON --&gt; MOUNT --&gt; RERENDER\n    RERENDER --&gt; CLS\n    RERENDER --&gt; TBT<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is a <strong>dual-rendering architecture<\/strong>: the server renders HTML, and the client re-renders parts of it. Neither side has the full picture. The gap between what the server knows and what the client knows is where the bugs live.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Variant B: SPA + BFF (Client Renders Everything, Server Provides Data)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The second variant looks more modern on the surface. A single-page application \u2014 React, Angular, or Vue \u2014 handles all rendering. The server exposes REST APIs (often through a BFF layer), and the client fetches data via HTTP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is no dual rendering, no hydration gap, no content flash. But the <strong>manual synchronization problem is identical<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Every API endpoint needs a corresponding TypeScript interface on the client<\/li>\n<li>Every DTO in C# or Java needs a hand-written model in TypeScript<\/li>\n<li>Every REST client is bespoke: custom fetch wrappers, error handling, retry logic, response parsing<\/li>\n<li>Every new field or renamed property requires coordinated changes across both codebases<\/li>\n<li>Every API version bump triggers a manual update cascade through the frontend<\/li>\n<\/ul>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart LR\n    subgraph SPA[\"SPA (React\/Angular\/Vue)\"]\n        UI[\"Components\"]\n        TSM[\"Hand-written TS models\"]\n        RC[\"Hand-written REST clients\"]\n    end\n\n    subgraph BFF[\"BFF \/ Backend\"]\n        CTRL[\"Controllers\"]\n        DTO[\"DTOs \/ ViewModels\"]\n        SVC[\"Services\"]\n    end\n\n    UI --&gt; TSM --&gt; RC --&gt; CTRL --&gt; DTO --&gt; SVC\n\n    classDef warn fill:#4e3400,stroke:#ffb74d,stroke-width:1px,color:#fff;\n    TSM:::warn\n    RC:::warn\n    DTO:::warn<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The rendering is clean, but the <strong>data contract layer<\/strong> \u2014 the types, the clients, the mapping \u2014 is just as manual and just as fragile as in the dual-rendering case. A renamed field in the backend does not produce a compile error in the frontend. It produces a <code>undefined<\/code> at runtime, caught by QA if you are lucky, or by a customer if you are not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Teams in this position often generate TypeScript clients from OpenAPI specs, which helps. But it only covers the REST surface. If the frontend also consumes a headless CMS, a SOAP service, or multiple microservices with their own contracts, those sources still need manual type definitions. The problem is reduced but not eliminated \u2014 and the REST clients themselves still carry per-endpoint boilerplate: headers, auth tokens, error mapping, pagination handling, cache invalidation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Both variants converge on the same core issue: <strong>manual synchronization of data contracts across a network boundary, at a scale where humans cannot keep up<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Five Problems That Compound<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following problems manifest in both architectural variants, though the first (the hydration gap) is specific to dual-rendering systems. The remaining four apply equally to SPA + BFF architectures.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. The Hydration Gap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Modern SSR frameworks like Nuxt or Next.js solve the &#8220;server renders, client takes over&#8221; problem with <strong>hydration<\/strong>: the client receives the exact same component tree the server rendered and <em>attaches<\/em> to it without re-rendering. The DOM stays stable. The user sees no flicker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A dual-rendering architecture has no such mechanism. Vue components mount <em>after<\/em> the server-rendered HTML is already visible. The browser shows the Razor output first \u2014 then, once JavaScript loads and runs, Vue replaces sections of the DOM with its own rendered output. The result is visible content jumps (Cumulative Layout Shift) and a page that feels slow even when the server responds quickly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In practice, this often looks like a user landing on a product comparison page and seeing a static HTML shell for 1\u20132 seconds, followed by a jarring re-render as Vue components initialize and inject dynamic content. On mobile devices with slower CPUs, the effect is even worse.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nsequenceDiagram\n    participant U as User\n    participant Br as Browser\n    participant S as ASP.NET MVC + Razor\n    participant JS as Vue JS Runtime\n\n    U-&gt;&gt;Br: Navigate to product comparison page\n    Br-&gt;&gt;S: HTTP GET \/products\/compare\n    S--&gt;&gt;Br: HTML (Razor-rendered shell&lt;br\/&gt;+ JSON blobs + script tags)\n    Br--&gt;&gt;U: Initial static HTML rendered&lt;br\/&gt;(*visible to user*)\n\n    par Network &amp; CPU\n        Br-&gt;&gt;JS: Download &amp; parse JS bundles\n        JS-&gt;&gt;Br: Initialize Vue runtime\n        JS-&gt;&gt;Br: Mount Vue components&lt;br\/&gt;and re-render sections\n    end\n\n    Br--&gt;&gt;U: DOM updates cause&lt;br\/&gt;content jumps (CLS) and delay&lt;br\/&gt;before interactive (TBT)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. The Data Translation Layer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In these architectures, every piece of data that must appear in both server-rendered HTML and client-side Vue components has to be <strong>manually translated<\/strong> between two type systems:<\/p>\n\n\n\n<pre><code class=\"language-text\">C# ViewModel (Server)          TypeScript Model (Client)\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500          \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nProductOfferModel.cs     \u2192     ProductOffer.ts\n  .PriceGross (decimal)           .priceGross (number)\n  .ProductName (string)           .productName (string)\n  .ValidFrom (DateTime)           .validFrom (string)\n  .IsDefault (bool)               .isDefault (boolean)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For every backend data model, a developer has to:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Write a C# ViewModel class  <\/li>\n<li>Write a corresponding TypeScript interface  <\/li>\n<li>Write a mapping function that transforms the C# object into JSON  <\/li>\n<li>Write another mapping function that deserializes it on the client side  <\/li>\n<li>Keep all four in sync whenever any field changes  <\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This translation layer is tedious, but the real problem is that it creates <strong>constant bugs<\/strong>. A renamed field in C# silently breaks the client. A new enum value triggers a runtime error in JavaScript. A date format change causes forms to display the wrong values.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Some teams address this with <strong>OpenAPI<\/strong> (Swagger) by generating TypeScript clients from the backend&#8217;s REST API spec. For pure REST architectures, that is a solid approach. But it only covers the REST surface. If the frontend also consumes a headless CMS, a SOAP service, or server-rendered JSON blobs, those sources still need their own manual type definitions. OpenAPI reduces the problem; it does not eliminate it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You end up with dozens of model pairs, each one a potential point of divergence.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart LR\n    subgraph Server[\"Server (C#)\"]\n        VM[\"ViewModel class&lt;br\/&gt;(e.g., ProductOfferModel.cs)\"]\n        MAP_S[\"Mapping function:&lt;br\/&gt;C# \u2192 JSON\"]\n    end\n\n    subgraph Wire[\"Wire Format\"]\n        JSON[\"JSON payload\"]\n    end\n\n    subgraph Client[\"Client (TypeScript\/Vue)\"]\n        TSIF[\"TypeScript interface&lt;br\/&gt;(ProductOffer.ts)\"]\n        MAP_C[\"Mapping\/parsing function:&lt;br\/&gt;JSON \u2192 TS model\"]\n        VC[\"Vue component\"]\n    end\n\n    VM --&gt; MAP_S --&gt; JSON --&gt; MAP_C --&gt; TSIF --&gt; VC\n\n    classDef warn fill:#4e3400,stroke:#ffb74d,stroke-width:1px,color:#fff;\n    VM:::warn\n    TSIF:::warn\n    MAP_S:::warn\n    MAP_C:::warn<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. No Unified API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In legacy systems that grew organically, the frontend usually does not talk to a single API. Instead, it calls:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>REST endpoints for business logic and transactional operations  <\/li>\n<li>Direct headless CMS REST APIs for marketing or content pages  <\/li>\n<li>Server-rendered JSON blobs for other server-controlled content  <\/li>\n<li>Various cloud service endpoints for user data, feature flags, or personalization  <\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Each integration has its own error handling, retry logic, response format, and TypeScript types. There is no shared understanding of what &#8220;calling the backend&#8221; even means.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result: <strong>every new feature requires figuring out which endpoints to call<\/strong>, in what order, and how to stitch the responses together. Frontend developers spend more time reading backend code than writing frontend code.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart LR\n    FE[\"Frontend app&lt;br\/&gt;(Vue in Razor pages)\"]\n\n    subgraph APIs[\"Backend &amp; Services\"]\n        REST[\"Business REST API\"]\n        CMS[\"Headless CMS REST API\"]\n        JSONB[\"Server-rendered JSON blobs\"]\n        CLOUD[\"Cloud services&lt;br\/&gt;(flags, personalization, auth, etc.)\"]\n    end\n\n    FE --&gt; REST\n    FE --&gt; CMS\n    FE --&gt; JSONB\n    FE --&gt; CLOUD<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4. Performance as an Afterthought<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Performance in these systems is rarely designed in \u2014 it is patched over with operational over-provisioning. The usual pattern looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Metric<\/th><th>Value<\/th><\/tr><\/thead><tbody><tr><td><strong>Median response time<\/strong> (homepage)<\/td><td>2,618 ms<\/td><\/tr><tr><td><strong>Error rate<\/strong> under normal load<\/td><td>3.91%<\/td><\/tr><tr><td><strong>JavaScript bundle size<\/strong><\/td><td>No code splitting \u2014 everything loaded upfront<\/td><\/tr><tr><td><strong>Infrastructure<\/strong><\/td><td>3\u00d7 P3v3 App Service instances (24 vCPU, 96 GB RAM)<\/td><\/tr><tr><td><strong>Scaling model<\/strong><\/td><td>Fixed \u2014 always on, always paying<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A 2.6-second median response time is usually not a server problem \u2014 the C# backend is often reasonably fast. The problem is the <strong>rendering pipeline<\/strong>: large HTML pages, no code splitting, no lazy loading, uncompressed assets, and the sequential load-parse-mount-re-render cycle Vue must perform on every page view.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Throwing hardware at the problem \u2014 and three P3v3 instances is a <em>lot<\/em> of compute \u2014 keeps the site alive, but it does not make it fast. And you pay the same at 3 AM with zero traffic as you do at noon during peak load.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Developer Experience Death by a Thousand Cuts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Adding a new content section \u2014 say, a promotional banner with a countdown timer \u2014 can require touching <strong>six distinct layers<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>C# TagHelper<\/strong> \u2014 to define how the server renders the banner&#8217;s outer HTML  <\/li>\n<li><strong>C# ViewComponent<\/strong> \u2014 to load data for the banner from a service  <\/li>\n<li><strong>CSHTML Razor View<\/strong> \u2014 to define the server-side template  <\/li>\n<li><strong>C# ViewModel<\/strong> \u2014 to define the data shape  <\/li>\n<li><strong>TypeScript Model<\/strong> \u2014 to mirror the data shape for the client  <\/li>\n<li><strong>Vue Component<\/strong> \u2014 to make the banner interactive (countdown logic)  <\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Six files, three languages, two rendering contexts. If the banner needs data from a headless CMS, add another REST client and another model conversion. If it needs form validation, add both C# server-side validation and JavaScript client-side validation \u2014 two implementations of the same rules.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In a SPA + BFF architecture, the first three layers disappear \u2014 but the remaining three do not. You still write the C# DTO, the TypeScript mirror, and the Vue component. You still write a REST client method. You still keep both sides in sync. The layer count drops from six to four, but the <strong>synchronization tax<\/strong> remains identical. Every field change still crosses the wire boundary by hand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is the cost of every feature. It compounds, and after a while developers start dreading change altogether.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart TB\n    FEAT[\"Feature: Promotional banner&lt;br\/&gt;with countdown timer\"]\n\n    TAG[\"C# TagHelper\"]\n    VCMP[\"C# ViewComponent\"]\n    RAZOR[\"CSHTML Razor View\"]\n    VM[\"C# ViewModel\"]\n    TS[\"TypeScript model\"]\n    VUE[\"Vue component&lt;br\/&gt;(countdown logic)\"]\n\n    FEAT --&gt; TAG\n    FEAT --&gt; VCMP\n    FEAT --&gt; RAZOR\n    FEAT --&gt; VM\n    FEAT --&gt; TS\n    FEAT --&gt; VUE<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Deeper Problem: Architectural Coupling Disguised as Separation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The irony is that these architectures <em>look<\/em> like they separate concerns. Server-side rendering is &#8220;separated&#8221; from client-side rendering. C# is &#8220;separated&#8221; from TypeScript. Razor views are &#8220;separated&#8221; from Vue components.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But the separation is structural, not functional. The layers remain tightly coupled through shared data contracts, rendering assumptions, and behavior expectations. Change one layer, and the ripple effects cross every boundary.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart LR\n    subgraph Server[\"Server\"]\n        CS[\"C# Models\"]\n        RZ[\"Razor Views\"]\n    end\n\n    subgraph Client[\"Client\"]\n        VUE[\"Vue Components\"]\n    end\n\n    CS --&gt; RZ\n    RZ --&gt; VUE\n\n    subgraph Coupling[\"Shared assumptions \/ tight coupling\"]\n        FN[\"Field names must match\"]\n        DF[\"Date formats must match\"]\n        EV[\"Enum values must match\"]\n        NH[\"Null handling must match\"]\n    end\n\n    CS --- Coupling\n    RZ --- Coupling\n    VUE --- Coupling<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Real separation of concerns means a change in one area does not force changes in another. These architectures fail that test badly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Breaking Out: The Architecture Used Instead<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The replacement architecture follows a different principle: <strong>one rendering engine, one type system, one unified API surface<\/strong>.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart LR\n    subgraph Browser[\"Browser\"]\n        B[\"User\"]\n    end\n\n    subgraph Nuxt[\"Nuxt 4 (SSR)\"]\n        V3[\"Vue 3&lt;br\/&gt;Component Tree\"]\n        APClient[\"Apollo GraphQL Client\"]\n    end\n\n    subgraph Gateway[\"Apollo Server&lt;br\/&gt;(GraphQL Gateway)\"]\n    end\n\n    subgraph Backends[\"Backend Subgraphs\"]\n        CMS[\"CMS Subgraph\"]\n        DOTNET[\".NET Backend Subgraph\"]\n    end\n\n    B &lt;--&gt;|HTTP| Nuxt\n    Nuxt --&gt; V3\n    V3 --&gt; APClient\n    APClient --&gt; Gateway\n    Gateway --&gt; CMS\n    Gateway --&gt; DOTNET<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The key shifts:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Single rendering engine<\/strong>: Vue 3 renders on both server and client. The server produces HTML; the client hydrates the same component tree. No content jumps, no flickering.  <\/li>\n<li><strong>Single API surface<\/strong>: A GraphQL gateway stitches together the headless CMS and the .NET backend into one schema. The frontend writes <code>.graphql<\/code> query files; code generation produces fully typed TypeScript composables. No manual model translation.  <\/li>\n<li><strong>Single type system<\/strong>: TypeScript everywhere, with types generated from the GraphQL schema. A renamed field in the backend breaks the TypeScript compiler \u2014 not the production website at 2 AM.  <\/li>\n<\/ul>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nsequenceDiagram\n    participant U as User (Browser)\n    participant N as Nuxt 4 SSR (Vue 3)\n    participant G as GraphQL Gateway\n    participant C as CMS Subgraph\n    participant D as .NET Subgraph\n\n    U-&gt;&gt;N: HTTP GET \/page\n    N-&gt;&gt;G: GraphQL query&lt;br\/&gt;(content + business data)\n    G-&gt;&gt;C: Resolve CMS fields\n    C--&gt;&gt;G: CMS data\n    G-&gt;&gt;D: Resolve .NET fields\n    D--&gt;&gt;G: .NET data\n    G--&gt;&gt;N: Unified GraphQL response\n    N--&gt;&gt;U: HTML (SSR) with embedded state\n    U-&gt;&gt;N: Hydration request (client-side)\n    N--&gt;&gt;U: Interactive Vue app&lt;br\/&gt;(no re-render jump)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What Gets Eliminated<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One of the most satisfying aspects of a migration like this is what you get to delete:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Layer<\/th><th>Old System<\/th><th>New System<\/th><\/tr><\/thead><tbody><tr><td><strong>C# ViewModels<\/strong><\/td><td>One per content type<\/td><td>\u274c Eliminated \u2014 types generated from GraphQL<\/td><\/tr><tr><td><strong>C# TagHelpers<\/strong><\/td><td>Custom HTML rendering<\/td><td>\u274c Eliminated \u2014 Vue components render everything<\/td><\/tr><tr><td><strong>CSHTML Razor Views<\/strong><\/td><td>Server-side templates<\/td><td>\u274c Eliminated \u2014 replaced by Vue SFC templates<\/td><\/tr><tr><td><strong>TypeScript model files<\/strong><\/td><td>Manual mirror of C# types<\/td><td>\u274c Eliminated \u2014 generated from schema<\/td><\/tr><tr><td><strong>REST client wrappers<\/strong><\/td><td>Per-endpoint fetch code<\/td><td>\u274c Eliminated \u2014 generated GraphQL composables<\/td><\/tr><tr><td><strong>JSON data injection<\/strong><\/td><td>Script tags with serialized models<\/td><td>\u274c Eliminated \u2014 SSR payload hydration<\/td><\/tr><tr><td><strong>Dual validation<\/strong><\/td><td>C# + JS validation rules<\/td><td>\u274c Eliminated \u2014 backend validation rules fetched at runtime<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Every row in that table represents hundreds of lines of code and hundreds of potential bugs \u2014 gone, not refactored.<\/p>\n\n\n\n<pre class=\"mermaid\">%%{init: {'theme': 'dark'}}%%\nflowchart TB\n    subgraph Legacy[\"Legacy layers\"]\n        L1[\"C# ViewModels\"]\n        L2[\"TagHelpers\"]\n        L3[\"CSHTML Razor Views\"]\n        L4[\"Manual TS models\"]\n        L5[\"REST client wrappers\"]\n        L6[\"JSON data injection\"]\n        L7[\"Dual validation logic\"]\n    end\n\n    subgraph New[\"New stack\"]\n        GQL[\"GraphQL schema\"]\n        CGEN[\"Code generation&lt;br\/&gt;(types + composables)\"]\n        VUE3[\"Vue 3 components&lt;br\/&gt;(SSR + CSR)\"]\n    end\n\n    L1 -.removed.-&gt; GQL\n    L2 -.removed.-&gt; VUE3\n    L3 -.removed.-&gt; VUE3\n    L4 -.removed.-&gt; CGEN\n    L5 -.removed.-&gt; CGEN\n    L6 -.removed.-&gt; VUE3\n    L7 -.moved.-&gt; GQL\n\n    GQL --&gt; CGEN --&gt; VUE3<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Next<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This article covered the <em>what<\/em> and <em>why<\/em>. The next articles provide the architectural overview and dive into the unified data layer:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 2<\/strong>: <em>The Target Architecture \u2014 A Bird&#8217;s-Eye View<\/em> \u2014 The four-container topology, the in-process GraphQL gateway, the code generation pipeline, and how every article in the series maps to the architecture.<\/li>\n<li><strong>Article 3<\/strong>: <em>GraphQL Schema Stitching \u2014 One API to Rule Them All<\/em> \u2014 How to unify multiple backend data sources into a single GraphQL gateway.<\/li>\n<li><strong>Article 4<\/strong>: <em>The @delegate Directive Deep Dive \u2014 Cross-Subgraph Field Resolution<\/em> \u2014 Typed placeholders and formatters for cross-subgraph queries.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>First in a series about migrating from legacy web architectures to a modern Nuxt 4 stack. The series covers architecture, code generation, performance, infrastructure, and the automation philosophy behind every decision. The Starting Point: Architectures That Work \u2014 But Barely Across multiple large, high-traffic, customer-facing web platforms \u2014 the kind where millions of visitors per [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":304,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-305","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=305\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production\" \/>\n<meta property=\"og:description\" content=\"First in a series about migrating from legacy web architectures to a modern Nuxt 4 stack. The series covers architecture, code generation, performance, infrastructure, and the automation philosophy behind every decision. The Starting Point: Architectures That Work \u2014 But Barely Across multiple large, high-traffic, customer-facing web platforms \u2014 the kind where millions of visitors per [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=305\" \/>\n<meta property=\"og:site_name\" content=\"Software Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-04T15:33:01+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-07T18:58:20+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1880\" \/>\n\t<meta property=\"og:image:height\" content=\"1253\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture\",\"datePublished\":\"2026-06-04T15:33:01+00:00\",\"dateModified\":\"2026-06-07T18:58:20+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305\"},\"wordCount\":1931,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/01-the-legacy-problem-1.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305\",\"name\":\"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/01-the-legacy-problem-1.jpg\",\"datePublished\":\"2026-06-04T15:33:01+00:00\",\"dateModified\":\"2026-06-07T18:58:20+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=305\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/01-the-legacy-problem-1.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/01-the-legacy-problem-1.jpg\",\"width\":1880,\"height\":1253},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=305#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=305","og_locale":"en_US","og_type":"article","og_title":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production","og_description":"First in a series about migrating from legacy web architectures to a modern Nuxt 4 stack. The series covers architecture, code generation, performance, infrastructure, and the automation philosophy behind every decision. The Starting Point: Architectures That Work \u2014 But Barely Across multiple large, high-traffic, customer-facing web platforms \u2014 the kind where millions of visitors per [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=305","og_site_name":"Software Production","article_published_time":"2026-06-04T15:33:01+00:00","article_modified_time":"2026-06-07T18:58:20+00:00","og_image":[{"width":1880,"height":1253,"url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","type":"image\/jpeg"}],"author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=305#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=305"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture","datePublished":"2026-06-04T15:33:01+00:00","dateModified":"2026-06-07T18:58:20+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=305"},"wordCount":1931,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=305#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=305","url":"https:\/\/softwareproduction.eu\/?p=305","name":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture - Software Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=305#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=305#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","datePublished":"2026-06-04T15:33:01+00:00","dateModified":"2026-06-07T18:58:20+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=305#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=305"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=305#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","width":1880,"height":1253},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=305#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"The Legacy Problem \u2014 When Manual Synchronization Becomes the Architecture"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/01-the-legacy-problem-1.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/305","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=305"}],"version-history":[{"count":5,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/305\/revisions"}],"predecessor-version":[{"id":462,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/305\/revisions\/462"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/304"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=305"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=305"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=305"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}