Back to Blog

Portfolio Architecture: Observer Pattern and Three-Layer Fallback

10 min read
Next.js 15TypeScriptObserver PatternISRArchitecture

A portfolio site should be the simplest thing a developer builds. Mine is not. Not because I over-engineered it for the sake of complexity, but because a portfolio is the one project where every architectural decision is visible to the people evaluating your work. This post walks through three patterns I chose and why.

Why a Portfolio Needs Architecture

Most portfolio sites are static pages with some CSS. That works fine if you are showcasing design. When your specialty is systems engineering and tool programming, the portfolio itself needs to demonstrate those skills. Hiring managers looking for someone who can architect game systems want to see architectural thinking — even in a web project.

This site runs on Next.js 15 with React 19, TypeScript, Tailwind CSS 4, Three.js for the 3D hub, and Framer Motion for animations. It deploys to both Vercel and Netlify for redundancy. Under that stack, three patterns define the architecture.

Pattern 1: Observer with Re-Entrancy Guard

The deployment observer manages events like build status changes, configuration updates, and theme switches. Subscribers register handlers, and the observer dispatches events to all of them using Promise.allSettled so a single failing handler does not block the rest.

The re-entrancy guard solves a subtle problem. If an observer handler emits another event during processing (for example, a theme change triggers a config update), the naive approach would recurse into the emit function and potentially deadlock or process events out of order.

// Re-entrancy guard in the DeploymentObserver
async emit(event: PortfolioEvent): Promise<ObserverResult[]> {
  // If we're already emitting, queue instead of recursing
  if (this.emitting) {
    this.eventQueue.push(event);
    return [];
  }

  this.emitting = true;
  try {
    // Dispatch to all observers with Promise.allSettled
    // Failed observers get one automatic retry
    const settled = await Promise.allSettled(
      observerList.map(observer => observer(event))
    );
    // ... process results
  } finally {
    this.emitting = false;
  }

  // Drain queued events after current emission completes
  while (this.eventQueue.length > 0) {
    const queued = this.eventQueue.shift();
    if (queued) await this.emit(queued);
  }
}

The guard sets a boolean flag during emission. Events arriving while the flag is set go into a queue. Once the current emission completes, the queue drains sequentially. This guarantees every event is processed exactly once, in order, without recursion.

This is the same pattern I use in Unreal Engine for delegate-based event systems. The domain changed (web instead of game engine), but the problem — safe asynchronous event dispatching — is identical.

Pattern 2: Three-Layer Error Fallback

A portfolio that crashes during a hiring manager's visit is worse than having no portfolio. The fallback strategy has three layers, each catching failures the previous layer cannot handle.

1

Route-level error boundary

error.tsx catches exceptions within any route segment. It renders a styled error page inside the existing layout (Navbar and Footer remain visible) with a "Try Again" button that calls React's reset() to re-render the segment.

2

Root layout crash handler

global-error.tsx activates when the root layout itself fails. Because the layout (and its CSS, fonts, and components) may be broken, this file renders standalone HTML with inline styles. No external dependencies — it works even if the entire component tree is down.

3

Static maintenance page

public/maintenance.html is a zero-dependency static file served directly by the CDN. If Next.js itself fails to boot — a bad deployment, a runtime crash in the server — the CDN can serve this page with contact information and a professional message.

Each layer is independent. Layer 2 does not import from Layer 1. Layer 3 does not use React at all. This isolation means a failure in one layer cannot cascade into the next.

Pattern 3: ISR-Powered GitHub Enrichment

The projects page displays live data from GitHub — stars, last push date, primary language — alongside curated descriptions. This data is fetched at build time using Next.js Incremental Static Regeneration (ISR) with a one-hour revalidation window.

// GitHub fetch with ISR revalidation
export async function fetchGitHubRepos(
  username: string
): Promise<GitHubRepo[]> {
  const res = await fetch(
    `https://api.github.com/users/${username}/repos`,
    {
      headers,
      next: { revalidate: 3600 }, // Re-fetch every hour
    }
  );
  return res.json();
}

// Enrichment merges live GitHub data with curated project data
export function enrichProjectWithGitHub(
  project: { repoName?: string },
  repos: GitHubRepo[]
): EnrichedGitHub {
  const repo = repos.find(
    r => r.name.toLowerCase() === project.repoName?.toLowerCase()
  );
  return {
    lastPushed: repo?.pushed_at ?? null,
    stars: repo?.stargazers_count ?? 0,
    liveLanguage: repo?.language ?? null,
  };
}

The key design decision was separating curated content from live data. Project descriptions, roles, and technical highlights are authored in a TypeScript data file that I control. GitHub stars and activity come from the API. ISR means visitors see fresh data without runtime API calls on every page load, and the site remains fully functional even if GitHub's API is temporarily unavailable — it just serves the last cached version.

Architectural Thinking Transfers

None of these patterns are web-specific. The Observer pattern with a re-entrancy guard works the same way in Unreal Engine delegates. The three-layer fallback mirrors how game engines handle crash recovery (in-game error screen, engine-level crash reporter, OS-level crash dump). ISR is conceptually similar to asset caching in game builds.

That transferability is the point. The technology stack changes between projects, but architectural principles — event-driven decoupling, defense-in-depth error handling, smart caching — stay constant. This portfolio is built to demonstrate that thinking as much as any individual project in it.

See It in Action

The full portfolio source code is available on GitHub. The Observer implementation, error boundaries, and ISR configuration are all in the repository.