<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://career-catalogue.pages.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://career-catalogue.pages.dev/" rel="alternate" type="text/html" /><updated>2026-07-16T20:08:13+00:00</updated><id>https://career-catalogue.pages.dev/feed.xml</id><title type="html">Career Catalogue</title><subtitle>Here I mainly talk about engineering, leadership, product, delivery, AI and the occasional side project.</subtitle><author><name>Ryan Howells</name></author><entry><title type="html">Squad-Based Agentic Workflows: When the Team Costs More Than the Work</title><link href="https://career-catalogue.pages.dev/squad-based-agentic-workflow/" rel="alternate" type="text/html" title="Squad-Based Agentic Workflows: When the Team Costs More Than the Work" /><published>2026-07-16T10:54:28+00:00</published><updated>2026-07-16T10:54:28+00:00</updated><id>https://career-catalogue.pages.dev/squad-based-agentic-workflow</id><content type="html" xml:base="https://career-catalogue.pages.dev/squad-based-agentic-workflow/"><![CDATA[<p>I was looking through my API usage the other day and had one of those quiet, uncomfortable moments every engineer knows. The output of my AI agent squad was the best I’d ever had. The bill was fifteen times what it should have been. Both of those things were true at once, and sitting with that contradiction taught me more about <strong>agentic workflows</strong> than any blog post I’d read on the subject.</p>

<p>For a couple of weeks I ran squad-based agent delivery on PocketRPG, my tick-based idle RPG. This is what I learned, when I’d still reach for a squad, and why I ultimately tore mine out.</p>

<h3 id="what-a-squad-actually-is">What a Squad Actually Is</h3>

<p>The idea is seductive because it maps directly onto how real software teams work. Instead of one agent doing everything, you define a roster of roles as markdown charters that live in your repo: a technical BA that turns a feature request into a breakdown with acceptance criteria, a read-only architect that produces the implementation plan, domain builders (in my case a gameplay engineer, a frontend designer and a backend developer) that write the code, and an adversarial QA agent that tears the finished diff apart at the end.</p>

<p>A lead session triages each task, spawns the roles it needs, hands each one a prompt with a goal, a set of owned files and acceptance criteria, then integrates the results. Builders get <strong>disjoint file ownership</strong> so they never fight over the same file. QA judges the integrated diff against the written criteria rather than trusting the builders’ own reports.</p>

<p>And I want to be honest here: it works. An agent told “you are adversarial, your job is to find where this change is wrong” catches bugs that a generalist agent, fresh off writing the code, sails straight past. The BA step catches ambiguity before anyone writes a line. The quality was real.</p>

<h3 id="the-bill-that-matters">The Bill That Matters</h3>

<p>Anthropic’s engineering write-up on their <a href="https://www.anthropic.com/engineering/multi-agent-research-system">multi-agent research system</a> puts a number on the cost: multi-agent setups burn roughly <strong>fifteen times the tokens</strong> of a normal session. My experience matched that almost exactly. Squad-tier tasks on PocketRPG came in at around 15x what the same work would have cost in a single session.</p>

<p>The multiplier stops being mysterious once you trace where the tokens go. Every spawned agent starts cold. Each of my five or six roles independently re-loaded the always-on project context, re-read the same engine files the lead had already read, got re-briefed on constraints the lead already knew, and then wrote a prose report the lead had to hold in its own context to integrate. Same files, same rules, paid for five times per task.</p>

<p>There’s a subtler cost too, and Cognition’s <a href="https://cognition.com/blog/dont-build-multi-agents">Don’t Build Multi-Agents</a> is the best writing on it: the telephone game. When you split dependent work across agents, decision-making disperses and context degrades at every handoff. Their example is a Flappy Bird clone where one subagent builds a Super Mario background and another builds a bird that moves nothing like the real thing. It’s funny right up until your backend agent and your frontend agent make subtly incompatible assumptions about a save-format field, and QA is the first to notice.</p>

<h3 id="when-a-squad-earns-its-keep">When a Squad Earns Its Keep</h3>

<p>I don’t think the architecture is wrong. I think it’s specific. Reading across Anthropic’s post and the wider research, a squad pays for itself when three things are all true.</p>

<p>The work has to <strong>decompose into genuinely independent directions</strong>. Breadth-first research is the canonical case: due diligence, competitive analysis, literature review. Ten subagents reading different sources in parallel, each compressing findings into a summary, is a real win because the parallelism is real.</p>

<p>The subtasks have to <strong>benefit from walled-off context</strong>. Specialised code review is a good example. Security, performance and API-contract reviewers don’t need to coordinate, so fanning them out costs little and each specialist genuinely beats a generalist.</p>

<p>And the task’s value has to <strong>cover the multiplier</strong>. Anthropic is blunt about this: the economics only work when the answer is worth a lot of tokens.</p>

<p>Sequential feature work on a single codebase fails all three. My architect’s plan fed the builders, and the builders’ diff fed QA. Nothing actually ran in parallel; everyone waited on everyone else, so I paid the coordination tax and never collected the parallelism dividend. Everyone needed the <em>same</em> context, which is precisely the situation the research flags as a bad fit. And “add a slayer task chain to an idle RPG” is not legal due diligence. The answer was never worth 15x.</p>

<h3 id="what-i-moved-to">What I Moved To</h3>

<p>The insight that made the migration easy is that the squad’s quality never came from process isolation. It came from <strong>role discipline</strong>: the BA’s evidence-first breakdown, the architect’s file-level plan, QA’s adversarial framing. Discipline transfers to a single agent for free. You don’t need a separate process to make an agent wear a different hat; you need the hat written down.</p>

<p>PocketRPG now runs what I call a delivery loop. One agent, one session, moving through dedicated steps. The role charters became step charters. Plan always runs and does the old BA work, resolving unknowns from repo evidence and emitting executable success criteria. Architect runs only when the work crosses a real boundary, like a save-format change or a migration. Engineer is one step for all the code, whichever domain it touches. World Designer runs for content and map work, which in a game codebase is a genuine discipline of its own. QA always runs, still adversarial, judging the raw diff against the criteria <em>written down in the Plan step</em> rather than against what the engineer step concluded.</p>

<p>The inter-agent plumbing got replaced by three small habits. The agent announces each step as it enters it, so the discipline is auditable in the transcript. Each step ends with a five-line handoff note covering decisions, files touched and open findings, so later steps rely on notes instead of re-reading. And a triage fast-path skips the loop entirely for trivial work, because ceremony on a typo fix burns tokens the same way a squad does, just more slowly.</p>

<p>I kept exactly one kind of subagent: read-only search agents for broad fan-out questions.</p>

<ul>
  <li>“Where does anything touch the prayer drain pool?” -&gt; That’s a subagent.</li>
  <li>“Which screens still use the old modal component?” -&gt; That’s a subagent.</li>
  <li>“Now write the fix.” -&gt; That stays in my session, always.</li>
</ul>

<p>Those read-only agents are the inverted case. They move file dumps <em>out</em> of the main context and return only conclusions, so they reduce net tokens rather than multiplying them. Writes stay single-threaded, which is Cognition’s middle ground too.</p>

<p>The honest trade-off is reviewer independence. Squad QA started cold and couldn’t inherit the builder’s blind spots; step QA shares a context with the code it’s reviewing. Pre-written criteria recover most of that, because the QA step is graded against a contract that existed before the code did. And if quality ever slips, the cheap escalation is a single cold review on demand, not a standing squad.</p>

<h3 id="the-numbers-honestly">The Numbers, Honestly</h3>

<p>A study out of UIUC, summarised well in <a href="https://www.augmentcode.com/guides/single-agent-vs-multi-agent-ai">Augment Code’s comparison guide</a>, measured multi-agent overhead at anywhere from <strong>4x to 220x</strong> the tokens of a single-agent equivalent, and found that with the budget held equal, a single agent matches or beats the multi-agent setup on dependent, multi-hop work. Read that range the other way round: moving to a single agent saves you anywhere from 4x to over 200x on the same work, depending on how badly your task fits the pattern. I was living at the 15x mark. Some teams are paying far worse without knowing it.</p>

<hr />

<p>Ultimately, this comes down to the same instinct I apply everywhere else in engineering: measure what a practice costs, not just how it feels. The squad <em>felt</em> rigorous. Watching five specialists coordinate reads as maturity. But rigour that costs 15x and produces the same diff isn’t rigour, it’s theatre with a metered API key. Write the hats down, make one agent wear them in order, and spend the savings on shipping more.</p>]]></content><author><name>Ryan Howells</name></author><category term="ai" /><category term="agentic-workflows" /><category term="projects" /><summary type="html"><![CDATA[Running a squad of AI agents produced my best output ever at fifteen times the cost. What I learned before tearing it out.]]></summary></entry><entry><title type="html">The PocketRPG Helper: Building an AI Chatbot That Never Costs Me a Penny</title><link href="https://career-catalogue.pages.dev/building-a-free-chatbot/" rel="alternate" type="text/html" title="The PocketRPG Helper: Building an AI Chatbot That Never Costs Me a Penny" /><published>2026-07-02T08:06:57+00:00</published><updated>2026-07-02T08:06:57+00:00</updated><id>https://career-catalogue.pages.dev/building-a-free-chatbot</id><content type="html" xml:base="https://career-catalogue.pages.dev/building-a-free-chatbot/"><![CDATA[<p>I’ve been building PocketRPG, my tick-based idle RPG, for a while now, and something kept nagging at me. Players would hit a wall — how does prayer drain work, what should I train next, why did my inventory stop filling — and the answers lived either in my head or buried in a help screen nobody read. A static wall of text is fine, but nobody reads fourteen collapsible guides. They want to ask <em>their</em> question and get <em>their</em> answer.</p>

<p>So I built a helper. A little 💬 button in the corner of the game that answers questions about PocketRPG — and only PocketRPG. This post is about why I did it, and how I did it without adding a single penny to my hosting bill.</p>

<h3 id="the-why-that-matters">The “Why” That Matters</h3>

