# 05. Real-World Architecture Patterns

The earlier chapters explained the mechanics of JavaScript in browsers. This final chapter answers the engineering question: given those mechanics, how should real frontend systems be structured?

Production frontend architecture is mostly about managing tradeoffs:

- initial load speed vs runtime flexibility
- server work vs client work
- caching aggressiveness vs freshness
- local simplicity vs shared state coordination
- bundle size vs feature richness
- team velocity vs long-term maintainability

Interviewers ask about these patterns because they reveal whether you can think past syntax and into system behavior.

This chapter connects back to the entire handbook:

- [01-javascript-fundamentals.md](./01-javascript-fundamentals.md), because closures, modules, and object models still shape large apps
- [02-javascript-in-the-browser.md](./02-javascript-in-the-browser.md), because architecture sits on top of browser runtime constraints
- [03-dom-event-loop-rendering.md](./03-dom-event-loop-rendering.md), because UI performance and scheduling determine what architectures feel fast
- [04-networking-storage-security.md](./04-networking-storage-security.md), because data fetching, caching, and auth drive most application complexity

## SPA vs MPA

One of the first architecture splits in web applications is whether the app behaves primarily like an MPA, a multi-page application, or an SPA, a single-page application.

### MPA: Multi-Page Application

In an MPA, navigation usually loads a new document from the server. Each page request returns fresh HTML.

Strengths:

- simpler mental model for navigation
- strong default SEO story
- server remains the center of rendering and routing
- less client-side JavaScript is often needed for basic experiences

Tradeoffs:

- full-page navigations can feel heavier
- state continuity across pages is less automatic
- rich app-like interactions may require more partial enhancement work

### SPA: Single-Page Application

In an SPA, the browser loads an application shell and subsequent navigation often updates view state without full document reloads.

Strengths:

- highly interactive UI flows
- smoother in-app transitions
- client-side state can persist across route changes
- fits well with component-driven architectures such as React and Vue

Tradeoffs:

- larger JavaScript cost up front if not optimized
- more complex client-side routing and state handling
- SEO and initial load behavior require deliberate design
- more architectural pressure on data fetching, hydration, caching, and error recovery

### High-Level Comparison

```mermaid
flowchart LR
    A[User action] --> B{App style}
    B -- MPA --> C[Browser navigates to new document]
    C --> D[Server returns HTML]
    D --> E[Browser parses and renders page]
    B -- SPA --> F[Client router updates route]
    F --> G[Fetch route data if needed]
    G --> H[Render updated view in existing page]
```

### Why This Tradeoff Exists

SPAs move more application coordination into the browser. MPAs keep more of it on the server. Neither is universally better. The right choice depends on product behavior, team expertise, SEO needs, latency sensitivity, and operational constraints.

Many modern systems are hybrids rather than pure versions of either.

## CSR, SSR, and Hydration

These terms describe how UI markup gets produced and when a page becomes interactive.

### CSR: Client-Side Rendering

With CSR, the browser loads JavaScript that then builds or updates the UI primarily on the client.

Benefits:

- flexible rich interactivity
- natural fit for component state and dynamic routing
- server can act more like a data API layer

Costs:

- initial render may be delayed by JavaScript download, parse, and execution
- poor bundle discipline can hurt startup badly
- SEO and low-end devices may suffer if CSR is the only strategy

### SSR: Server-Side Rendering

With SSR, the server sends HTML for the requested view. The page can often show useful content earlier because the browser does not need to wait for all client rendering logic before seeing structure.

Benefits:

- faster meaningful content for many pages
- stronger SEO by default
- better performance for users on weak devices or slow networks

Costs:

- server rendering complexity
- coordination between server-rendered markup and client behavior
- more architectural attention needed around caching and personalization

### Hydration

Hydration is the process where client-side JavaScript attaches interactivity to HTML that already exists from server rendering.

This is a powerful compromise:

- server provides initial HTML quickly
- client attaches event handlers and stateful behavior afterward

