Server-Side Page Fragment Composition means the server (or an edge layer in front of it) builds one HTML page by stitching together HTML fragments that are each owned, built, and deployed by a different service or team.
Intro
Server-Side Page Fragment Composition means the server (or an edge layer in front of it) builds one HTML page by stitching together HTML fragments that are each owned, built, and deployed by a different service or team. Instead of one frontend application that every team must change and release together, each team publishes its own fragment (for example, “claim summary”, “policy panel”, “document list”), and a composer assembles them into the page before the browser receives it. The user sees one page; the organisation runs several independent frontend pipelines.
Why we need this
Backend teams split the monolith into services years ago, but many organisations still ship one big frontend. Every team that needs a UI change must touch the same Angular workspace or the same Razor/MVC project, queue behind the same release train, and share the same regression suite. The backend is decoupled; the frontend is the new monolith.
Business reasons: faster, independent releases per business capability; clear ownership (the Claims team owns everything a claims handler sees about a claim, from database to pixels); lower coordination cost between teams (see Day 4, Service per Team).
Technical reasons: SEO and first-paint performance matter for some pages (customer portal, public quote pages), and server-rendered HTML gives fast, crawlable output without shipping a large JavaScript bundle. Fragments can be cached independently, so a slow or rarely changing fragment does not slow down the whole page. Teams can also upgrade technology at different speeds (one fragment on Razor, another on Blazor SSR) without a big-bang rewrite.
What problem it solves
Problem (from the topic list): a monolithic frontend blocks independent frontend deployment.
Without it, in our insurance claims system, the “Claim Details” page shows data from four capabilities: Claims (status, timeline), Policy (coverage, deductible), Documents (photos, PDFs), and Payments (settlement history). One frontend repo contains all four UI areas. Consequences:
- A typo fix in the Payments panel needs a full frontend build, full regression, and a shared release slot.
- A broken change from one team blocks the release of everyone else.
- Merge conflicts in shared layout, routing and state files.
- The frontend team becomes a bottleneck that has to translate every backend team’s requirements.
- One team’s library upgrade (or Angular major upgrade) becomes everyone’s project.
With server-side fragment composition, the page is a thin shell plus slots. Each slot is filled by a fragment endpoint owned by the capability team, and each team deploys its endpoint alone.
When it is needed (and when it is NOT)
Good fit:
- Content-heavy, mostly read-oriented pages (claim summary, policy overview, customer dashboard) where SEO, fast first paint, and low client JavaScript matter.
- Several teams contribute regions to one page and want independent deployment.
- You are migrating from a server-rendered monolith (MVC/Razor) and want to carve out pieces gradually (pairs well with Day 49, Strangler Fig).
- Users are on slow devices or networks, so server rendering helps.
Poor fit:
- Highly interactive, stateful UIs (a drag-and-drop claims triage board, a live adjuster workbench). Fragments that must share rich client-side state fit micro-frontends (Day 53) or a single SPA better.
- One small team owns the whole UI. A modular monolith frontend is simpler and cheaper.
- Fragments need heavy cross-talk (fragment A updates fragment B on every click). You will re-invent an event bus in the browser and lose the simplicity.
- You cannot tolerate the extra server-side latency and failure surface of the composer and cannot invest in caching and timeouts.
How to identify the problem (key signals)
- Frontend release frequency is much lower than backend release frequency (backends ship daily, the UI monthly).
- Pull requests from three or more teams regularly touch the same layout, routing, or shared component files, with frequent merge conflicts.
- “Frontend freeze” or “release train” meetings exist mainly to coordinate teams whose changes are unrelated.
- One team’s failing UI test blocks unrelated teams’ deployments (a single shared CI pipeline is red for reasons nobody else owns).
- Backend teams file tickets to “the frontend team” for small changes and wait weeks.
- Bundle size and build times grow every quarter; nobody owns performance of the whole page.
- Framework upgrades (for example an Angular major upgrade) are postponed for years because they must happen everywhere at once.
Flow Diagram
A thin shell fetches fragments in parallel with timeouts and assembles one HTML page.
flowchart LR BR["Browser"] --> FD["Front Door"] FD --> SH["Claims Web shell"] SH -- "400 ms budget" --> F1["Policy UI fragment"] SH -- "400 ms budget" --> F2["Documents UI fragment"] SH -- "400 ms budget" --> F3["Payments UI fragment"] SH <--> RC[("Redis fragment cache")] F3 -. "timeout: fallback HTML" .-> SH SH -- "composed page" --> BRLevel 1: Beginner
Analogy: a newspaper front page. The layout (masthead, columns) is fixed by the editor, but sports, finance, and weather desks each write their own box. On press night the boxes are dropped into the layout and one page is printed. Each desk can change its box without asking the others.
The simplest possible version in ASP.NET Core: a page that calls a fragment endpoint on another service and inlines the returned HTML.
// Shell service: Program.cs (.NET 10, minimal API)using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);builder.Services.AddHttpClient("policy", c => c.BaseAddress = new Uri("http://policy-ui"));var app = builder.Build();
app.MapGet("/claims/{id}", async (string id, IHttpClientFactory f) =>{ var client = f.CreateClient("policy"); // Fragment owned by the Policy team var policyHtml = await client.GetStringAsync($"/fragments/policy-panel?claimId={id}");
var page = $""" <!doctype html> <html><body> <h1>Claim {System.Net.WebUtility.HtmlEncode(id)}</h1> <section id="policy">{policyHtml}</section> </body></html> """; return Results.Content(page, "text/html");});
app.Run();// Policy service: owns its fragmentapp.MapGet("/fragments/policy-panel", (string claimId) =>{ var html = "<div class=\"panel\"><h2>Coverage</h2><p>Deductible: 500</p></div>"; return Results.Content(html, "text/html");});Beginner rules: the fragment returns a piece of HTML, not a full page; the shell decides where it goes; if the fragment fails, the shell must still render something.
Level 2: Intermediate
In a real .NET + Angular + SQL database application you rarely inline strings. Typical structure for the claims portal:
- Shell (Claims Web): ASP.NET Core Razor Pages or MVC, owns layout, navigation, authentication cookie, and the composition step.
- Fragment services: Policy UI, Documents UI, Payments UI, each an ASP.NET Core app returning Razor partial views over its own database (Database per Service, Day 5).
- Angular is used only where interactivity is needed, as an “island” inside a fragment (for example, an upload widget), loaded from that team’s own static path.
A typed composer with per-fragment timeout and fallback (the fallback ties back to Day 29):
public record FragmentResult(string Html, bool Degraded);
public sealed class FragmentComposer(IHttpClientFactory factory, ILogger<FragmentComposer> log){ public async Task<FragmentResult> GetAsync( string client, string path, string fallbackHtml, CancellationToken ct) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromMilliseconds(400)); // per-fragment budget try { var http = factory.CreateClient(client); using var resp = await http.GetAsync(path, cts.Token); resp.EnsureSuccessStatusCode(); return new(await resp.Content.ReadAsStringAsync(cts.Token), false); } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { log.LogWarning(ex, "Fragment {Client}{Path} degraded", client, path); return new(fallbackHtml, true); } }}The page model fetches fragments in parallel, so total time is roughly the slowest fragment, not the sum:
public class ClaimModel(FragmentComposer composer) : PageModel{ public FragmentResult Policy = default!, Documents = default!, Payments = default!;
public async Task OnGetAsync(string id, CancellationToken ct) { var p = composer.GetAsync("policy", $"/fragments/policy-panel?claimId={id}", "<p>Policy details are temporarily unavailable.</p>", ct); var d = composer.GetAsync("documents",$"/fragments/documents?claimId={id}", "<p>Documents are temporarily unavailable.</p>", ct); var m = composer.GetAsync("payments", $"/fragments/payments?claimId={id}", "<p>Payments are temporarily unavailable.</p>", ct); await Task.WhenAll(p, d, m); (Policy, Documents, Payments) = (p.Result, d.Result, m.Result); }}<!-- Claim.cshtml --><h1>Claim @Model.Id</h1><section aria-label="Policy">@Html.Raw(Model.Policy.Html)</section><section aria-label="Documents">@Html.Raw(Model.Documents.Html)</section><section aria-label="Payments">@Html.Raw(Model.Payments.Html)</section>Html.Raw is only safe because the fragments come from services you own over a trusted internal network and are themselves encoded by Razor. Never compose HTML from user-supplied or third-party sources this way.
Other practical points:
- Contract: agree a fragment contract (URL, query parameters, CSS class prefix, allowed HTML, events). Without it, teams break each other silently (see Day 51, Consumer-Driven Contract Test).
- Styling: use a shared design system CSS and a per-team class prefix (
pol-,doc-) to avoid collisions. - Authentication: the shell authenticates the user and forwards a token or identity headers to fragments (Day 38, Access Token).
- Angular islands: a fragment can include
<claim-upload></claim-upload>plus a script tag for that team’s own Angular Elements bundle. Keep the number of islands small.
Level 3: Advanced
Performance:
- Parallel fetch, tight per-fragment timeouts, and a page-level time budget. Total latency is the slowest fragment plus composer overhead.
- Cache fragments by their natural key. A policy panel that rarely changes can be cached for minutes with
Cache-Controland revalidated with ETags; a payments panel is short-lived or uncached. UseIMemoryCache/IDistributedCachein the composer, or ASP.NET Core output caching on the fragment service. - Stream the page. Send the shell head and above-the-fold content first and stream slower fragments as they arrive (ASP.NET Core can flush the response incrementally; Blazor SSR supports streaming rendering). Use skeleton placeholders for the slow parts.
- Lazy fragments: for below-the-fold content, render a placeholder and let the browser fetch the fragment after load (a small amount of JavaScript or htmx-style attributes).
Scalability: the composer is stateless and scales horizontally. The dominant risk is fan-out: one page view causes N internal calls, so a traffic spike multiplies load on every fragment service. Protect them with rate limits, caches and bulkheads (Days 27 and 30).
Security:
- Injection: fragments are HTML. A compromised or buggy fragment service can inject script into the entire page (same origin). Treat fragment services as trusted code, sanitise anything user-generated inside them, and add a Content-Security-Policy with nonces.
- Authorisation belongs inside each fragment service; the shell hiding a panel is not security.
- Do not forward the browser’s full cookie set to every fragment; forward only the identity data each one needs.
- CSRF tokens for forms inside fragments must be issued in a way that the shell and the fragment agree on.
Failure modes:
- One slow fragment drags the whole page (no timeout). Fix: budgets and fallbacks.
- Circular or deep composition (fragment includes a fragment includes a fragment) multiplies latency. Limit depth to one.
- Version skew: shell expects markup that a fragment has changed. Fix: contract tests and additive-only changes.
- Duplicate assets: two fragments each load their own copy of a script or CSS. Fix: shared design system and explicit asset manifest.
- Partial failure showing misleading content (for example payments panel blank looks like “no payments”). Show an explicit “unavailable” state.
Common mistakes: building a distributed “god shell” that knows every fragment’s internals; using fragments for highly interactive widgets; skipping observability so nobody knows which fragment made the page slow; no fallback content.
Level 4: Expert and Architect view
Alternatives compared:
| Approach | Composition point | Independent deploy | SEO / first paint | Interactivity | Main cost |
|---|---|---|---|---|---|
| Server-side fragment composition (this topic) | Server or edge | Yes, per fragment | Excellent | Limited, islands only | Composer latency, fragment contracts |
| Client-side composition / micro-frontends (Day 53) | Browser | Yes, per micro-app | Weaker unless SSR added | Excellent | Bundle duplication, runtime integration, shared state |
| Single SPA (Angular) with modular libraries | Build time | No, one pipeline | Weaker without SSR | Excellent | Coordinated releases |
| Server-rendered monolith (MVC/Razor) | In one app | No | Excellent | Limited | Team coupling |
| Iframes | Browser | Yes | Poor | Isolated | Poor UX, sizing, accessibility, deep links |
| Edge-side includes (CDN/proxy) | Edge | Yes | Excellent | Limited | Feature-limited, vendor-specific |
Combines well with: Service per Team (Day 4), Database per Service (Day 5), API Gateway and BFF (Days 19 and 20) for the shell, Circuit Breaker, Bulkhead and Fallback (Days 26, 27, 29) inside the composer, Consumer-Driven Contract Tests (Day 51) for fragment contracts, Strangler Fig (Day 49) to migrate a legacy UI piece by piece, and Distributed Tracing (Day 31) to attribute slow pages to a fragment.
ADR-style justification:
- Title: ADR-052 Compose the Claim Details page from server-rendered fragments.
- Context: four teams (Claims, Policy, Documents, Payments) share one frontend release pipeline; UI releases are monthly while backends ship daily; the customer portal needs good SEO and first paint on mobile.
- Decision: the Claims Web shell composes HTML fragments served by each capability team, with a per-fragment timeout of 400 ms, parallel fetch, cached fragments where data allows, and explicit fallback content. Interactive needs are met with small Angular Elements islands owned by the fragment’s team.
- Consequences (positive): independent deployments, clearer ownership, smaller client bundles, better first paint. Consequences (negative): extra server hops, a contract to maintain, harder cross-fragment interactions, need for good tracing and caching.
- Alternatives rejected: micro-frontends now (interactivity needs are low, integration overhead not justified); keeping a single SPA (does not remove the release bottleneck).
- Revisit when: pages become highly interactive or fragments need shared client state.
Azure implementation
Service names and tier names below are stable, but check current pricing pages before budgeting; prices and included quotas change.
Services that implement or support this topic:
- Hosting fragment services and the shell: Azure Container Apps (simple, scale-to-zero, built-in ingress) or Azure App Service (Linux, .NET 10). AKS when you already run Kubernetes (Day 46).
- Edge and caching: Azure Front Door (Standard or Premium) for global entry, TLS, WAF and caching of static and cacheable responses. Front Door does not assemble fragments for you (it is not an ESI engine); composition happens in your shell service.
- Internal routing: Azure Container Apps ingress with internal environments, or Application Gateway / API Management in front for the shell’s public APIs.
- Cache: Azure Cache for Redis (or Azure Managed Redis, the newer offering that is replacing the older Azure Cache for Redis tiers over time; check the current retirement timeline) as a distributed cache for composed fragments.
- Secrets and identity: Managed Identity plus Azure Key Vault for service-to-service auth; Microsoft Entra ID for user sign-in.
- Observability: Application Insights with OpenTelemetry (Azure Monitor OpenTelemetry Distro for .NET) so one trace shows the shell and each fragment call.
- Static assets for Angular islands: Azure Blob Storage static website or Azure Static Web Apps behind Front Door.
How to configure (key points):
- Deploy the shell and each fragment service as separate Container Apps, each with its own revision and CI/CD pipeline. Fragment apps use internal ingress only; the shell is external.
- Give the shell a system-assigned managed identity; fragment services validate the caller’s token (Entra ID app roles) rather than trusting the network alone.
- Put Front Door in front of the shell; enable WAF policy; cache only responses marked
Cache-Control: public(never personalised claim pages). - Add the OpenTelemetry distro so trace context (
traceparent) flows on every fragment call; alert on p95 per fragment. - Set Container Apps min replicas above zero for the shell and latency-sensitive fragments to avoid cold starts on the critical path.
Pricing and tier considerations:
- Container Apps: Consumption plan bills per vCPU-second, GiB-second and requests, with a monthly free grant; Dedicated (workload profiles) suits steady traffic. Scale-to-zero saves money but adds cold-start latency, so keep minimum replicas for composer-critical services.
- App Service: priced by plan (Basic, Standard, Premium v3/v4 family); one plan can host several small fragment apps, at the cost of shared resources (Day 44 trade-off).
- Front Door: Standard and Premium have a monthly base fee per profile plus per-request and data-transfer charges; Premium adds private link origins and advanced WAF rules. Choose Premium only if you need private origins.
- Redis: cost driven by tier and size; start small, size by cached fragment volume.
- Application Insights: billed by data ingested; sample high-volume fragment traces.
Reference architecture (text): Browser to Front Door (WAF, TLS, static asset caching) to Claims Web shell (Container App, external ingress, managed identity). The shell fans out in parallel over internal ingress to Policy UI, Documents UI and Payments UI (each Container App with its own database: Azure SQL, PostgreSQL Flexible Server, Blob Storage). The shell reads and writes a Redis cache for cacheable fragments. All apps emit OpenTelemetry to Application Insights and Log Analytics. Key Vault holds secrets; Entra ID handles user and service identities. Angular Elements island bundles live in Blob Storage behind Front Door.
Teaching guide for my team
Beginner, 2 minutes: “Think of the claim page as a newspaper front page. The Claims team owns the layout. Policy, Documents and Payments each write their own box. When you open the page, the server fetches all the boxes and prints one page. If a box is late, we print ‘temporarily unavailable’ in that spot instead of failing the whole page. Each team can change its own box without asking anyone else.”
Intermediate, 5 minutes: Explain the shell, fragment endpoints and contract; show the FragmentComposer and why fragments are fetched in parallel with a timeout; explain caching per fragment and why personalised HTML must not be cached publicly; explain that interactivity is limited to small islands; contrast with micro-frontends (browser composes) and a single SPA (one pipeline); finish with the risks: fan-out load, contract drift, injection through fragments.
Hands-on exercise: build a shell and two fragment services (Policy and Payments) in .NET 10 using the code above. Make the Payments fragment sleep 2 seconds on request. Expected outcome: with a 400 ms timeout the page still loads in about 400 ms with the Payments panel showing the fallback message, a warning is logged, and the Policy panel renders normally. Then add Cache-Control: max-age=60 plus caching in the composer to the Policy fragment and confirm the second request does not call the Policy service (check logs).
Interview-style questions:
- How does server-side fragment composition differ from micro-frontends? Answer: composition happens on the server or edge and delivers finished HTML, so it is good for SEO and simple content; micro-frontends compose in the browser and suit rich interactivity but cost bundle size and runtime integration.
- What happens when one fragment is slow or down? Answer: without protection the whole page is slow; use per-fragment timeouts, parallel fetching, cached copies, and explicit fallback content, plus circuit breakers on repeated failure.
- What are the security risks? Answer: fragments run in the page’s origin, so a bad fragment can inject script; mitigate with trusted services only, encoding inside fragments, Content-Security-Policy, and authorisation enforced in each fragment service.
Mastery checklist
- I can explain why a monolithic frontend blocks independent deployment even when the backend is split.
- I can build a shell that fetches fragments in parallel with timeouts and fallbacks.
- I can decide which fragments are cacheable and which must never be cached publicly.
- I can define a fragment contract (URL, parameters, CSS prefix, markup rules) and test it with consumer-driven contracts.
- I can name the security risks of composing HTML and the mitigations (CSP, encoding, per-fragment authorisation).
- I can trace a slow page to the fragment responsible using distributed tracing.
- I can compare this pattern with micro-frontends, a single SPA and iframes, and justify the choice in an ADR.
- I can describe an Azure hosting layout including Front Door, Container Apps, Redis and Application Insights.
Key takeaway
Split the page, not just the backend: let each team own and deploy its own server-rendered fragment, compose them in a thin shell with timeouts, caching and fallbacks, and keep interactivity to small islands.