<p>The honest answer is that the game got deeper than the documentation. Idle supplies, slayer tasks, combo food, auto-banking tied to your Agility level — mechanics stack up, and every player arrives at them in a different order. A chatbot flips the model: instead of me guessing what players need to know, they tell me. And because it can read the player’s own character, the answer isn’t generic. “What should I do next?” gets answered with <em>your</em> stats, <em>your</em> slayer task, <em>your</em> bank.</p>

<p>The second reason is less noble: I wanted to see if I could do it for free. Properly free. Not “free until it isn’t.”</p>

<h3 id="standing-on-cloudflare">Standing on Cloudflare</h3>

<p>The whole game runs on Cloudflare Pages, so the chatbot lives there too, as a Pages Function. Workers AI gives you access to hosted open-source models with a free allocation of 10,000 “neurons” a day — neurons being Cloudflare’s billing unit for inference. The model I settled on is <strong>GLM-4.7-Flash</strong>, which is remarkably cheap ($0.06 per million input tokens), handles multi-turn tool calling well, and — I learned this the hard way — is actually still available. My first choice had been deprecated a month before I wired it up, and I spent an evening staring at error 5028 wondering what I’d broken.</p>

<h3 id="grounding-it-a-knowledge-index-and-an-mcp-server">Grounding It: A Knowledge Index and an MCP Server</h3>

<p>A model on its own will happily invent drop rates. I didn’t want that, so the helper is grounded two ways.</p>

<p>First, a <strong>knowledge index</strong>. My player guide is a markdown file; a build script chops it into chunks and bakes it into the worker. Every question runs a lexical search over it, and the best matches get handed to the model as context with strict instructions: answer from this, or say you don’t know.</p>

<p>Second, <strong>tools</strong>. PocketRPG already has an MCP server so AI assistants can play the game, and the chatbot borrows a slice of it — a read-only allowlist. It can inspect items, look up monsters, check your slayer task. It cannot spend, equip, or sell anything, and the character ID is pinned server-side so it can only ever see the person asking.</p>

<h3 id="the-guardrail-never-pay-a-penny">The Guardrail: Never Pay a Penny</h3>

<p>This is the part I’m most pleased with. I’m on the Workers Paid plan, which means going over the free allocation quietly becomes a bill. I didn’t want a soft limit; I wanted a mathematical one.</p>

<p>Every message <strong>reserves its worst-case cost up front</strong> in a little D1 ledger — before any AI call happens. If the reservation doesn’t fit inside the day’s budget, no call is made. When the answer comes back, the actual token usage is metered and the unused reserve is refunded. The budget itself sits below the free allocation, with the gap sized so that even a message in flight can’t push actual spend over the line. Failures make it <em>more</em> conservative, never less: if anything goes wrong mid-request, the full reserve stays charged. There is no code path where I pay.</p>

<ul>
  <li>“What if two messages race for the last of the budget?” -&gt; The reserve is atomic; one of them loses.</li>
  <li>“What if the API doesn’t report usage?” -&gt; Keep the whole reserve.</li>
  <li>“What if a request gets killed mid-generation?” -&gt; The reserve was already counted.</li>
</ul>

<h3 id="when-the-budget-runs-out">When the Budget Runs Out</h3>

<p>The helper doesn’t just go dark when the day’s allocation is spent. It degrades to <strong>retrieval-only answers</strong>: the same knowledge search runs, and you get the relevant guide sections verbatim instead of a generated reply. Less polished, still useful, costs nothing. The player’s daily question isn’t counted against them for a degraded answer either — that felt only fair.</p>

<hr />