But hydration is not free. The browser still has to download, parse, and run the JavaScript, then connect it to the existing DOM.

### Render Strategy Diagram

```mermaid
flowchart TD
    A[User requests page] --> B{Strategy}
    B -- CSR --> C[Server sends minimal HTML and JS assets]
    C --> D[Browser downloads JS]
    D --> E[Client renders UI]
    B -- SSR --> F[Server renders HTML]
    F --> G[Browser displays content]
    G --> H[Client JS loads]
    H --> I[Hydration attaches interactivity]
```

### Streaming and Progressive Delivery

Modern frameworks increasingly support streaming HTML or data so the browser can start showing useful content sooner instead of waiting for the entire page to be fully ready.

This follows the same browser principles from earlier chapters: progressive work generally feels faster than large all-at-once work.

## State Management Patterns

The phrase "state management" is often used too broadly. In production systems, the first question is not "Which library should we use?" It is "What kind of state is this?"

### Common State Categories

1. Local UI state
2. Server state
3. Shared client application state
4. URL state
5. Form state

### Local UI State

Examples:

- whether a modal is open
- current tab selection
- text inside an input

This state usually belongs close to the component or feature using it.

### Server State

Examples:

- product lists from an API
- current user profile from the backend
- notifications feed

This state is different because the server is the source of truth. It has freshness, invalidation, and cache concerns. Treating server state like plain local state often creates bugs and redundant refetching.

### Shared Client State

Examples:

- theme
- feature flags already loaded into the client
- cross-cutting workflow state

This state may justify a shared store, but only when many parts of the app truly need coordinated access.

### URL State

Examples:

- search query
- pagination page
- selected filters
- current route segment

If state changes should be shareable, restorable, or navigable with back/forward history, the URL is often the right place for it.

### Why Misclassifying State Hurts Architecture

If every piece of state gets pushed into one global store, the app becomes harder to reason about. If important cross-cutting state is scattered locally, coordination becomes painful.

Good frontend architecture often begins with correct state classification.

## State Flow in a Real App

```mermaid
flowchart LR
    A[User interaction] --> B[UI component state]
    B --> C[Action or event]
    C --> D[Server state cache or shared store]
    D --> E[API client]
    E --> F[Backend]
    F --> D
    D --> G[Derived view model]
    G --> H[Rendered UI]
```

This kind of layered flow is common in React and Vue apps even when the exact libraries differ.

## Caching Strategies

Caching in frontend systems is layered. Many teams think only about one layer and miss the rest.

### Browser HTTP Cache

The browser can cache network responses according to HTTP cache headers. This helps with:

- repeat visits
- static assets
- reducing unnecessary transfers

### CDN Cache

Before requests even reach your origin, a CDN may serve cached assets or responses from edge locations. This is especially valuable for static JavaScript bundles, images, and sometimes cacheable HTML or API responses.

### In-Memory App Cache

Client apps often keep recent data in memory so route changes or repeated views do not trigger unnecessary requests immediately.

### Service Worker Cache

Service workers can intercept requests and implement custom caching logic, enabling offline support and app-shell strategies.

### Cache Layer Diagram

```mermaid
flowchart TD
    A[User navigates or requests data]
    A --> B{Browser cache hit?}
    B -- Yes --> C[Serve cached response]
    B -- No --> D{CDN cache hit?}
    D -- Yes --> E[Serve from edge cache]
    D -- No --> F[Origin server]
    F --> G[Response stored according to policy]
    G --> H[Client memory cache may retain parsed data]
```

### Stale-While-Revalidate Thinking

A common production strategy is to show cached data quickly and refresh in the background. This gives users fast response while still converging toward fresh data.

The important architectural lesson is that freshness and latency are usually in tension. Good caching is about choosing where to sit on that tradeoff, not pretending there is no tradeoff.

## Lazy Loading and Code Splitting

Large frontend apps fail slowly before they fail visibly. Bundle size creeps up, startup cost increases, and low-end devices begin to struggle.