<p>Ultimately, this was a project about constraints. Free-tier limits, a deprecated model, a hard rule that I’d never pay for a single message — every one of those constraints made the design better. The result is a helper that knows the game, knows your character, admits what it doesn’t know, and runs on a budget of exactly zero. That’s a standard I can live with.</p>]]></content><author><name>Ryan Howells</name></author><category term="ai" /><category term="projects" /><summary type="html"><![CDATA[Building an AI helper for my idle RPG, PocketRPG, that answers players' questions in their own words - and costs me nothing to run.]]></summary></entry><entry><title type="html">PocketRPG Deepdive</title><link href="https://career-catalogue.pages.dev/pocketrpg-deepdive/" rel="alternate" type="text/html" title="PocketRPG Deepdive" /><published>2026-05-27T07:57:55+00:00</published><updated>2026-05-27T07:57:55+00:00</updated><id>https://career-catalogue.pages.dev/pocketrpg-deepdive</id><content type="html" xml:base="https://career-catalogue.pages.dev/pocketrpg-deepdive/"><![CDATA[<p>PocketRPG is an idle/simulation RPG I’ve been building. It takes its cues from a fantasy RPG I played as a kid: combat, skilling, gathering, raids, clues and minigames, all advancing on a 600ms tick that keeps running while the tab is in the background. It runs in the browser, the layout works on both phones and desktops, and the production stack currently costs me nothing to run.</p>

<p>This is a write-up of how it’s built, for a technical reader. I’ll cover the stack, the server-authority model, the way I’ve used Cloudflare’s free tiers, the save system, PvP, the economy and the responsive UI. I’ve left out anything that’s an actual secret or key; where a detail is sensitive I’ve described the mechanism instead.</p>

<h2 id="the-stack">The stack</h2>

<p>The client is Preact rather than React. For a UI that’s mostly lists, grids and one combat screen, the full React runtime would have been weight I didn’t need, and Preact is a few KB. State lives in a Preact context with hooks. The game logic sits in separate modules that don’t import anything from the UI.</p>

<p>That last point is a hard rule in the repo: <code class="language-plaintext highlighter-rouge">src/engine/</code> is pure game logic with no UI imports. The combat formulas, the XP curve and the loot rolls are all deterministic and can be tested on their own. The tick is 600ms, gameplay rounding always uses <code class="language-plaintext highlighter-rouge">Math.floor</code>, and the same inputs produce the same outputs every time. That determinism is what makes the regression tests worth having, and it’s also what later made server-side PvP feasible.</p>

<p>Dev and the normal build run on Vite. There’s an extra TypeScript transpile step feeding a custom concatenator, because the production artifact is a single <code class="language-plaintext highlighter-rouge">index.html</code>: every module transpiled and stitched into one file with Tailwind pulled in from a CDN, served as a static asset. For an offline-first app that means one file to cache and no module waterfall to resolve over a poor mobile connection.</p>

<p>Concatenating everything into one scope has a downside. Every top-level declaration has to be globally unique, or you get a duplicate-identifier error that breaks the whole app. A <code class="language-plaintext highlighter-rouge">check:single</code> step parses the output and fails the build if, say, two modules both define a top-level <code class="language-plaintext highlighter-rouge">formatGold</code>. It’s caught me a few times, which is the point.</p>

<h2 id="running-on-cloudflare">Running on Cloudflare</h2>

<p>The backend is Cloudflare Pages, Pages Functions and D1, with nothing else behind it.</p>

<ul>
  <li>Pages serves the static app. Bandwidth and requests are effectively unmetered on the free plan, which suits a single-file SPA.</li>
  <li>Pages Functions are the API. Every endpoint under <code class="language-plaintext highlighter-rouge">/api/*</code> is a function. The free plan has a daily request limit (100k/day at the time of writing). An idle game doesn’t need to talk to the server constantly, so that ceiling is comfortable.</li>
  <li>D1 is the database, SQLite at the edge. The free tier covers a few GB of storage and a daily allowance of rows read and rows written. The billing unit is rows, not connections or time.</li>
</ul>

<p>That billing model shaped the data design more than anything else, and I’ll come back to it. The result is that auth, cloud saves, a leaderboard, the economy and PvP all run for nothing a month, and the architecture doesn’t need to change when usage grows past the free tier. It’s the same Pages, Functions and D1; the only thing that changes is the bill. The design already respects the limits that scale would impose.</p>

<p>Deploys are driven from git. A push to a branch triggers a build and ship. Preview has its own D1 database and production has another, so I can test against realistic data on preview without touching live accounts. <code class="language-plaintext highlighter-rouge">wrangler.toml</code> wires the bindings, and secrets (the JWT signing key, OAuth client secrets, Stripe keys) live in the Pages dashboard rather than the repo.</p>

<h2 id="treating-the-client-as-untrusted">Treating the client as untrusted</h2>

<p>Most of the more interesting decisions come from one assumption: the browser is untrusted. Not because players are malicious, but because anything in client memory can be edited, and an editable client means there’s no real economy to speak of.</p>

<p>So the client computes and displays progression, but it doesn’t get to commit anything that matters. XP ticking up as you grind, coins from a common drop, the HP bar: the client handles those locally and they feel instant. When something with economic weight happens (a boss unique, a raid drop, a clue reward, a credit purchase, a PvP loot transfer) the server is the only thing that writes it, and it validates the change itself.</p>

<p>That’s easy to say and a steady discipline to keep, because the convenient option is always to trust the save blob the client uploads. I did a hardening pass to close that gap, and it left a clear split:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PUT /api/save</code> is a sync channel for non-economy state: UI settings, theme, the current task, HP. When a save arrives the server doesn’t store it wholesale. It diffs the incoming blob against its own copy and rejects any increase in protected items (boss, raid and clue uniques) that didn’t arrive through a proper grant. Items that legitimately drop client-side from monsters and clues are on an allowlist and pass; anything else trying to appear in the bank is discarded.</li>
  <li>Protected rewards go through dedicated action-completion endpoints: <code class="language-plaintext highlighter-rouge">/api/actions/monster/complete</code>, <code class="language-plaintext highlighter-rouge">/api/actions/raid/complete</code>, <code class="language-plaintext highlighter-rouge">/api/actions/clue/complete</code> and so on. Those are the only authoritative grant paths, and each one validates every reward item against the canonical drop table for its source. Request a raid unique from a monster that doesn’t drop it and the response is a 403.</li>
</ul>

<p>The save ends up in three zones: server-owned (XP, bank, inventory, equipment, slayer points, never accepted from the client), client-owned (UI state, current action, theme) and revalidated (levels, which have to match what the server recomputes from the XP it holds). The client can claim any combat level it likes; the server recomputes it and ignores the claim.</p>

<h2 id="saving-often-without-burning-rows">Saving often without burning rows</h2>

<p>An idle game has to save often. The tab can close at any moment and the player expects to return exactly where they left off. The client saves on a short cadence and on every meaningful event (a kill, a clue completion, a minigame finish, a level-up), plus a heartbeat every 30 seconds while an active task runs.</p>

<p>Saving often against a database billed per row written needs some care. The approach is that a save is one row. Each character has a single row in <code class="language-plaintext highlighter-rouge">saves</code>, and a write is one <code class="language-plaintext highlighter-rouge">UPDATE</code>. There’s no row per inventory slot or per skill; the whole state is one JSON document, gzipped into a blob column. Compression takes a fair chunk off a typical save, and a hard size ceiling stops a corrupted or oversized blob being stored.</p>

<p>Integrity comes from optimistic concurrency on a <code class="language-plaintext highlighter-rouge">save_revision</code> counter. Each save carries the revision it believes it’s editing, and the write is a compare-and-swap:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="n">saves</span>
   <span class="k">SET</span> <span class="n">save_blob</span> <span class="o">=</span> <span class="o">?</span><span class="p">,</span> <span class="n">save_data</span> <span class="o">=</span> <span class="o">?</span><span class="p">,</span> <span class="n">updated_at</span> <span class="o">=</span> <span class="o">?</span><span class="p">,</span> <span class="n">save_revision</span> <span class="o">=</span> <span class="n">save_revision</span> <span class="o">+</span> <span class="mi">1</span>
 <span class="k">WHERE</span> <span class="n">character_id</span> <span class="o">=</span> <span class="o">?</span> <span class="k">AND</span> <span class="n">save_revision</span> <span class="o">=</span> <span class="o">?</span>
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">meta.changes</code> is zero, something else advanced the state (another tab or device) and this write is stale. The server returns a 409 with the current revision, and the client pulls fresh and retries. There’s no auto-merge and no grace window. Last-write-wins on game state is how a stale background tab overwrites the raid drop you earned in the foreground a moment ago, so I’d rather the write fail and force a re-pull.</p>

<p>Some state is monotonic and must never move backwards: boss kill counts, the collection log, and the nonces that block replayed actions. Kept in the save blob, a last-write-wins moment or a backup restore could reset a kill count from 500 to 10. So those live in their own tables instead:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kill_counts</code>, incremented server-side with <code class="language-plaintext highlighter-rouge">kill_count = kill_count + 1</code>, so it only goes up.</li>
  <li><code class="language-plaintext highlighter-rouge">collection_log</code>, append-only, with a composite primary key of <code class="language-plaintext highlighter-rouge">(character_id, item_id, source_type, source_id)</code> so a replay is an idempotent no-op rather than a duplicate.</li>
  <li><code class="language-plaintext highlighter-rouge">action_nonces</code>. Every protected action carries a single-use nonce, and the server does an <code class="language-plaintext highlighter-rouge">INSERT … ON CONFLICT DO NOTHING</code> on it before anything else. If the row already exists it’s a replay and the action is refused. Because the nonce sits in its own table it stays consumed even if the save blob is rolled back, which matters: replay defence that depends on the save being trustworthy doesn’t work.</li>
</ul>

<p>A separate <code class="language-plaintext highlighter-rouge">character_idle_state</code> table is the authoritative record of when a character was last active and what task it was running, and the server stamps it with its own clock. Trusting the client clock for offline progression would let someone wind their system time forward a week and collect a week of rewards.</p>

<p>Reads stay cheap too. The leaderboard and the PvP combat-level band need <code class="language-plaintext highlighter-rouge">total_level</code> and <code class="language-plaintext highlighter-rouge">combat_level</code> for a lot of characters at once. Rather than joining <code class="language-plaintext highlighter-rouge">saves</code> and parsing a JSON blob per row in a Worker, those two values are denormalised onto the <code class="language-plaintext highlighter-rouge">characters</code> table and kept current on each save, so the leaderboard is a plain indexed <code class="language-plaintext highlighter-rouge">SELECT</code>.</p>

<h2 id="the-database-tables">The database tables</h2>

<p>The schema is spread across nineteen migrations but stays fairly small:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">oauth_identities</code>: one row per Google or GitHub login, i.e. the account.</li>
  <li><code class="language-plaintext highlighter-rouge">characters</code>: an identity owns many characters, with unique case-insensitive usernames. It carries the denormalised <code class="language-plaintext highlighter-rouge">total_level</code> and <code class="language-plaintext highlighter-rouge">combat_level</code>, the ironman and one-life flags, the credit balance, and <code class="language-plaintext highlighter-rouge">active_match_id</code>, which is the PvP lock.</li>
  <li><code class="language-plaintext highlighter-rouge">saves</code>: one per character. Gzipped blob, revision counter, server timestamp.</li>
  <li><code class="language-plaintext highlighter-rouge">character_idle_state</code>: authoritative last-active time and current task.</li>
  <li><code class="language-plaintext highlighter-rouge">collection_log</code>, <code class="language-plaintext highlighter-rouge">kill_counts</code>, <code class="language-plaintext highlighter-rouge">action_nonces</code>: the monotonic and dedup tables above.</li>
  <li><code class="language-plaintext highlighter-rouge">audit_events</code>: a durable log of high-value mutations such as credit grants and spends, protected reward claims, PvP settlements and trading-post movements. Cloudflare’s <code class="language-plaintext highlighter-rouge">console.log</code> output is ephemeral and hard to search, which is no help when a player asks where a drop went, so this is a real table I can query.</li>
  <li><code class="language-plaintext highlighter-rouge">stripe_events</code> and <code class="language-plaintext highlighter-rouge">purchase_grants</code>: payment idempotency and a per-transaction ledger, so a retried Stripe webhook can’t grant twice and I can answer “did this person receive what they paid for” without reading logs.</li>
  <li><code class="language-plaintext highlighter-rouge">trading_post_offers</code>: the whole player economy in one table.</li>
  <li><code class="language-plaintext highlighter-rouge">pvp_waiting_room</code>, <code class="language-plaintext highlighter-rouge">pvp_matches</code>, <code class="language-plaintext highlighter-rouge">pvp_invitations</code>, <code class="language-plaintext highlighter-rouge">pvp_intents</code>: PvP.</li>
</ul>

<p>Indexes get the most deliberate attention, because they aren’t free. Every secondary index is more data the database has to write and keep in order on each insert and update, and on a platform that bills per row written, an over-indexed table quietly multiplies the cost of every save. So the default is no index, and each one has to justify itself against a query that actually runs and runs often.</p>

<p>The indexes that exist map onto hot read paths. The leaderboard sorts by total level, so <code class="language-plaintext highlighter-rouge">characters</code> has a composite index on <code class="language-plaintext highlighter-rouge">(total_level DESC, id)</code>. The order book is searched by item and side at a given price, so <code class="language-plaintext highlighter-rouge">trading_post_offers</code> is indexed on <code class="language-plaintext highlighter-rouge">(item_id, offer_type, status, price, created_at)</code>, in that column order, because that’s the order the query filters and then sorts in. The PvP match loop reads unapplied intents by match and tick on every poll, so <code class="language-plaintext highlighter-rouge">pvp_intents</code> has a composite index on <code class="language-plaintext highlighter-rouge">(match_id, tick_number, character_id, character_seq)</code>. Pending invitations get their own index because the lobby polls for them. Composite indexes are ordered to match how a query reads, not alphabetically or by gut feel, since a leading column the query doesn’t filter on makes the index useless to it.</p>

<p>Several of these are partial, with a <code class="language-plaintext highlighter-rouge">WHERE</code> clause that trims the index down to the rows that are ever queried. The leaderboard index only covers <code class="language-plaintext highlighter-rouge">WHERE deleted_at IS NULL AND total_level &gt; 33</code>, so deleted and brand-new characters never take up space in it. The “one active match per character” constraint is a <code class="language-plaintext highlighter-rouge">UNIQUE</code> index with <code class="language-plaintext highlighter-rouge">WHERE status = 'active'</code>, which does two jobs at once: it enforces the rule (a second active match for either side fails on the constraint instead of on application code I’d have to remember to write) and it stays small, because completed and aborted matches drop straight out of it. A partial index is smaller, faster to scan and cheaper to maintain than a full one, so where a column has a clear “live” subset it’s the natural choice.</p>

<p>The other half of the decision is the tables left almost unindexed on purpose. <code class="language-plaintext highlighter-rouge">saves</code> is the hottest write path in the game, and it’s read and written almost entirely by primary key (<code class="language-plaintext highlighter-rouge">character_id</code>), so it carries nothing beyond that key; a secondary index there would tax every save for a query that doesn’t exist. Low-cardinality flags like the ironman boolean are the same story, since indexing a column with two possible values rarely helps the planner and still adds write cost. The line I hold is that an index is a standing tax on writes paid in exchange for one read being fast, so it’s only worth adding when that read is both real and frequent.</p>

<h2 id="authentication">Authentication</h2>

<p>There’s no password handling in PocketRPG: no password storage, no reset emails, no hashes to leak. Sign-in is Google or GitHub only.</p>

<p>It’s a standard three-leg OAuth flow, written directly against the Workers runtime with no SDK. Hitting <code class="language-plaintext highlighter-rouge">/api/auth/google</code> (or <code class="language-plaintext highlighter-rouge">/github</code>) generates a random state nonce, stores it in a short-lived <code class="language-plaintext highlighter-rouge">oauth_state</code> cookie (<code class="language-plaintext highlighter-rouge">HttpOnly; Secure; SameSite=Lax</code>, ten minutes) and redirects to the provider. The redirect URI is built from the incoming request’s host rather than hardcoded, so the same code works on preview and production. The callback checks the state cookie against the query parameter, exchanges the code for an access token using the provider client secret, and uses the provider’s stable user ID (<code class="language-plaintext highlighter-rouge">sub</code> on Google, <code class="language-plaintext highlighter-rouge">id</code> on GitHub) as the identity key.</p>

<p>Sessions are stateless JWTs signed with HS256 through the Web Crypto API, with no JWT library involved. The token holds the identity row ID, the provider and a display name, and expires after 30 days. Going stateless was deliberate: a protected request is verified with a signature check and no database round-trip, which keeps the per-request row cost at zero and scales horizontally without effort. <code class="language-plaintext highlighter-rouge">requireAuth</code> reads the bearer token, verifies the signature and expiry, and hands the endpoint a known identity. Characters attach to that identity by <code class="language-plaintext highlighter-rouge">owner_id</code>.</p>

<p>The trade-off is worth stating. A stateless 30-day JWT held client-side scales well but is awkward to revoke. Moving it into an <code class="language-plaintext highlighter-rouge">HttpOnly</code> cookie and adding a token-version check for instant revocation is still on the list rather than done.</p>

<h2 id="pvp-and-the-tick">PvP and the tick</h2>

<p>PvP is where the determinism work paid off, and also where I had to be realistic about what free infrastructure can do.</p>

<p>The engine is server-authoritative and shared. A single pure-logic module, <code class="language-plaintext highlighter-rouge">pvpEngine.js</code>, runs both in the client (for a responsive view) and inside the Pages Function (as the source of truth). Clients don’t send “I hit you for 14”. They send intents tagged with a tick number (“eat the food in slot 3”, “queue a special”, “switch to the second weapon”), the server applies them, and it produces the next authoritative state. The client renders state it doesn’t own.</p>

<p>Because the engine is deterministic, the server resolves a tick unambiguously. Within a tick, intents apply in a fixed order (<code class="language-plaintext highlighter-rouge">tick_number</code>, then <code class="language-plaintext highlighter-rouge">character_id</code>, then a per-character sequence number), timers decrement, then both attacks resolve off the snapshot taken at the start of resolution, so neither player sees the other’s hit before their own lands. If both players die on the same tick, the lower character ID wins. The outcome is identical on every run, which is what you need when two clients and a server all have to agree on what happened.</p>

<p>The 600ms tick is the unit of fairness here. Attack speed, freeze duration, stun length and special-attack energy regen are all counted in ticks rather than wall-clock milliseconds, so latency changes when you see an outcome, not the outcome itself.</p>

<p>Match concurrency is optimistic, like saves. Both clients poll and try to advance the tick, and the advancing <code class="language-plaintext highlighter-rouge">UPDATE</code> carries <code class="language-plaintext highlighter-rouge">WHERE current_tick = ?</code>. Whoever lands first takes the tick; the other gets <code class="language-plaintext highlighter-rouge">changes === 0</code>, re-reads and continues. The database does the arbitration, and it’s the one component both clients already share.</p>

<p>The lock that keeps PvP consistent with the rest of the game is <code class="language-plaintext highlighter-rouge">characters.active_match_id</code>. While it’s set, <code class="language-plaintext highlighter-rouge">/api/save</code>, <code class="language-plaintext highlighter-rouge">/api/idle</code> and <code class="language-plaintext highlighter-rouge">/api/purchase</code> all refuse writes for that character. Inventory during a match lives inside the match state and is written back atomically when the match ends. That’s what makes loot transfer trustworthy: the loser’s tradeable items move to the winner’s bank in the same atomic batch that ends the match, coins are special-cased so they transfer despite being flagged untradeable elsewhere, and a forfeit runs through the same path as a death.</p>

<p>Version one of the transport is HTTP polling, roughly every 600ms, so on mobile the state on screen is 600–1500ms stale. It’s near-real-time rather than real-time. I chose polling because it costs nothing and it’s correct, and the upgrade path is already designed: move the match loop into a Durable Object behind a WebSocket. The engine, schema and UI stay as they are and only the transport changes, so I’m not writing combat twice.</p>

<h2 id="the-economy">The economy</h2>

<p>The most important economic decision in PocketRPG is that there’s barely any player-to-player economy at all.</p>

<p>MMO economies inflate because the world spawns endless common goods, players sell them to one another, and the gold supply climbs. PocketRPG doesn’t have that loop. The trading post is one table, <code class="language-plaintext highlighter-rouge">trading_post_offers</code>, and its order book only accepts three kinds of item: boss uniques, raid uniques and clue rewards, the genuinely rare drops. Everything else falls into one of two buckets. Common items with a shop value are instant-sold to the game at a fixed price, which is a sink with no player on the other side. Everything beyond that is untradeable.</p>

<p>A single gate function decides this: untradeable items are rejected, order-book items (the uniques) are allowed onto the book, and anything else with a shop value goes to the instant-sell path. The same check runs on both listing and search, server-side, so the client can’t push an item onto the market that doesn’t belong there.</p>

<p>The order book is an escrow system. Listing an offer debits your coins or items first and writes them into escrow on your save, and only then inserts the offer and runs matching against the opposing side (cheapest sells first for a buyer, best bids first for a seller, with any price improvement refunded inline). If matching fails after escrow there’s a compensating refund, so you’re never debited for an offer that didn’t take. Partial fills accumulate into <code class="language-plaintext highlighter-rouge">coins_pending</code> and <code class="language-plaintext highlighter-rouge">items_pending</code> for collection later. Concurrent fills against the same resting offer use a conditional relative update, <code class="language-plaintext highlighter-rouge">quantity_remaining = quantity_remaining - ? WHERE quantity_remaining &gt;= ?</code>, so if two buyers race for the last unit the second write affects zero rows, raises an “offer changed” error, and matching moves on to the next offer.</p>

<p>There’s a deliberate gold sink on the way out, too. You can instant-sell an existing listing: it detaches from you, stays in the book at its original price for someone else to buy, and pays you 80%. The other 20% disappears. A small, unique-only economy leaves very few places for inflation to start, and sinks like this cover the rest.</p>

<p>Ironman accounts can’t use any of it, and the server enforces that rather than leaving it to the client to hide the button.</p>

<h2 id="responsive-layout">Responsive layout</h2>

<p>PocketRPG started mobile-first and looked bad on a desktop for a long time: full-width nav tabs stranded across a 1440px viewport, single columns everywhere, no hover behaviour. Fixing it followed one rule: additive only, with the mobile DOM left untouched.</p>

<p>Every desktop change is a Tailwind breakpoint prefix or a <code class="language-plaintext highlighter-rouge">hidden md:block</code> / <code class="language-plaintext highlighter-rouge">md:hidden</code> toggle. The mobile layout doesn’t change at all; desktop is layered on at <code class="language-plaintext highlighter-rouge">md:</code> (768px). That breakpoint is where the bottom tab bar becomes a side navigation rail, single columns become master/detail, and grids widen out. Inventory goes from 4 columns on a phone to 10 on a wide screen, the bank goes to 12, and active combat splits into three panes (monster on the left, log and actions in the middle, inventory, prayers and spells on the right) so you can see the whole fight rather than scrolling it.</p>

<p>The goal was a desktop layout that feels like a desktop game (side navigation, multiple columns, hover and keyboard support, Esc to close modals) rather than a phone frame centred on a big screen. Mobile keeps the compact, thumb-friendly layout with 44px tap targets. The components and logic are shared; only the available space differs. Since it’s all CSS and conditional layout rather than separate screens, none of it needed new logic tests.</p>

<p>Two things caught me out. When a scrolling combat log becomes a grid child it needs <code class="language-plaintext highlighter-rouge">min-h-0</code> on the container or the auto-scroll quietly stops working. And hover styles need wrapping in <code class="language-plaintext highlighter-rouge">@media (hover: hover)</code>, or they stick on touch devices after a tap. Both are easy fixes that stay invisible until a real device shows them.</p>

<h2 id="ci-and-deployment">CI and deployment</h2>

<p>The pipeline is deliberately simple. GitHub Actions runs on every push and pull request: install, then <code class="language-plaintext highlighter-rouge">npm run ci</code>, which is the full build, the single-file rebuild and the <code class="language-plaintext highlighter-rouge">check:single</code> duplicate-identifier guard. The logic regression tests (Vitest, logic-only and deterministic) gate the build through a <code class="language-plaintext highlighter-rouge">prebuild</code> hook, so a build can’t be produced with failing tests. Deployment is Cloudflare Pages watching the repo: the preview branch goes to the preview environment and its own database, and main goes to production. There’s no separate CI server or deploy script to maintain.</p>

<p>The commit gate I run locally is the same one CI runs, so passing locally and passing in CI mean the same thing. For a single-file artifact with global-scope constraints, the duplicate-identifier check is the one that catches the most real problems.</p>

<h2 id="what-id-do-again">What I’d do again</h2>

<p>A few things I’d repeat on another project of this shape. Designing to the free tier’s actual constraint (for D1, that it bills per row) is what produced the one-row save, the denormalised leaderboard columns and the stateless JWT, and the result was better than if the limit hadn’t been there. Deciding what the server owns up front matters, because retrofitting authority onto a system that trusted the client is slow and error-prone; I know because I did some of it. And keeping the free version and the eventual paid version on the same code, as with PvP polling now and a Durable Object later, means scaling is a transport and a billing change rather than a rewrite.</p>

<p>PocketRPG is still in progress. The JWT-to-cookie move, the WebSocket PvP transport and more economy sinks are all still open. The structure holds up, it costs nothing to run today, and the point at which it starts costing something will be the point at which enough people are playing for that to be fine.</p>

<p>The game’s live if you want to try to break it.</p>]]></content><author><name>Ryan Howells</name></author><category term="projects" /><category term="ai" /><summary type="html"><![CDATA[A deep dive into PocketRPG, my browser-based idle RPG that runs on a 600ms tick and costs nothing to host.]]></summary></entry><entry><title type="html">Modern Society: Objectively Great, Subjectively Awful?</title><link href="https://career-catalogue.pages.dev/modern-society-objectively-great-subjectively-awful/" rel="alternate" type="text/html" title="Modern Society: Objectively Great, Subjectively Awful?" /><published>2026-03-18T19:37:19+00:00</published><updated>2026-03-18T19:37:19+00:00</updated><id>https://career-catalogue.pages.dev/modern-society-objectively-great-subjectively-awful</id><content type="html" xml:base="https://career-catalogue.pages.dev/modern-society-objectively-great-subjectively-awful/"><![CDATA[<p>This post is a change of pace for me. I usually stick to technical or career-focused topics, but I have been thinking about a specific shift in modern society lately. It felt like a conversation worth starting here.</p>

<p>The spark for this was a Jimmy Carr stand-up routine. He talked about how we are fundamentally misaligned with how incredible our current lives are compared to the past. He made the point that we live in a world that would look like a utopia to our ancestors, yet we often feel more dissatisfied than ever. I started digging into the data to see if the objective facts back this up.</p>

<h3 id="the-objective-success">The Objective Success</h3>

<p>If you look at the actual metrics of human life, the progress is undeniable. We have reached a level of safety and comfort that was once impossible.</p>

<ul>
  <li><strong>Childhood Survival:</strong> Global child mortality has dropped significantly. What used to be a common family tragedy is now a rare event in most parts of the world.</li>
  <li><strong>Hot Showers:</strong> We have instant access to hot water on demand. This was a luxury reserved for royalty only a few generations ago.</li>
  <li><strong>Information:</strong> We carry the entire sum of human knowledge in our pockets. Education and real-time news are accessible to almost everyone.</li>
  <li><strong>Societal Growth:</strong> Freedom of speech, diversity, and inclusion are at historic highs. While not perfect, the opportunity to succeed regardless of background is greater than ever.</li>
  <li><strong>Safety:</strong> Global poverty and hunger have seen massive declines. In a broad historical context, violent crime rates are remarkably low in most developed societies.</li>
</ul>

<h3 id="the-subjective-disconnect">The Subjective Disconnect</h3>

<p>If the stats are so positive, why is the general mood so heavy? The issue is that while the floor of human existence has been raised, the foundations of a stable life are becoming harder to reach.</p>

<p>Housing is the clearest example of this. We live in a society where a mobile phone, which is a piece of technology more advanced than what put humans on the moon, is affordable. However, a basic house is not. For many people under thirty, property ownership feels like a dream. We can buy the toys of modern life, but the security of a home is slipping away.</p>

<p>Social media complicates this further. It is a powerful tool for free speech and instant connection, but it can also be a toxic influence on young people. It presents a fake reality where we compare our daily lives to a stranger’s curated highlight reel. This creates a persistent sense of lack and fuels depression because we want what others only appear to have.</p>

<h3 id="breaking-the-consumer-mould">Breaking the Consumer Mould</h3>

<p>Capitalism is responsible for the innovation that gives us medical breakthroughs and technology. Yet, it also thrives on our dissatisfaction. It is easy to fall into a cycle where money becomes the only metric of success. We chase the latest designer clothes or the newest mobile just to feel like we are keeping up.</p>

<p>Stepping away from that consumerist lifestyle is good for your health and your bank account. Breaking the mould that society has created involves realising that stuff is not a substitute for well-being. The pressure to constantly upgrade is a trap that keeps you on a treadmill of work and spending.</p>

<h3 id="the-importance-of-gratitude">The Importance of Gratitude</h3>

<p>The solution to this modern malaise is not necessarily more progress. It is self-reflection.</p>

<p>Gratitude is not about ignoring the housing crisis or the negatives of digital life. It is about grounding yourself in the fact that, objectively, we are some of the luckiest people to ever live.</p>

<p>Taking a moment to appreciate the miracles like hot water, clean air, and the ability to speak our minds changes the internal narrative. It is about being mindful of what we actually have instead of focusing on what society tells us we are missing.</p>]]></content><author><name>Ryan Howells</name></author><category term="reflections" /><summary type="html"><![CDATA[A change of pace: why life has never been objectively better, yet so many of us feel subjectively worse.]]></summary></entry><entry><title type="html">Motivation in Engineers</title><link href="https://career-catalogue.pages.dev/motivation-in-engineers/" rel="alternate" type="text/html" title="Motivation in Engineers" /><published>2026-03-18T02:49:11+00:00</published><updated>2026-03-18T02:49:11+00:00</updated><id>https://career-catalogue.pages.dev/motivation-in-engineers</id><content type="html" xml:base="https://career-catalogue.pages.dev/motivation-in-engineers/"><![CDATA[<p>One of the most persistent myths in engineering management is that motivation is something you “do” to people. We often approach it like a simple checklist item: identify the lack of energy, assign a motivation task, such as a lunch and learn or a generic shout-out, and move it to “done.”</p>

<p>In reality, motivation is a delicate ecosystem built on four specific distinctions: <strong>Hygiene</strong>, <strong>Mastery</strong>, <strong>Autonomy</strong>, and <strong>Purpose</strong>. Proper leadership is not about finding one secret lever; it is about maintaining a constant balance between these pillars. If any one of them is neglected, the connection between the engineer’s craft and the impact of their code begins to break down. You do not motivate an engineer by adding external pressure; you motivate them by focusing on this balance and removing the friction that prevents them from doing their best work.</p>

<h3 id="1-the-foundation-hygiene-factors">1. The Foundation: Hygiene Factors</h3>

<p>Before you can inspire an engineer to reach for the “stretch zone”, which is the space just beyond their current comfort level but before the point of total frustration, you must ensure the baseline is covered. Hygiene factors, including salary, benefits, work-life balance, and basic tools, are the table stakes of management.</p>

<p>These factors are critical for retention, but they are ineffective at driving day-to-day engagement. If an engineer is working on a legacy monolith with no test coverage and a deployment process that takes six hours, a 5% bonus will not make them excited to wake up on Monday. Hygiene factors keep people from leaving, but they do not create the drive to innovate. Once the hygiene is settled, you must look toward intrinsic motivators to move the needle.</p>

<h3 id="2-the-right-level-of-challenge-mastery">2. The Right Level of Challenge (Mastery)</h3>

<p>Engineers are professional problem solvers. When the problems disappear or become repetitive, boredom sets in.</p>

<p>The most engaged engineers are those working in the stretch zone. As a manager, your job is to catalog the specific technical interests of your team. If someone wants to learn Rust, find a low-risk internal tool they can build with it. If they are fascinated by distributed systems, let them lead the discovery phase for the next service.</p>

<h3 id="3-radical-ownership-autonomy">3. Radical Ownership (Autonomy)</h3>

<p>Nothing kills motivation faster than being told exactly how to implement a solution. When we hand an engineer a 20-page technical specification and tell them to “just code it,” we are not hiring an engineer; we are hiring a compiler.</p>

<p>High-performing teams thrive when they are given problems, not solutions. Instead of saying, “Build a caching layer using Redis with a 10-minute TTL,” try saying, “Our API latency is spiking during peak hours, and it is hurting our conversion rate. How should we fix it?” Giving away the “how” is the only way to get true buy-in.</p>

<h3 id="4-the-line-of-sight-purpose">4. The Line of Sight (Purpose)</h3>

<p>We often bury our engineers under layers of abstraction. They see the pull request, the build pipeline, and the cloud infrastructure, but they rarely see the human on the other side of the screen. To fix this, you must shorten the feedback loop between code and impact. Motivation thrives when an engineer is exposed to the closing of a cycle, where they can see their contribution reach its destination and solve a real world problem.</p>

<p>This is where the importance of regular sprint ends becomes clear. These milestones are not just for tracking progress; they provide a vital psychological finish line. Closing a sprint provides a sense of achievement and a regular opportunity to see the loop fully closed. Without these intentional moments of completion, the work can feel like an endless stream of tasks without a clear destination.</p>

<ul>
  <li><strong>Share the Why:</strong> Explain how a technical refactor reduces infrastructure costs, which in turn allows the company to invest in more headcount or better tools.</li>
  <li><strong>Expose the Pain:</strong> Let engineers sit in on user research sessions. Watching a customer struggle with an interface lag is a far more powerful motivator than a generic bug report.</li>
  <li><strong>Celebrate the Completion:</strong> When the sprint ends and a feature is delivered, do not just move on to the next priority. Document the impact of that work and show the team exactly how they closed the gap for the user.</li>
</ul>

<h3 id="building-a-motivation-framework">Building a Motivation Framework</h3>

<p>If you are looking to reset your team’s energy, start by looking at your 1:1s. Stop using that time for status updates; that is what Teams and standups are for. Instead, ask questions that uncover the friction:</p>

<ul>
  <li>“What part of our stack makes you feel like you are fighting the tools rather than building features?”</li>
  <li>“If you had a week to work on anything in our codebase, what would it be?”</li>
  <li>“Do you feel like you have the ‘why’ behind our current roadmap?”</li>
</ul>

<p>Motivation is not a speech you give; it is the environment you build. If you protect their time, respect their craft, and keep these four distinctions in focus, the motivation takes care of itself.</p>]]></content><author><name>Ryan Howells</name></author><category term="leadership" /><category term="engineering-management" /><summary type="html"><![CDATA[Motivation isn't a task you tick off. Rethinking how engineering managers actually create the conditions for motivated teams.]]></summary></entry><entry><title type="html">Implementing Fuzzy Search with Levenshtein Distance</title><link href="https://career-catalogue.pages.dev/fuzzy-search-with-levenshtein/" rel="alternate" type="text/html" title="Implementing Fuzzy Search with Levenshtein Distance" /><published>2026-03-17T20:17:07+00:00</published><updated>2026-03-17T20:17:07+00:00</updated><id>https://career-catalogue.pages.dev/fuzzy-search-with-levenshtein</id><content type="html" xml:base="https://career-catalogue.pages.dev/fuzzy-search-with-levenshtein/"><![CDATA[<p>Vladimir Levenshtein, a Soviet mathematician, developed the concept of edit distance in 1965 whilst working at the Keldysh Institute of Applied Mathematics. His work was originally aimed at correcting errors in binary codes transmitted through noisy communication channels, where bits could be deleted, inserted, or substituted during transmission. This mathematical foundation eventually became a cornerstone for modern string matching, providing a way to quantify the similarity between two sequences.</p>

<p>Exact string matching is often insufficient for production search systems because it fails to account for user typos or character variations. Implementing a fuzzy search mechanism allows the system to return relevant results based on string similarity rather than binary equality.</p>

<h3 id="the-levenshtein-distance-algorithm">The Levenshtein Distance Algorithm</h3>

<p>Levenshtein distance, or edit distance, is a metric used to measure the difference between two sequences. It represents the minimum number of single-character edits required to transform one string into another.</p>

<p>The allowed operations are:</p>
<ol>
  <li><strong>Insertion</strong>: Adding a character.</li>
  <li><strong>Deletion</strong>: Removing a character.</li>
  <li><strong>Substitution</strong>: Replacing one character with another.</li>
</ol>

<h3 id="application-in-bioinformatics">Application in Bioinformatics</h3>

<p>In the field of genetics, this algorithm is fundamental to DNA sequence alignment. By treating the four nucleotide bases (A, C, G, T) as characters in a string, researchers use edit distance to identify mutations. Substitutions represent point mutations, whilst insertions and deletions (indels) account for bases added or lost during replication. Identifying the minimum number of edits allows scientists to determine the evolutionary distance between species or locate specific genetic disorders. Surprisingly, though, we aren’t using it to map the human genome, we just want to make sure the search bar works when someone forgets how to spell “plumber.”</p>

<h3 id="application-in-search-systems">Application in Search Systems</h3>

<p>In a search context, the Levenshtein distance is typically used alongside a predefined threshold. For example, a maximum edit distance of 2 is often sufficient to catch common typing errors without introducing excessive noise into the results.</p>

<ol>
  <li><strong>Preprocessing</strong>: Strings are normalised (lowercased, whitespace trimmed) before calculation.</li>
  <li><strong>Filtering</strong>: The algorithm runs against a candidate set of results to identify those within the acceptable distance threshold.</li>
  <li><strong>Ranking</strong>: Results are sorted by distance, where a lower distance indicates a higher relevance.</li>
</ol>

<p>Using this algorithm provides a more robust search interface that accounts for human error and improves result discoverability.</p>]]></content><author><name>Ryan Howells</name></author><category term="algorithms" /><category term="engineering-practice" /><summary type="html"><![CDATA[From a 1965 Soviet error-correction problem to modern string matching - implementing fuzzy search with Levenshtein distance.]]></summary></entry><entry><title type="html">Test Driven Development (TDD) and AI</title><link href="https://career-catalogue.pages.dev/test-driven-development-and-ai/" rel="alternate" type="text/html" title="Test Driven Development (TDD) and AI" /><published>2026-03-10T20:07:03+00:00</published><updated>2026-03-10T20:07:03+00:00</updated><id>https://career-catalogue.pages.dev/test-driven-development-and-ai</id><content type="html" xml:base="https://career-catalogue.pages.dev/test-driven-development-and-ai/"><![CDATA[<h2 id="so-what-is-tdd">So, what is TDD?</h2>

<p>TDD is a software development process that relies on the repetition of a very short development cycle. First, the developer writes an initially failing automated test case that defines a desired improvement or new function. Then, they produce the minimum amount of code to pass that test, and finally, they refactor the new code to acceptable standards.</p>

<p>This approach was largely pioneered by Kent Beck in the late 1990s as part of the Extreme Programming (XP) movement. Over the last two decades, it developed into a massive industry standard. It promised us a world of self-documenting code, fewer regressions, and modular design. Yet, if we are being honest, the industry has largely treated TDD as an academic ideal rather than a daily reality. The shift in mindsets toward velocity often means the feedback loop we crave is the first thing to be sacrificed.</p>

<h3 id="my-experience-with-the-gold-standard">My Experience with the “Gold Standard”</h3>

<p>In my own experience, TDD has been successful but limited in its application. I found myself only reaching for it when dealing with logic-heavy requirements. In those specific scenarios, the feedback loop was tight and the confidence it provided was unmatched.</p>

<p>But for everything else, such as simple CRUD operations or basic API integrations, TDD often felt like a tax. The mental overhead of writing the grunt work tests before the implementation led to a mindset where testing became an afterthought. We often did it at the end of a sprint to satisfy a coverage metric rather than to actually drive the design of the code.</p>

<h3 id="ai">AI</h3>

<p>The rise of AI in our IDEs has changed this equation. We have moved past simple autocomplete. We are now in a position where we can use AI as a partner in crime to facilitate a true TDD workflow.</p>

<p>The biggest barrier to TDD has always been the cognitive load of the blank page. Writing the initial test suite requires a level of energy that many find draining. Now, we can ask our AI partner to build the testing suite based directly on the requirement, analyze the scope, and prompt for the best approach.</p>

<p>This is not about letting the tool write the code for us. It is about letting it frame the problem so we can focus on the solution.</p>

<h3 id="so-lets-let-ai-drive-right">So, let’s let AI drive? Right?</h3>

<p>No. Before we get too ahead of ourselves, it is important to acknowledge that AI misses the mark, a lot.</p>

<p>While it can be incredibly helpful, it sometimes misses the core business logic. Conversely, it can over-test for perfection, generating a suite that is far too verbose or focused on irrelevant details like simple property mappings. It creates noise where we need clarity.</p>

<p>As experts in our field, we are ultimately responsible for the standardisation and readability of our solutions. This includes the test suites. We cannot simply outsource our judgment. I have found it still needs significant hand-holding to ensure the tests align with the specific standards we want to maintain as a team.</p>

<h3 id="summary">Summary</h3>

<p>The grunt work, including the setup, the mocks, and the repetitive assertions, was previously what we left until the very last minute. By using AI to handle this heavy lifting at the start of the cycle, we can finally return to a true Test First mentality.</p>

<p>When the boilerplate is generated in seconds, the excuse for not doing TDD evaporates. We can spend our mental energy on the architecture and the logic while the AI handles the initial scaffolding.</p>

<p>We are seeing a fundamental shift. AI has given us the ability to close the feedback loop faster than ever before. It handles the friction so we can focus on the craft. If you have struggled to make TDD stick in the past, the barrier to entry has never been lower. However, the driver is very much still the human.</p>]]></content><author><name>Ryan Howells</name></author><category term="engineering-practice" /><category term="ai" /><category term="testing" /><summary type="html"><![CDATA[How test-driven development and AI complement each other, and why writing the test first still matters in an AI-assisted workflow.]]></summary></entry><entry><title type="html">Closing the Feedback Loop</title><link href="https://career-catalogue.pages.dev/closing-the-feedback-loop/" rel="alternate" type="text/html" title="Closing the Feedback Loop" /><published>2026-03-06T21:59:50+00:00</published><updated>2026-03-06T21:59:50+00:00</updated><id>https://career-catalogue.pages.dev/closing-the-feedback-loop</id><content type="html" xml:base="https://career-catalogue.pages.dev/closing-the-feedback-loop/"><![CDATA[<h2 id="closing-the-feedback-loop">Closing the Feedback Loop</h2>

<p>A high-performing team is rarely defined by technical ability alone. Instead, it is often the rhythm of the team and how they interact with the wider business that determines success. While many organisations focus on the mechanics of a sprint, the most effective teams prioritising the feedback loop that exists at the end of every two-week cycle.</p>

<h3 id="creating-a-positive-forum-for-value">Creating a Positive Forum for Value</h3>

<p>The fortnightly sprint demo should be more than a status update. It is a dedicated, positive forum where the team can demonstrate the tangible value they have delivered. This regular interaction ensures that stakeholders are closely aligned with the work as it evolves, providing a platform for constructive dialogue.</p>

<p>By sharing progress every two weeks, the team creates a consistent drumbeat of delivery. This frequent engagement builds a level of trust that is difficult to achieve through reports or emails alone. It allows stakeholders to see the craftsmanship behind the features and understand the complexities the team is navigating.</p>

<h3 id="the-impact-on-team-morale">The Impact on Team Morale</h3>

<p>Demonstrating work to a live audience has a profound effect on a team’s internal culture. Software engineering can often feel abstract, but presenting a functional feature to the person who requested it provides immediate validation.</p>

<p>When a team sees their work being appreciated and understands how it solves a specific problem, it fosters a sense of ownership and pride. This direct connection to the business outcome is a significant driver of engagement. Statistics from Gallup suggest that teams receiving regular, meaningful feedback see a substantial increase in productivity and a reduction in turnover, as members feel their contributions are recognisable and valued.</p>

<h3 id="closing-the-loop-effectively">Closing the Loop Effectively</h3>

<p>The feedback loop is only successful if it is actually closed. Gathering input during a demo is the first step, but the second step is demonstrating how that input has been processed.</p>

<p>Closing the loop involves a clear follow-up. If a stakeholder provides a suggestion, the team needs to communicate how that fits into the roadmap. Even if a request cannot be prioritised immediately, acknowledging it and explaining the reasoning prevents the stakeholder from feeling ignored. This transparency ensures the relationship remains a partnership rather than a transactional request-and-delivery model.</p>

<h3 id="driving-performance-through-alignment">Driving Performance through Alignment</h3>

<p>The benefits of this approach are visible in the team’s output. According to data from the Project Management Institute, poor communication is a leading cause of project failure. By enforcing a strict fortnightly feedback cycle, teams significantly reduce the risk of misalignment.</p>

<ul>
  <li>Productivity: Regular feedback loops allow teams to course-correct early, reducing the time spent on rework.</li>
  <li>Transparency: A clear audit trail of feedback and actions keeps everyone on the same page.</li>
  <li>Focus: Knowing there is a demo at the end of the fortnight encourages the team to reach a true “Definition of Done” for every task.</li>
</ul>

<h3 id="summary">Summary</h3>

<p>Regular feedback loops are the foundation of a healthy, high-performing team. By utilising the fortnightly sprint to create a positive environment for demonstration and feedback, you ensure that the team is not just building software, but building the right solution for the business. When you close the loop effectively, you build the trust and morale necessary for long-term project success.</p>]]></content><author><name>Ryan Howells</name></author><category term="agile" /><category term="leadership" /><summary type="html"><![CDATA[The rhythm of a team, not raw technical ability, defines high performance. Why the feedback loop at the end of every sprint matters most.]]></summary></entry><entry><title type="html">VPNs: The Digital Tunnel You Can’t Live Without</title><link href="https://career-catalogue.pages.dev/vpns/" rel="alternate" type="text/html" title="VPNs: The Digital Tunnel You Can’t Live Without" /><published>2026-02-14T19:47:11+00:00</published><updated>2026-02-14T19:47:11+00:00</updated><id>https://career-catalogue.pages.dev/vpns</id><content type="html" xml:base="https://career-catalogue.pages.dev/vpns/"><![CDATA[<p>In a world where our digital footprint is tracked, logged, and sold before we even finish a search query, privacy has become a luxury. If you’ve spent any time looking into network security, you’ve hit the term <strong>VPN</strong> (Virtual Private Network).</p>

<p>It is no longer just a tool for the “tech-obsessed” or people trying to hide sketchy activity. In 2026, a VPN is a fundamental layer of digital hygiene.</p>

<h2 id="what-is-a-vpn">What is a VPN?</h2>

<p>At its core, a VPN creates a secure, encrypted “tunnel” between your device and the internet. When you connect to a VPN, your data is scrambled before it even leaves your hardware.</p>

<p>Instead of your traffic going straight to a website, it goes to a VPN server first. This server masks your real IP address and replaces it with its own. To the outside world, you aren’t browsing from your home in London; you’re browsing from a data center in Switzerland or New York.</p>

<h2 id="a-brief-history">A Brief History</h2>

<h3 id="the-corporate-roots-1996">The Corporate Roots (1996)</h3>
<p>VPNs didn’t start as privacy tools for the public. They were born at Microsoft in 1996 when Gurdeep Singh-Pall developed the <strong>Point-to-Point Tunneling Protocol (PPTP)</strong>. For over a decade, VPNs were strictly corporate. They were the clunky, slow tools you used to access your office files while working from home.</p>

<h3 id="the-privacy-explosion-2013---present">The Privacy Explosion (2013 - Present)</h3>
<p>The shift to the mainstream happened around 2013. The Edward Snowden leaks acted as a massive wake-up call, showing the public exactly how much government surveillance and ISP data harvesting was actually happening.</p>

<p>In recent years, we’ve seen another massive spike. As governments worldwide increase internet restrictions and censorship, and as ISPs began selling user browsing history to advertisers, the VPN moved from a “nice-to-have” to a “must-have.”</p>

<h2 id="why-use-one-the-pros">Why Use One? (The Pros)</h2>

<ul>
  <li><strong>Public Wi-Fi Protection:</strong> Every time you use “Free Airport Wi-Fi,” you are at risk of Man-in-the-Middle attacks. A VPN encrypts that connection so hackers can’t sniff your passwords.</li>
  <li><strong>Bypassing Throttling:</strong> Some ISPs purposefully slow down your connection if they see you are gaming or streaming 4K video. A VPN hides your activity type, preventing targeted slowdowns.</li>
  <li><strong>Global Access:</strong> Geo-blocking is a pain. A VPN lets you access content, news, and streaming libraries that are restricted in your specific region.</li>
  <li><strong>Ad Privacy:</strong> By masking your identity, it becomes significantly harder for companies to build a “buyer profile” on you to hit you with targeted ads.</li>
</ul>

<h2 id="the-trade-offs-the-cons">The Trade-offs (The Cons)</h2>

<ul>
  <li><strong>Latency:</strong> Because your data has to travel to an extra server and go through the encryption process, you might see a slight increase in “ping” or a minor drop in top speeds.</li>
  <li><strong>Trust Displacement:</strong> You are essentially moving your trust from your ISP to your VPN provider. This is why choosing the right one matters.</li>
  <li><strong>The “Robot” Check:</strong> Because many people share the same VPN IP address, you’ll occasionally run into more CAPTCHAs while browsing.</li>
</ul>

<h2 id="the-free-vpn-trap">The “Free” VPN Trap</h2>

<p>I cannot stress this enough: <strong>Never use a free VPN.</strong></p>

<p>Running a global network of high-speed servers is incredibly expensive. If a company isn’t charging you a subscription fee, they have to make money elsewhere. In the world of free VPNs, <strong>you and your data are the profit.</strong> These “services” often log every site you visit, every app you use, and every bit of your metadata, only to sell it to the highest bidder. Using a free VPN is often less private than using no VPN at all.</p>

<h2 id="tunnelling">Tunnelling</h2>

<p>If a VPN is a secure “tunnel” through the internet, how exactly is that tunnel dug? For the tech-curious, “tunnelling” is the most critical part of the process. It’s the mechanism that allows private data to travel across a public network without being intercepted or understood by anyone in between.</p>

<h2 id="what-is-tunnelling-really">What is Tunnelling, Really?</h2>

<p>In standard networking, your data is sent in “packets.” Each packet has a header (like an address on an envelope) and a payload (the letter inside). Normally, your ISP reads that header and sends the packet to its destination.</p>

<p><strong>Tunnelling</strong> changes this by using a process called <strong>Encapsulation</strong>.</p>

<p>Instead of sending your data packet out into the open, the VPN takes your entire packet—header and all—and stuffs it <em>inside</em> another packet. This new outer packet has a new header that only points to the VPN server.</p>

<p>To any observer (like your ISP or a hacker on public Wi-Fi), the original “inner” packet is invisible. All they see is a stream of data going from your device to a single IP address: the VPN server.</p>

<h2 id="the-three-pillars-of-a-tunnel">The Three Pillars of a Tunnel</h2>

<p>A tunnel isn’t just about hiding data; it has to be fast and unbreakable. To achieve this, it relies on three specific steps:</p>

<h3 id="1-encapsulation">1. Encapsulation</h3>
<p>As mentioned, this is the “envelope inside an envelope” trick. It allows non-routable or private protocols to be carried over the public internet.</p>

<h3 id="2-encryption">2. Encryption</h3>
<p>Encapsulation hides the <em>destination</em>, but Encryption hides the <em>content</em>. Even if someone “breaks” the outer envelope, they find the inner envelope is written in a code that would take a supercomputer billions of years to crack. Most modern tunnels use <strong>AES-256</strong> or <strong>ChaCha20</strong>.</p>

<h3 id="3-authentication">3. Authentication</h3>
<p>Before the tunnel is established, your device and the VPN server perform a “handshake.” This ensures that both sides are who they say they are. This prevents “Man-in-the-Middle” attacks where a hacker tries to pretend to be your VPN provider to steal your data.</p>

<h2 id="why-tunnelling-matters-for-your-privacy">Why Tunnelling Matters for Your Privacy</h2>

<p>Without a tunnel, your digital life is an open book. Every site you visit, every unencrypted form you fill out, and every “ping” your apps send is logged by your ISP and likely sold to data brokers.</p>

<p>By forcing all your traffic through an encrypted tunnel, you reclaim the “Right to be Forgotten.” You aren’t just hiding a single search; you are creating a permanent barrier between your identity and the companies that want to commodify it.</p>

<h2 id="the-modern-protocols-choosing-your-tunnel">The Modern Protocols: Choosing Your Tunnel</h2>

<p>The “protocol” is the set of rules that determines how the tunnel is built. Not all tunnels are created equal.</p>

<h3 id="openvpn-the-reliable-veteran">OpenVPN: The Reliable Veteran</h3>
<p>OpenVPN has been around forever. It’s highly configurable and can “disguise” its traffic to look like standard web traffic (HTTPS), which is great for bypassing strict government firewalls that try to block VPNs specifically. However, it’s “heavy” and can be slower on mobile devices.</p>

<h3 id="ikev2ipsec-the-mobile-specialist">IKEv2/IPSec: The Mobile Specialist</h3>
<p>If you’ve ever noticed your VPN stays connected even when you switch from Wi-Fi to 4G/5G, you’re likely using IKEv2. It’s excellent at re-establishing a tunnel quickly if the connection drops, making it the go-to for smartphones.</p>

<h2 id="wireguard-the-lean-mean-encryption-machine">WireGuard: The Lean, Mean Encryption Machine</h2>

<p>For years, the tech world relied on OpenVPN. It was solid, but it was a beast - hundreds of thousands of lines of code, slow to connect, and a nightmare to audit for security holes.</p>

<p>Then came <strong>WireGuard</strong>.</p>

<h3 id="why-its-better">Why It’s Better</h3>
<p>WireGuard is roughly <strong>4,000 lines of code</strong>. In the software world, less code means a smaller “attack surface.” There are fewer places for bugs to hide and fewer ways for hackers to break in.</p>

<ul>
  <li><strong>State-of-the-Art Cryptography:</strong> It doesn’t use outdated math. It relies on modern primitives like <strong>ChaCha20</strong> for encryption and <strong>Curve25519</strong> for key exchange. This is basically the “Formula 1” of encryption.</li>
  <li><strong>Instant Connection:</strong> Because it’s so lightweight, it connects in milliseconds. You don’t have to wait for that “negotiating” spinning wheel you see with older VPNs.</li>
  <li><strong>Battery Life:</strong> If you’ve ever felt your phone getting hot while using a VPN, it’s because the CPU is working overtime to scramble data. WireGuard is so efficient that it barely impacts your battery life compared to the heavy lifting of OpenVPN.</li>
</ul>

<h2 id="split-tunnelling-your-digital-fork-in-the-road">Split Tunnelling: Your Digital “Fork in the Road”</h2>

<p>By default, a VPN sends <strong>all</strong> your traffic through the encrypted tunnel. This is great for privacy but can be a headache for performance. This is where <strong>Split Tunnelling</strong> comes in.</p>

<p>Think of it as a digital fork in the road. You can choose which apps go through the secure VPN and which ones use your regular, high-speed home internet.</p>

<h3 id="why-youd-use-it">Why You’d Use It</h3>
<ol>
  <li><strong>Gaming without Lag:</strong> You can set your game (like Call of Duty or Counter-Strike) to bypass the VPN. This gives you the lowest possible “ping” while your web browser stays encrypted and private in the background.</li>
  <li><strong>Local Devices:</strong> Ever tried to print a document while on a VPN and noticed your printer “disappeared”? With split tunnelling, you can let your local network traffic bypass the tunnel so you can still use your printer or smart home devices.</li>
  <li><strong>Banking and Apps:</strong> Some banking apps get suspicious if they see you “logging in from a different country” every day. You can exclude your banking app from the VPN so it always sees your real, local IP.</li>
</ol>

<h2 id="the-technical-reality">The Technical Reality</h2>
<p>In 2026, most top-tier VPN providers (the ones worth that <strong>£50 a year</strong>) have integrated WireGuard and Split Tunnelling as standard features.</p>

<p>WireGuard handles the heavy-duty security with minimal overhead, while Split Tunnelling ensures you aren’t sacrificing your gigabit fiber speeds just to stay private. It’s no longer a choice between being “safe” or being “fast”—you can finally have both.</p>

<h2 id="remote-access-vpns-how-the-office-follows-you-home">Remote Access VPNs: How the Office Follows You Home</h2>

<p>If the standard VPN is a tunnel for your personal privacy, the <strong>Remote Access VPN</strong> is a secure bridge back to the mothership. In 2026, where “the office” is often a kitchen table or a seat in a crowded airport, understanding how you’re connecting to your company’s internal servers is vital.</p>

<p>It’s the difference between having full, secure access to your work files and accidentally leaving the digital back door open for anyone on the same Wi-Fi network.</p>

<h2 id="the-corporate-handshake">The Corporate “Handshake”</h2>

<p>Unlike the personal VPNs we use for Netflix or privacy, a Remote Access VPN is designed specifically to connect an individual user to a private corporate network.</p>

<p>When you fire up your work VPN, your device (the <strong>Client</strong>) initiates a connection with a <strong>Network Access Server (NAS)</strong> or a gateway sitting at your company’s data center. Once you authenticate—usually with a password and a mandatory MFA (Multi-Factor Authentication) prompt—an encrypted tunnel is established.</p>

<p>At that point, your laptop behaves as if it’s physically plugged into the office wall. You get a local corporate IP address, and you can access internal tools, printers, and databases that aren’t available on the public internet.</p>

<h2 id="why-businesses-dont-just-use-regular-internet">Why Businesses Don’t Just Use “Regular” Internet</h2>

<p>You might wonder why we can’t just log into work sites like we log into Facebook. The reason is <strong>Network Exposure</strong>.</p>

<p>Most internal company tools aren’t built to be “public-facing.” They often have vulnerabilities that hackers can exploit if they’re visible to the open web. A Remote Access VPN hides these tools behind a wall. The only way to even <em>see</em> the login page for the company’s internal database is to be inside the VPN tunnel first.</p>

<h2 id="remote-access-vs-site-to-site">Remote Access vs. Site-to-Site</h2>

<p>It’s easy to get these confused, but the distinction is simple:</p>

<ul>
  <li><strong>Remote Access (User-to-Network):</strong> This is for you, the individual. You install a client (like Cisco AnyConnect or NordLayer) on your laptop. You “dial in” when you need to work.</li>
  <li><strong>Site-to-Site (Network-to-Network):</strong> This is a permanent bridge. If your company has an office in London and another in New York, a Site-to-Site VPN connects the two entire buildings. The employees don’t have to “log in” to it; the routers at each office handle the tunnel 24/7.</li>
</ul>

<h2 id="the-2026-shift-from-vpn-to-zero-trust-ztna">The 2026 Shift: From VPN to Zero Trust (ZTNA)</h2>

<p>We are currently seeing a massive transition in how remote access works. Traditional VPNs have one major flaw: once you’re in the tunnel, you often have “lateral” access to the whole network. If a hacker steals your VPN keys, they can jump from your account to the HR files to the finance server.</p>

<p>This is why many companies are moving toward <strong>Zero Trust Network Access (ZTNA)</strong>.</p>

<p>In a Zero Trust setup, there is no “broad tunnel.” Instead of connecting you to the network, it connects you to <strong>specific applications</strong>. Every time you try to open a file or a tool, the system re-verifies your identity and device health. It’s more restrictive, but it prevents one compromised laptop from taking down the whole company.</p>

<h2 id="the-remote-work-benefit">The Remote Work Benefit</h2>

<p>Beyond security, the Remote Access VPN has a massive psychological benefit for the modern hybrid worker.</p>

<p>Many people now use their VPN as a <strong>work-life boundary</strong>. When the VPN is on, you’re “at the office.” When you disconnect at 5:00 PM, you’ve effectively “left the building.” It’s a digital ritual that helps prevent the burnout of being “always on.”</p>

<h2 id="the-verdict-on-personal-vpns">The Verdict on personal VPNs</h2>

<p>A high-quality, audited “no-logs”, utilising WireGuard VPN is one of the best investments you can make for your digital life.</p>

<p>Whether you want to stay truly incognito, stop your information from being sold to advertising giants, or just want to ensure your ISP isn’t throttling your streaming speeds, a VPN should be your default way of browsing. It’s a small price to pay for reclaiming your privacy in an era where everything is up for sale.</p>]]></content><author><name>Ryan Howells</name></author><category term="security" /><category term="networking" /><summary type="html"><![CDATA[What a VPN actually does, why privacy has become a luxury, and how the digital tunnel protects your traffic.]]></summary></entry><entry><title type="html">The Adapter Design Pattern: Making Mismatched Systems Play Nicely</title><link href="https://career-catalogue.pages.dev/adapter-design-pattern/" rel="alternate" type="text/html" title="The Adapter Design Pattern: Making Mismatched Systems Play Nicely" /><published>2026-02-06T15:30:32+00:00</published><updated>2026-02-06T15:30:32+00:00</updated><id>https://career-catalogue.pages.dev/adapter-design-pattern</id><content type="html" xml:base="https://career-catalogue.pages.dev/adapter-design-pattern/"><![CDATA[<p>Sometimes you design something clean, modern, and intentionally shaped for your domain… and then reality arrives with a system that refuses to fit. Legacy APIs, third‑party platforms, or just “the thing we haven’t rewritten yet” - they all have their own ideas about how data should look and how operations should behave.</p>

<p>That’s where the <strong>Adapter pattern</strong> earns its keep.</p>

<p>The Adapter pattern provides a <strong>translator</strong> between two incompatible interfaces, allowing your application to speak in its preferred vocabulary while another system continues doing things its own way. Rather than forcing your core codebase to bend around an awkward dependency, the Adapter absorbs the mismatch.</p>

<hr />

<h2 id="why-use-an-adapter">Why use an Adapter?</h2>

<p>Real systems are rarely consistent. Different platforms evolve separately, teams have different design philosophies, and vendors make decisions you cannot undo. Adapters let you:</p>

<ul>
  <li>Introduce clean, domain‑focused interfaces without waiting for dependent systems to change.</li>
  <li>Integrate third‑party services without leaking their quirks into your own code.</li>
  <li>Gradually modernise a system - new code talks to the new interface, while the adapter keeps the old world running.</li>
  <li>Maintain clarity: <strong>your domain stays pure, the complexity stays at the boundary.</strong></li>
</ul>

<hr />

<h2 id="what-an-adapter-actually-does">What an Adapter <em>actually</em> does</h2>

<p>At its core, an Adapter is a small piece of code that:</p>

<ol>
  <li><strong>Receives a request</strong> using your preferred interface.</li>
  <li><strong>Translates</strong> that request into the format the external system expects.</li>
  <li><strong>Invokes the external system</strong> and gathers its response.</li>
  <li><strong>Maps the response</strong> back into your own domain concepts.</li>
</ol>

<p>It doesn’t add business logic.
It doesn’t orchestrate workflows.
It simply converts <strong>intent</strong> from one shape to another.</p>

<p>Think of it as an interpreter and not a decision‑maker.</p>

<hr />

<h2 id="when-not-to-use-an-adapter">When <em>not</em> to use an Adapter</h2>

<p>Although powerful, an Adapter isn’t always the right tool. Avoid it when:</p>

<ul>
  <li>You fully control both systems and can safely update one to match the other.</li>
  <li>The mismatch is conceptual, not structural (e.g., one system thinks in versions, another in endorsements). In those cases, you need a <strong>façade</strong> or <strong>anti‑corruption layer</strong>, not a simple Adapter.</li>
  <li>You’re tempted to add retries, caching, or domain rules. That’s service‑layer behaviour, not Adapter behaviour.</li>
</ul>

<p>Adapters should stay <strong>boring and predictable</strong>.</p>

<hr />

<h2 id="why-the-adapter-pattern-matters">Why the Adapter pattern matters</h2>

<p>Adapters help you defend the <strong>shape</strong> of your system. They ensure that:</p>

<ul>
  <li>Vendor oddities don’t leak into your domain.</li>
  <li>Your interfaces stay stable even when external systems don’t.</li>
  <li>Integrations remain replaceable; swap the adapter, keep the interface.</li>
  <li>Technical debt stays isolated rather than spreading through your entire codebase.</li>
</ul>

<p>It’s a small pattern with a big architectural impact.</p>

<hr />

<h2 id="final-thoughts">Final thoughts</h2>

<p>Clean design isn’t about eliminating every legacy system in sight, it’s about preventing yesterday’s decisions from dictating tomorrow’s architecture. The Adapter pattern helps you draw that line. Build the interfaces <em>you</em> want, then write adapters that translate them to whatever the outside world requires. Your domain stays consistent, your integrations stay manageable and your code becomes more manageable.</p>]]></content><author><name>Ryan Howells</name></author><category term="software-design" /><category term="engineering-practice" /><summary type="html"><![CDATA[How the Adapter pattern lets clean, modern code work with legacy APIs and third-party systems that refuse to fit your domain.]]></summary></entry></feed>