Code splitting is how you avoid shipping every feature to every user on day one of a session.

### Common Strategies

- route-based splitting
- component-level lazy loading
- loading heavy admin or analytics features only when needed
- deferring non-critical third-party scripts

### Why It Works

Users rarely need the full app graph immediately. If the first screen needs only a subset of code, shipping less upfront lowers download, parse, and execution cost.

### Tradeoffs

- more network boundaries at runtime
- loading states become part of UX design
- too much fragmentation can create request overhead and complexity

Good architecture chooses meaningful split points rather than slicing code randomly.

## A Practical Frontend Architecture for React- or Vue-Style Apps

Many real applications settle into a layered shape.

### Common Layers

1. App shell and bootstrapping
2. Router
3. Feature modules or route modules
4. Shared design system and UI primitives
5. API client and data-fetching layer
6. Server-state cache and invalidation logic
7. Local state and component logic
8. Observability, error reporting, and performance instrumentation

### Example Architecture Diagram

```mermaid
flowchart TD
    A[Browser loads app shell] --> B[Router]
    B --> C[Route module]
    C --> D[Feature components]
    D --> E[Design system primitives]
    C --> F[State and business logic]
    F --> G[API client]
    G --> H[Backend or BFF]
    F --> I[Server-state cache]
    I --> D
    C --> J[Analytics and error reporting]
```

### Why This Structure Works

- the router controls high-level navigation concerns
- feature modules keep domain logic close to the screens that need it
- design-system components reduce duplicated UI implementation
- API and cache layers centralize network policy and invalidation rules
- observability is treated as infrastructure rather than an afterthought

This is the difference between a demo app and a production system. Production systems deliberately separate concerns that would otherwise tangle together under growth.

## BFFs, Edge Logic, and Backend Coordination

Many large frontend systems talk not directly to many microservices, but to a BFF, a backend-for-frontend.

Why teams do this:

- reduce over-fetching and under-fetching
- tailor APIs to UI needs
- centralize auth and aggregation logic
- shield the browser from backend complexity

This is especially relevant in system design interviews because frontend architecture and backend API design are tightly coupled.

## Performance Is an Architectural Concern, Not a Last-Mile Concern

Teams often treat performance like a final optimization pass. That usually fails.

Performance is shaped by architecture from the start:

- choosing CSR-only vs SSR or hybrid rendering
- deciding bundle boundaries
- choosing cache strategies
- classifying state correctly
- deciding how much work runs on the main thread
- avoiding unnecessary DOM churn

If you get those decisions wrong, no amount of local component optimization will fully rescue the app.

## What Strong Frontend Engineers Optimize For

In production, strong frontend architecture usually tries to improve all of these together:

- fast first meaningful render
- quick route transitions
- resilient data loading and retry behavior
- understandable state ownership
- security-safe auth handling
- small enough bundles and incremental loading
- observability for failures and regressions
- a codebase structure teams can extend without chaos

That combination is what interviewers are really probing when they ask broad architecture questions.

## Interview-Ready Summary

- MPAs center navigation and rendering on the server; SPAs keep a longer-lived client runtime and often do in-place route transitions.
- CSR, SSR, and hydration are different tradeoffs about where markup is produced and when interactivity arrives.
- Not all state is the same; local UI state, server state, shared client state, and URL state should usually be managed differently.
- Caching is layered across browser cache, CDN, app memory, and sometimes service workers.
- Lazy loading and code splitting reduce startup cost by shipping less JavaScript up front.
- Real frontend architecture usually includes an app shell, router, feature modules, a data layer, caches, shared UI primitives, and observability.
- Performance and maintainability are architectural properties, not just component-level concerns.

## Closing Perspective

If you can explain the browser using the full path covered by this handbook, you are operating at a strong engineering level:

- JavaScript language fundamentals
- browser runtime boundaries
- DOM, rendering, and scheduling
- networking, storage, and security
- production application architecture

That path is what turns "I know JavaScript" into "I can reason about modern web systems from first principles."