[
  {
    "id": "kill-switch-before-launching-saas",
    "title": "Why I Built a 'Kill Switch' System Before Launching My SaaS",
    "slug": "kill-switch-before-launching-saas",
    "date": "2026-05-16",
    "readTime": "14 min read",
    "published": true,
    "excerpt": "I shipped ParseApi this week — drop PDFs into a folder and they become a live REST API. But this article isn't about the product. It's about the work I did before launch building something that produces no features, no revenue, and no demos: a kill switch system.",
    "bodyHtml": "<p>\n  I shipped my SaaS this week. It's called <strong>ParseApi</strong> — you drop PDFs into a folder and it becomes a live\n  REST API that returns structured JSON. No code, no schema setup, no glue scripts. Upload a folder of invoices, get\n  back an endpoint you can call from your app. It's document AI, self-serve, built solo on .NET, and it went live a\n  few days ago at <a href=\"https://parseapi.dev\" target=\"_blank\" rel=\"noopener noreferrer\">parseapi.dev</a>.\n</p>\n\n<p>\n  But this article isn't about the product. It's about the work I did <em>before</em> launch building something\n  that produces no features, no revenue, and no demos: a kill switch system.\n</p>\n\n<p>\n  If you're a solo founder about to put anything in front of the public that calls a paid API — an LLM, an OCR service,\n  a maps API, anything metered — this is the post I wish I'd read first.\n</p>\n\n<h2>The fear that started it</h2>\n\n<p>\n  My product calls AI APIs. Every document a user uploads costs me money to process. The pricing math looks fine on a\n  spreadsheet: a few tenths of a cent per page, comfortably under what I charge.\n</p>\n\n<p>Then I did a different kind of math.</p>\n\n<p>\n  What happens if someone writes a 20-line script that hammers my upload endpoint? What happens if a bot discovers my\n  homepage demo — the one with no signup required — and decides my server is now its personal free LLM? What happens\n  if I have a bug in my own retry logic that calls the AI provider five times instead of once?\n</p>\n\n<p>\n  I'm one person. I have a day job. I have a family. If something goes wrong at 2 AM, nobody is awake to notice. The\n  AI provider doesn't call me. It just bills me.\n</p>\n\n<p>\n  I ran the worst-case number. An unprotected endpoint, a motivated abuser, eight hours of me being asleep. The figure\n  was uncomfortably close to four digits. For a product that had earned exactly zero dollars.\n</p>\n\n<p>That's when I stopped building features and started building brakes.</p>\n\n<h2>The principle: fail closed</h2>\n\n<p>Here's the single idea the whole system is built on.</p>\n\n<p>\n  When something breaks, software can fail in one of two directions. It can <strong>fail open</strong> — keep working,\n  keep serving requests, keep doing whatever it was doing. Or it can <strong>fail closed</strong> — stop, refuse, lock\n  the door.\n</p>\n\n<p>\n  Most software fails open by default, because failing open <em>feels</em> better. The site stays up. Users aren't\n  inconvenienced. Nobody files a support ticket.\n</p>\n\n<p>\n  But \"keep working\" when the thing that's working is <em>spending your money</em> is exactly wrong. If my\n  cost-tracking system breaks, I don't want my app to shrug and keep calling the AI provider. I want it to stop.\n  Immediately. Refuse everything. Let me wake up to a frozen app instead of an empty bank account.\n</p>\n\n<p>A frozen app costs me a few hours of signups. A fail-open bug costs me a number I can't afford.</p>\n\n<p>\n  So every protective layer I built follows one rule: <strong>if you're not sure, stop.</strong> If the cost ledger is\n  unreachable, refuse AI calls. If the config system throws, refuse AI calls. If a usage check is ambiguous, refuse.\n  The default answer to \"should I spend money right now?\" is no, unless every check explicitly says yes.\n</p>\n\n<h2>It's not one switch. It's a panel of them.</h2>\n\n<p>The phrase \"kill switch\" makes you picture one big red button. That's not what you want.</p>\n\n<p>\n  The problem with one button is that the right response depends on what's actually wrong. If one bad actor is abusing\n  the demo, I want to kill the demo and leave everything else running. If my costs are spiking, I want to freeze AI\n  processing but keep the dashboard alive so I can investigate. If there's a security issue, I want the whole thing\n  dark.\n</p>\n\n<p>\n  One button can't express all of that. So I built a graduated set of controls, from \"pause one feature\" to \"stop\n  everything.\"\n</p>\n\n<p>Every flag is a string constant, and they all live in one file so there's a single place to look:</p>\n\n<pre><code>public static class FeatureFlag\n{\n    public const string AiExtractionEnabled    = \"AiExtractionEnabled\";\n    public const string UploadsEnabled         = \"UploadsEnabled\";\n    public const string SignupsEnabled         = \"SignupsEnabled\";\n    public const string PublicApiEnabled       = \"PublicApiEnabled\";\n    public const string WebhookDeliveryEnabled = \"WebhookDeliveryEnabled\";\n    public const string StripeWebhookEnabled   = \"StripeWebhookEnabled\";\n    public const string OAuthSignInEnabled     = \"OAuthSignInEnabled\";\n    public const string ReadOnlyMode           = \"ReadOnlyMode\";\n    public const string StrictRateLimitMode    = \"StrictRateLimitMode\";\n    public const string DefaultDenyWindow      = \"DefaultDenyWindow\";\n    // ...\n}</code></pre>\n\n<p>\n  There's a detail in that list worth pausing on. Most of these are <em>enable flags</em> — the safe value is\n  <code>true</code>, and flipping to <code>false</code> shuts a feature off. But three of them\n  (<code>ReadOnlyMode</code>, <code>StrictRateLimitMode</code>, <code>DefaultDenyWindow</code>) are <em>hazard\n  flags</em> — the safe value is <code>false</code>, and flipping to <code>true</code> turns on a defensive mode.\n  Because sometimes the safe response to trouble isn't switching a feature off. It's switching a degraded mode on. You\n  want both directions available.\n</p>\n\n<p>\n  <code>ReadOnlyMode</code> is the one I'm proudest of. When I don't yet know what's wrong — costs look weird, errors\n  are climbing, something's off — I can flip read-only. Every existing user can still read their data. Every existing\n  API integration still returns results. But nothing new gets created: no uploads, no extractions, no signups, no\n  charges. The bleeding stops while I keep my head and investigate. It's the emergency brake that doesn't crash the\n  car.\n</p>\n\n<h2>The technical detail that makes it actually work</h2>\n\n<p>\n  A kill switch is useless if flipping it requires a code deploy. By the time your CI pipeline finishes building and\n  your container restarts, you've burned ten minutes — and if something is actively going wrong, ten minutes is a long\n  time.\n</p>\n\n<p>The flags have to change <em>live</em>. No redeploy. No restart.</p>\n\n<p>So the flags live in a Postgres table, fronted by a small interface:</p>\n\n<pre><code>public interface IFeatureGate\n{\n    bool IsEnabled(string flag);\n\n    Task&lt;Result&gt; SetAsync(string flag, bool newValue,\n        Guid? actorUserId, string actorKind, string? reason,\n        string? ipAddress, CancellationToken ct);\n\n    Task&lt;Result&gt; SetManyAsync(\n        IReadOnlyDictionary&lt;string, bool&gt; flags, /* ... */);\n\n    IReadOnlyDictionary&lt;string, bool&gt; SnapshotAll();\n}</code></pre>\n\n<p>\n  Changing a flag becomes: open the admin console, flip a toggle, done. The running app picks it up immediately — no\n  deploy, no restart. The admin console has one toggle switch per flag, plus a single \"PANIC\" button that calls\n  <code>SetManyAsync</code> with every enable flag off and every hazard flag on. Atomic. One row in the audit log. One\n  email to me.\n</p>\n\n<figure>\n  <img src=\"images/kill-switch-admin.png\" alt=\"ParseApi admin console showing PANIC MODE, READ-ONLY MODE, RESUME NORMAL action cards, a grid of feature flag toggles, and quick actions for blocking IPs and freezing organizations\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The live admin console for ParseApi. Three confirmation-gated action cards on top (PANIC, READ-ONLY, RESUME), the full grid of enable flags and hazard flags below, and per-incident tools at the bottom (block IP, freeze org). Every action lands in an audit log and sends an email.</figcaption>\n</figure>\n\n<p>\n  Then — and this is the part that's easy to skip — you have to actually <em>check</em> the flag everywhere it matters.\n  A flag that's defined but not enforced is theatre. The check itself is one line, dropped into every entry point that\n  does something expensive or risky:\n</p>\n\n<pre><code>// UploadDocumentCommand.cs\nif (!featureGate.IsEnabled(FeatureFlag.UploadsEnabled))\n    return Result&lt;DocumentDto&gt;.Failure(new Error(\n        \"uploads_disabled\",\n        \"Uploads are temporarily disabled. Please try again later.\"));</code></pre>\n\n<p>A few of these checks have a subtlety worth showing. The Stripe webhook handler still returns <code>200 OK</code> even when the flag is off:</p>\n\n<pre><code>// StripeWebhookEndpoint.cs — we ACK 200 even when disabled,\n// so Stripe doesn't keep retrying while billing logic is suspect.\nif (!featureGate.IsEnabled(FeatureFlag.StripeWebhookEnabled))\n{\n    logger.LogWarning(\"Stripe webhook received while disabled. Dropping.\");\n    return Results.Ok();\n}</code></pre>\n\n<p>\n  If I'd returned an error there, Stripe would queue the event and retry it for days — and when I flipped billing back\n  on, I'd get a flood of stale webhooks all at once, which is exactly the chaos I was trying to avoid. So I acknowledge\n  it, drop it, and log it. Small thing. The kind of small thing you only think of because you sat and imagined the\n  failure.\n</p>\n\n<p>\n  That check sits in front of every code path that costs money or touches something fragile. If the flag is off, none\n  of them proceed. They fail closed, return a clean error, and the user sees a polite \"temporarily unavailable\" instead\n  of me seeing a charge.\n</p>\n\n<h2>The switches that flip themselves</h2>\n\n<p>\n  Manual switches are only as fast as my reflexes. If abuse starts at 3 AM, the manual panel does nothing until I wake\n  up, make coffee, and notice.\n</p>\n\n<p>So the most important layer is the one that acts without me: automated tripwires.</p>\n\n<p>\n  A tripwire is a small class that runs every 30 seconds, checks one metric, and flips a flag if that metric crosses a\n  line. The contract is tiny:\n</p>\n\n<pre><code>public interface ITripwire\n{\n    string Name { get; }\n    bool Enabled { get; }\n    Task&lt;TripwireResult&gt; EvaluateAsync(CancellationToken ct);\n    Task OnTrippedAsync(TripwireResult result, CancellationToken ct);\n}</code></pre>\n\n<p>\n  The one I lose the least sleep over — because it exists — is the AI cost tripwire. The logic is six lines:\n</p>\n\n<pre><code>public async Task&lt;TripwireResult&gt; EvaluateAsync(CancellationToken ct)\n{\n    decimal threshold = opts.CurrentValue.AiCostRate.MaxUsdPerHour;\n    decimal spend = await q.SumAiCostLastHourAsync(ct);\n\n    if (spend &lt; threshold)\n        return new TripwireResult(false, null);\n\n    return new TripwireResult(true,\n        $\"AI cost rate ${spend:F2}/hr exceeded ${threshold:F2}/hr threshold\");\n}\n\npublic async Task OnTrippedAsync(TripwireResult result, CancellationToken ct)\n{\n    await gate.SetAsync(FeatureFlag.AiExtractionEnabled, false,\n        null, \"tripwire\", result.Reason, null, ct);\n    await gate.SetAsync(FeatureFlag.DemoEnabled, false,\n        null, \"tripwire\", result.Reason, null, ct);\n}</code></pre>\n\n<p>\n  If my AI spend crosses $3 in any rolling hour, the system disables AI processing <em>and</em> the demo, then emails\n  me — all before I've stirred. By the time I read the email, the bleeding has already stopped. I'm reading a report,\n  not racing a meter.\n</p>\n\n<p>\n  I picked $3/hour because if the app burns through that much, something is wrong — even with a thousand active users,\n  my unit economics don't run that hot that fast. And look at the asymmetry of being wrong. If the tripwire fires when\n  it shouldn't, extraction pauses for fifteen minutes while I check the alert. If it <em>doesn't</em> fire when it\n  should, I get an open-ended invoice. One of those mistakes is an annoyance. The other can end the project. So I\n  tuned it to make the cheap mistake.\n</p>\n\n<p>I have seven of these, each watching a different failure mode:</p>\n\n<ul>\n  <li><strong>AI cost rate</strong> — spend per hour exceeds a threshold → freeze AI and the demo</li>\n  <li><strong>Error rate</strong> — extraction job failure rate spikes → flip read-only mode</li>\n  <li><strong>Suspicious IP</strong> — one IP exceeds 200 requests in 5 minutes → block that IP</li>\n  <li><strong>Signup spike</strong> — abnormal signup volume from one subnet → disable signups</li>\n  <li><strong>Storage growth</strong> — uploads growing faster than expected → disable uploads</li>\n  <li><strong>Extraction volume</strong> — one account runs too many extractions in an hour → freeze that account</li>\n  <li><strong>Failed auth</strong> — one IP fails login 50+ times in 10 minutes → block that IP</li>\n</ul>\n\n<p>\n  Each one is about thirty lines. And each one flips flags through the <em>same</em> <code>IFeatureGate.SetAsync</code>\n  call a human would use. That was deliberate. From the gated code's point of view there is no difference between \"I\n  flipped this from my phone\" and \"a tripwire flipped this at 3 AM\" — same audit row, same email, same cooldown rules.\n  One path, not two.\n</p>\n\n<p>The loop that runs them is deliberately stupid:</p>\n\n<pre><code>foreach (ITripwire tw in tripwires)\n{\n    try\n    {\n        if (!tw.Enabled) continue;\n\n        TripwireResult result = await tw.EvaluateAsync(ct);\n        if (!result.Tripped) continue;\n\n        logger.LogWarning(\"Tripwire {Name} TRIPPED: {Reason}\",\n            tw.Name, result.Reason);\n        await tw.OnTrippedAsync(result, ct);\n    }\n    catch (Exception ex)\n    {\n        logger.LogError(ex, \"Tripwire {Name} threw during evaluation.\",\n            tw.Name);\n    }\n}</code></pre>\n\n<p>\n  The try-catch is inside the loop, per tripwire, and that's not an accident. Early on I had a tripwire with a broken\n  query. Because the exception escaped the loop, it was silently stopping every tripwire <em>after</em> it from running\n  — including one that was correctly trying to fire. One bad detector was hiding a real incident. Catch per iteration,\n  never an early return. One tripwire failing cannot blind the rest.\n</p>\n\n<h2>The cooldown: protecting myself from myself</h2>\n\n<p>Here's a subtle one I almost didn't build, and I'm glad I did.</p>\n\n<p>\n  When a tripwire fires and I get the alert, my instinct — your instinct, everyone's instinct — is to log in and turn\n  everything back on. <em>It's probably fine. Let me just flip it back and see.</em>\n</p>\n\n<p>\n  That instinct is dangerous. If the tripwire fired for a real reason, flipping it back immediately just restarts the\n  bleeding. Maybe faster this time.\n</p>\n\n<p>\n  I role-played the incident in my head, start to finish. The tripwire fires at 2 AM. AI extraction goes off. My phone\n  wakes me. I open the dashboard, see one suspicious customer, ban them, flip extraction back on, go back to bed. Five\n  minutes later the tripwire fires again — the same person signed up under a new email. I flip it on. Five minutes\n  later, again.\n</p>\n\n<p>\n  That's flapping. It's the failure mode of every alerting system that lets you dismiss an alert without a forced pause\n  to think. And I could see, lying there imagining it, that half-asleep me would absolutely do this.\n</p>\n\n<p>So every flag has a cooldown — but only in the <em>re-enabling</em> direction:</p>\n\n<pre><code>public static TimeSpan ReEnableCooldown(string flag) =&gt; flag switch\n{\n    AiExtractionEnabled =&gt; TimeSpan.FromMinutes(15),\n    UploadsEnabled      =&gt; TimeSpan.FromMinutes(5),\n    DemoEnabled         =&gt; TimeSpan.FromMinutes(60),\n    SignupsEnabled      =&gt; TimeSpan.FromMinutes(5),\n    PublicApiEnabled    =&gt; TimeSpan.FromMinutes(15),\n    ReadOnlyMode        =&gt; TimeSpan.FromMinutes(5),\n    DefaultDenyWindow   =&gt; TimeSpan.FromHours(6),\n    // ...\n};</code></pre>\n\n<p>\n  You can <em>always</em> disable a feature instantly. You can <em>never</em> instantly re-enable it. The fifteen\n  minutes on AI extraction isn't really for the system — the system doesn't need it. It's for me. It's fifteen enforced\n  minutes where I have to actually look at what tripped instead of impulse-toggling it because I want to go back to\n  sleep.\n</p>\n\n<p>\n  <code>DefaultDenyWindow</code> has the longest cooldown in the system, six hours. That flag, when on, makes the\n  request pipeline deny anything not on an explicit allowlist — it's the \"deny the world\" switch, for when someone is\n  actively attacking the service. If I turn it on during an incident, I'm committed to six hours of denied traffic.\n  That's the point. The most aggressive switch in the system should not be reversible in the middle of an adrenaline\n  rush. Past-me with a clear head doesn't trust 2 AM-me with an open toggle, so he took the toggle away.\n</p>\n\n<h2>The part that took the longest: the runbook</h2>\n\n<p>\n  The code was maybe 60% of the effort. The other 40% was a plain-text document called\n  <code>emergency-procedures.md</code>.\n</p>\n\n<p>\n  Here's the scenario it exists for. It's 3 AM. An alert fires. I'm groggy. And — worst case — the admin console itself\n  is the thing that's broken. I cannot rely on my own UI to save me.\n</p>\n\n<p>So the runbook documents every emergency action in a way that doesn't depend on my app working at all:</p>\n\n<ul>\n  <li>How to flip every flag directly from the host's environment variables, no app needed</li>\n  <li>How to roll back to the previous deploy in two clicks</li>\n  <li>How to rotate every API key — AI providers, Stripe, storage — if a key is compromised</li>\n  <li>How to suspend the entire service at the host level</li>\n  <li>How to switch on the CDN's \"under attack\" mode</li>\n  <li>A bookmarked list of every dashboard I might need, already organized in a folder</li>\n</ul>\n\n<p>\n  I wrote it like instructions for a stranger. Because at 3 AM, a tired, scared version of me <em>is</em> a stranger —\n  one with bad judgment and shaky hands. That person doesn't need cleverness. They need a checklist.\n</p>\n\n<p>\n  If you build one thing from this article, build the runbook. The code protects you from the predictable failures.\n  The runbook protects you from the ones you didn't predict.\n</p>\n\n<h2>The layers, stacked</h2>\n\n<p>\n  None of these pieces is sufficient alone. The point is the stack — defense in depth. For an attacker or a bug to\n  actually cost me money, it has to get past <em>all</em> of these:\n</p>\n\n<ol>\n  <li><strong>Provider hard caps</strong> — Anthropic and OpenAI both have a monthly spending limit set in their dashboards, auto-recharge disabled. This is the floor. Even if every line of my code is wrong, the provider stops at the cap.</li>\n  <li><strong>Rate limits</strong> — every endpoint, especially the unauthenticated ones, capped per IP.</li>\n  <li><strong>Cost circuit breakers</strong> — hard daily and monthly ceilings in my own code, set <em>below</em> the provider caps so I hit mine first.</li>\n  <li><strong>The model allow-list</strong> — only the cheap, fast models can be called at all. The expensive ones aren't reachable from any code path.</li>\n  <li><strong>Tripwires</strong> — automated, flip switches the moment a metric goes wrong.</li>\n  <li><strong>Feature flags</strong> — manual graduated control when I'm awake and watching.</li>\n  <li><strong>The runbook</strong> — out-of-band procedures for when the app itself is the problem.</li>\n</ol>\n\n<p>Each layer catches what slipped past the one before it. A single bug defeats one layer. It does not defeat seven.</p>\n\n<h2>What it felt like the day I tested it</h2>\n\n<p>\n  The day I finished all this, I tried to break it on purpose. I made a folder and uploaded eighty sample documents\n  back to back, watching the dashboard.\n</p>\n\n<p>\n  Around document seventy, the tripwire fired. AI extraction switched off. Document seventy-one still <em>uploaded</em>\n  fine — uploads are a separate flag, and there was no reason to block them — but its extraction job just sat there\n  marked pending. My phone buzzed with the email. The flag stayed locked for fifteen minutes. When the cooldown expired\n  I re-enabled it, the queued jobs drained, and everything carried on.\n</p>\n\n<p>\n  Total cost to me: a fifteen-minute pause to confirm the trip was real. Total cost to customers: zero, because there\n  weren't any yet — but if there had been, they'd have seen \"extraction temporarily paused\" instead of waiting months\n  for a refund on an invoice I couldn't have paid.\n</p>\n\n<h2>Was it worth the work?</h2>\n\n<p>\n  I'll be honest about the cost. All that work bought zero features. Nothing on that list makes the product better\n  for a happy, paying customer. If everything goes perfectly, every line of it is dead weight.\n</p>\n\n<p>But here's the reframe that made the decision easy.</p>\n\n<p>\n  I'm a solo founder. My single biggest risk isn't slow growth or a competitor or a missing feature. It's an\n  <em>unbounded, unexpected loss</em> that I can't see coming and can't afford to absorb. Slow growth I can survive.\n  A four-figure surprise bill in week one might end the whole thing — not because of the money alone, but because of\n  what it does to your nerve.\n</p>\n\n<p>\n  All that work didn't buy features. It bought the ability to sleep. It bought the ability to launch <em>at\n  all</em> without a knot in my stomach. They turned \"what if I wake up to a disaster\" into \"if something goes wrong,\n  the system already handled it and emailed me a report.\"\n</p>\n\n<p>For a solo founder, that's not overhead. That's the thing that lets you take the shot.</p>\n\n<h2>If you're about to launch something</h2>\n\n<p>A short checklist, learned the careful way:</p>\n\n<ul>\n  <li><strong>Decide your fail direction.</strong> When in doubt, your system should stop, not continue. Especially anything that spends money.</li>\n  <li><strong>Set hard caps at the provider.</strong> Every paid API has a spending limit setting. Set it. Disable auto-recharge. This is your floor and it's free.</li>\n  <li><strong>Make your switches live.</strong> If flipping a flag needs a redeploy, it's too slow. Live config, no restart.</li>\n  <li><strong>Automate the switches you can.</strong> You will be asleep. Tripwires work the night shift.</li>\n  <li><strong>Build a cooldown.</strong> Protect the app from the panicked version of you.</li>\n  <li><strong>Write the runbook.</strong> Plain text. Assume your own tools are broken. Assume the reader is exhausted.</li>\n  <li><strong>Stack the layers.</strong> No single protection is enough. Make an attacker beat all of them.</li>\n</ul>\n\n<p>\n  You don't need my exact system. You need <em>a</em> system, sized to your risk. If your product calls a paid API and\n  you're operating solo, you need this more than you need your next feature.\n</p>\n\n<p>Build the brakes before you need them. You won't get a warning.</p>\n\n<hr>\n\n<p>\n  <em>I'm building ParseApi in public — a document-to-API service built solo on .NET. It turns a folder of PDFs into\n  a live REST API. It went live this week; the free tier is at\n  <a href=\"https://parseapi.dev\" target=\"_blank\" rel=\"noopener noreferrer\">parseapi.dev</a>. And if you've got your\n  own kill-switch war stories, I'd genuinely like to hear them.</em>\n</p>",
    "body": "I shipped my SaaS this week. It's called ParseApi — you drop PDFs into a folder and it becomes a live REST API that returns structured JSON. No code, no schema setup, no glue scripts. Upload a folder of invoices, get back an endpoint you can call from your app. It's document AI, self-serve, built solo on .NET, and it went live a few days ago at parseapi.dev.\n\nBut this article isn't about the product. It's about the work I did before launch building something that produces no features, no revenue, and no demos: a kill switch system.\n\nIf you're a solo founder about to put anything in front of the public that calls a paid API — an LLM, an OCR service, a maps API, anything metered — this is the post I wish I'd read first."
  },
  {
    "id": "building-aviation-software-with-ai",
    "title": "Building an Aviation Compliance System with Claude Code",
    "slug": "building-aviation-software-with-ai",
    "date": "2026-05-05",
    "readTime": "9 min read",
    "excerpt": "This year I started a new project — an aviation compliance application — built on .NET 10 with a modular monolith architecture. It's the first serious system I've built where Claude Code did most of the typing. Here's the workflow that's actually working, the recurring pitfalls of AI-assisted development, and the honest experience of being more productive and more exhausted than ever before.",
    "bodyHtml": "<p>\n  This year I started a new project — an aviation compliance application. It's a mid-to-large size system with\n  a complex domain, regulatory constraints, and the kind of user requirements that make you stare at the wall\n  for a while before writing a single line of code.\n</p>\n\n<p>\n  It's also the first project where I leaned heavily on Claude Code from day one. After years of using AI tools\n  — GitHub Copilot since the day it launched, then Cursor, and now Claude Code — I think I've finally settled\n  into a workflow that doesn't feel like wrestling with a brilliant intern who occasionally hallucinates entire\n  libraries.\n</p>\n\n<p>\n  Here's what that workflow looks like, what's working, and what I'm still trying to figure out.\n</p>\n\n<h2>A quick history with AI tools</h2>\n\n<p>\n  I'm not new to this. I've been using AI assistants for development since GitHub Copilot was first released. Over\n  the years I've moved through:\n</p>\n\n<ul>\n  <li><strong>GitHub Copilot</strong> — the original \"autocomplete on steroids\"</li>\n  <li><strong>Cursor</strong> — the first IDE that felt like AI was a first-class citizen</li>\n  <li><strong>Claude Code</strong> — currently my daily driver for serious work</li>\n</ul>\n\n<p>\n  Along the way, I've built things I never would have built alone — not because I couldn't, but because I never\n  would have had the time.\n</p>\n\n<p>\n  Back in 2024, I built myself a Netflix-style TV app. The reason was almost embarrassingly simple: I was paying\n  for a Netflix subscription almost entirely to watch <em>The Office</em>. So I cancelled Netflix, organized my\n  movie files on my computer in a clean folder structure, and wrote a small TV app that scans the folder and plays\n  the show with a Netflix-like UI — rows, thumbnails, \"continue watching\", the works. I cancelled the subscription\n  that month and never looked back.\n</p>\n\n<figure class=\"small\">\n  <img src=\"images/the-office.jpg\" alt=\"Promotional poster for The Office TV show featuring the main cast\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The reason this whole side-project existed. Worth every line of code.</figcaption>\n</figure>\n\n<p>\n  A few months later I built another small Cursor-assisted utility — a controller for the external lights and\n  motion sensors around my house. Lights on at dusk, off at sunrise, with motion-triggered overrides. Boring,\n  useful, and built in an afternoon. The kind of project that traditionally would have stayed on the \"someday\"\n  list forever.\n</p>\n\n<p>\n  These small wins matter, because they're the proof that AI tooling doesn't just speed up your day job — it\n  lowers the activation energy for ideas that used to die quietly in your notes app.\n</p>\n\n<h2>The aviation project: the setup</h2>\n\n<p>\n  This year's project is different. It's not a weekend tool; it's a real system with stakeholders, compliance\n  requirements, and the kind of audit trail expectations you'd expect from anything in aviation.\n</p>\n\n<p>\n  I picked <strong>.NET 10</strong> with a <strong>modular monolith</strong> architecture. There's a long version\n  of why, but the short version is: I wanted the operational simplicity of a monolith with the structural\n  discipline of microservices, without paying the distributed-systems tax this early in the project's life.\n</p>\n\n<p>\n  The architecture I designed has five core parts:\n</p>\n\n<ul>\n  <li><strong>Building Blocks</strong> — cross-cutting primitives (auth, logging, errors, MediatR pipeline)</li>\n  <li><strong>Modules</strong> — the actual bounded contexts of the business domain</li>\n  <li><strong>Shared</strong> — kernel types intentionally shared across modules</li>\n  <li><strong>Tests</strong> — mirrored at every layer</li>\n  <li><strong>Host</strong> — the composition root</li>\n</ul>\n\n<figure>\n  <img src=\"images/aviation-architecture.png\" alt=\"Modular monolith architecture diagram showing Host, three Modules, Building Blocks, and Shared Kernel layers\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The high-level architecture: a Host composing three modules over Building Blocks and a Shared Kernel. Each module follows a 4-layer Clean Architecture pattern.</figcaption>\n</figure>\n\n\n<figure>\n  <img src=\"images/aviation-structure.png\" alt=\"Directory tree showing the project structure with src, modules, building blocks, shared kernel, tests, _specs and _plans folders\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The actual on-disk layout — three modules with strict 4-layer separation, plus dedicated <code>_specs</code> and <code>_plans</code> folders that drive the spec-driven workflow described below.</figcaption>\n</figure>\n\n<p>\n  I defined the project structure, dependencies, and test layout myself. That part isn't outsourced. The\n  architecture is the spine; AI is helpful muscle, but I don't let it choose the spine.\n</p>\n\n<h2>The CLAUDE.md is half the project</h2>\n\n<p>\n  If there's one thing I'd tell engineers starting out with Claude Code on a serious project, it's this:\n  <strong>the quality of your CLAUDE.md is the quality of your output.</strong>\n</p>\n\n<p>\n  For the aviation project, my CLAUDE.md spells out:\n</p>\n\n<ul>\n  <li><strong>Project overview</strong> — what we're building and for whom</li>\n  <li><strong>Solution structure</strong> — the five-part layout above</li>\n  <li><strong>Dependency rules (strict)</strong> — which layer can reference which, no exceptions</li>\n  <li><strong>Module rules</strong> — what belongs in a module, what doesn't</li>\n  <li><strong>Feature implementation order</strong> — the non-negotiable sequence below</li>\n  <li><strong>Coding conventions</strong> — naming, async patterns, nullable reference handling</li>\n  <li><strong>Tech stack</strong> — exact libraries and versions</li>\n  <li><strong>MediatR pipeline</strong> — validation, logging, transactions</li>\n  <li><strong>Error response shape</strong> — one canonical envelope, no exceptions</li>\n  <li><strong>Core values</strong> — quality bars, what \"done\" actually means</li>\n</ul>\n\n<p>\n  Inside each feature, the order is fixed:\n</p>\n\n<pre><code>1. Domain         entities, value objects, domain events, repository interfaces\n2. Application    commands/queries (CQRS), handlers, DTOs, validation\n3. Infrastructure DbContext, entity configs, repository implementations\n4. Presentation   thin controllers delegating to MediatR, ViewModels, Razor views\n5. Tests          at each layer</code></pre>\n\n<p>\n  When the model knows the order, the architectural style, and the dependency rules, it stops inventing creative\n  shortcuts. When it doesn't, it will happily call a DbContext from inside a controller and tell you with a\n  straight face that \"this should work.\"\n</p>\n\n<h2>Spec-driven, not vibe-driven</h2>\n\n<p>\n  The single biggest workflow shift for me has been moving to <strong>spec-driven development</strong>. The flow\n  is simple:\n</p>\n\n<ul>\n  <li><strong>Spec</strong> — what are we actually building, in detail</li>\n  <li><strong>Plan</strong> — how, broken into concrete steps with file-level scope</li>\n  <li><strong>Implementation</strong> — the part most people incorrectly think is the work</li>\n</ul>\n\n<p>\n  I now spend more than 50% of my time in the first two stages. That sounds extreme, and I get pushback every\n  time I describe it, but the math works out.\n</p>\n\n<p>\n  The spec phase is where I let Claude ask me detailed clarifying questions. I don't rush past these. The model\n  is genuinely good at finding the soft spots in a requirement — the cases I haven't thought through, the edges\n  I'm hand-waving, the ambiguities that would have cost me a day to discover later. I treat that conversation\n  like a code review of my own thinking.\n</p>\n\n<p>\n  By the time the plan is locked, the implementation is mostly mechanical. The model writes a lot of correct\n  code in a single pass. That's the productivity win — but it only happens if the spec is right. With a\n  half-baked spec and weak guardrails, the result tends to go off the rails fast.\n</p>\n\n<h2>The recurring pitfalls (and how to defend against them)</h2>\n\n<p>\n  Even with a strong spec and a strict CLAUDE.md, the model has predictable failure modes. I now reference\n  these directly in both the project guardrails and in every spec.\n</p>\n\n<p>\n  <strong>1. The model invents things.</strong> APIs that don't exist. Libraries that don't exist. Methods\n  that look right but aren't. The smaller the context the model has on your codebase, the worse this gets.\n  The fix is mostly upstream: feed it the right files, ground it in the actual code, and verify any\n  unfamiliar import.\n</p>\n\n<p>\n  <strong>2. It generates too much code.</strong> Given any feature, the default tendency is to over-build.\n  Extra abstractions. Extra \"for future use\" interfaces. Extra options nobody asked for. I now state explicitly:\n  <em>no premature abstraction, no defensive code for cases that can't happen, no helpers for one-off\n  operations.</em> Three similar lines is better than a clever generalization.\n</p>\n\n<p>\n  <strong>3. It only tests the happy path.</strong> AI-written tests are, by default, a joke. The model will\n  call the function, assert it doesn't throw, and call it good. Real testing — boundary cases, failure modes,\n  compliance-critical edge cases — has to be requested explicitly and reviewed carefully. In an aviation compliance\n  context, this isn't optional. I now write the test plan as part of the spec, not after the fact.\n</p>\n\n<p>\n  When I list these pitfalls in the spec (\"avoid the following common failure modes during implementation\"),\n  the result is dramatically better. Not perfect — but better.\n</p>\n\n<h2>What this feels like as the engineer</h2>\n\n<p>\n  I want to be honest about the lived experience, because the public conversation about AI in software is either\n  \"we're all replaced\" or \"it's all hype,\" and the truth is much weirder.\n</p>\n\n<p>\n  <strong>I am more productive than I have ever been.</strong> Projects that would have taken six months are\n  landing in six weeks. Features I would have de-scoped get built. The TV app, the lights system — those would\n  not exist without these tools. The aviation project would still be in design review.\n</p>\n\n<p>\n  <strong>I work more, not less.</strong> I genuinely thought AI tooling would give me my evenings back. It\n  hasn't. It has lifted the ceiling on what I can attempt, so I keep attempting more. I work day and night on\n  this project — not because the tool is slow, but because the opportunity surface is wider than it has ever\n  been. \"Super productive\" and \"super busy\" turn out to be the same sentence.\n</p>\n\n<p>\n  <strong>I'm less certain about the code than I used to be.</strong> This is the part I'm still wrestling with.\n  Even when the project is well-architected, well-spec'd, and well-tested, there's a small voice asking: \"did\n  you really read that diff, or did you read enough of it?\" Confidence isn't free. AI gives you speed; it does\n  not automatically give you the same intuitive ownership you got from typing every line yourself. That has to\n  be earned back deliberately.\n</p>\n\n<p>\n  <strong>Code review has become the bottleneck.</strong> This one surprised me. Reviewing AI-generated code is\n  harder than reviewing human code, not easier. Humans write in patterns you recognize, with mistakes you can\n  predict. AI writes in a style that's often plausible but subtly wrong — a function that looks idiomatic but\n  uses a deprecated overload, or a test that asserts the wrong thing in a way that still compiles. Reviewing it\n  well takes more focus, not less. The teams that ship AI-generated PRs after a 30-second skim will pay for it\n  later.\n</p>\n\n<h2>What I'd tell a serious team starting out</h2>\n\n<p>\n  If you're picking up Claude Code (or any equivalent agentic tool) for a real project, three things matter most:\n</p>\n\n<ul>\n  <li><strong>Own the architecture.</strong> Don't outsource the spine of the system. Project structure,\n  dependency rules, module boundaries — those are yours.</li>\n  <li><strong>Invest in the spec.</strong> Spend half your time before any code is written. Let the model help\n  you find the holes in your requirements; don't let it skip past them.</li>\n  <li><strong>Treat code review as the new engineering.</strong> This is where quality is preserved or lost.\n  Slow it down, not speed it up.</li>\n</ul>\n\n<p>\n  AI assistance is, in my view, the single biggest productivity shift I have lived through in 16 years of\n  writing software. It's also the one that most rewards engineering discipline. The teams that get the most out\n  of it aren't the ones who let it run free — they're the ones who give it the strictest scaffolding.\n</p>\n\n<hr>\n\n<p>\n  <strong>Coming next:</strong> I'll share my experience using <strong>OpenAI Codex</strong> on a follow-up\n  project — how the workflow translates (or doesn't) when the underlying tooling changes, where Codex pulls\n  ahead, where Claude Code still wins, and what the hand-off between the two looks like in practice.\n</p>\n\n<p>\n  The aviation project is still in flight (sorry). I'll keep writing about how this evolves — what's holding up,\n  what's breaking, and what the workflow looks like a year from now.\n</p>",
    "body": "This year I started a new project - an aviation compliance application. It's a mid-to-large size system with a complex domain, regulatory constraints, and the kind of user requirements that make you stare at the wall for a while before writing a single line of code.\n\nIt's also the first project where I leaned heavily on Claude Code from day one. After years of using AI tools - GitHub Copilot since the day it launched, then Cursor, and now Claude Code - I think I've finally settled into a workflow that doesn't feel like wrestling with a brilliant intern who occasionally hallucinates entire libraries.\n\nHere's what that workflow looks like, what's working, and what I'm still trying to figure out.",
    "published": true
  },
  {
    "id": "reflections-on-shipping-software",
    "slug": "reflections-on-shipping-software",
    "title": "Reflections on 16 Years of Shipping Software",
    "date": "2025-07-23",
    "readTime": "8 min read",
    "published": true,
    "excerpt": "I've been writing production code since 2008. That's not something I say to impress anyone — it's just context for what follows. Over those 16 years, I've built payroll systems for Ethiopian government agencies, redesign...",
    "body": "I've been writing production code since 2008. That's not something I say to impress anyone —\n          it's just context for what follows. Over those 16 years, I've built payroll systems for\n          Ethiopian government agencies, redesigned the UN's humanitarian affairs website, connected\n          2.1 million schools to real-time connectivity data, and migrated 20+ enterprise systems to\n          Azure. Different domains, different continents, wildly different constraints.\n        \n\n        \n          What follows are observations — not rules. They're the things I notice I actually believe,\n          now that I've had enough time to see patterns repeat.\n        \n\n        Shipping is the skill\n\n        \n          The first lesson, and the one I keep relearning: the ability to ship is a skill in\n          itself, separate from the ability to code well. Some of the most technically talented\n          engineers I've worked with were terrible at shipping. They would endlessly refine, re-architect,\n          add abstraction layers that \"would be useful later.\" Later rarely came.\n        \n\n        \n          Shipping doesn't mean cutting corners. It means making a concrete decision about what \"good\n          enough\" looks like for this context, right now, and executing on it. The next version will be\n          better because you'll know things you can't know until it's running in production with real users\n          making real decisions with it.\n        \n\n        \n          The gap between what you imagine your software does and what it actually does collapses the\n          moment it hits production.\n        \n\n        Your users will always surprise you\n\n        \n          Whether it was UN agency staff in New York or school administrators in Kenya, users consistently\n          do things with your software that you never imagined. This sounds obvious, but it has a practical\n          consequence: don't build for the user you assume you have. Build for the user who will find your\n          system three years from now, in a context you couldn't predict.\n        \n\n        \n          At UNICEF's Project Connect, we built REST APIs that ended up being consumed by policy groups,\n          economists, and journalists — not just internal technical teams. We hadn't fully anticipated those\n          users. But because we took API-first design seriously — proper documentation, stable contracts,\n          predictable versioning — we could accommodate them without emergency refactors. Good default\n          decisions compound.\n        \n\n        Boring technology is underrated\n\n        \n          I've used many frameworks, platforms, and tools over 16 years. The things I reach for most are\n          rarely the newest. C#, .NET Core, SQL, REST APIs. Not because they're exciting, but because\n          they're boring in the right way — predictable, well-documented, with solutions to most problems\n          already written and indexed somewhere.\n        \n\n        \n          The geeky part of my brain is always interested in new tools. The part responsible for shipping\n          production systems that governments and NGOs depend on reaches for boring, stable, proven\n          technology. There's a time and place for experimentation — but that time is not when you're\n          migrating mission-critical enterprise systems.\n        \n\n        # The stack I keep reaching for\nC# / .NET Core  — predictable, performant, enterprise-grade\nAzure Functions  — serverless without the sharp edges\nSQL Server       — boring and reliable\nREST + OpenAPI   — universal, well-understood contracts\n\n        Pick boring where it matters. Experiment on the edges.\n\n        What hasn't changed\n\n        \n          I started my career building desktop Windows apps in 2008. Since then, the industry has moved\n          through cloud, mobile, microservices, containers, serverless, and now AI. Despite all of it,\n          the core problems remain identical:\n        \n\n        \n          Understanding what the user actually needs (not what they said they need)\n          Decomposing a complex problem into pieces teams can actually build\n          Communicating clearly with people who don't think in systems\n          Resisting the urge to over-engineer for requirements you don't have yet\n        \n\n        \n          The tools change. The problems don't. This is either reassuring or frustrating depending on\n          the day.\n        \n\n        On AI and what's next\n\n        \n          I've spent the last few years integrating AI into enterprise systems — Azure Cognitive Services,\n          generative AI pipelines, intelligent document processing. The capability jump is real. I'm not\n          a skeptic.\n        \n\n        \n          But I'm wary of a specific failure mode I'm starting to see: engineers using AI to ship bad\n          code faster. The leverage is genuine, but so is the risk of losing fluency with the fundamentals\n          that make systems maintainable and trustworthy over time. If you can't read and reason about\n          the code your AI assistant generates, you don't own it — you're just a conduit.\n        \n\n        \n          Use the tools. Stay curious. Understand what's happening underneath.\n        \n\n        \n\n        \n          This is a first post on what I hope will be a regular space for thinking out loud about\n          software, architecture, and building things that outlast the hype cycle. Not trying to be\n          authoritative — these are working notes from 16 years of stumbling through the industry.\n        \n\n        More soon.",
    "bodyHtml": "<p>\n          I've been writing production code since 2008. That's not something I say to impress anyone —\n          it's just context for what follows. Over those 16 years, I've built payroll systems for\n          Ethiopian government agencies, redesigned the UN's humanitarian affairs website, connected\n          2.1 million schools to real-time connectivity data, and migrated 20+ enterprise systems to\n          Azure. Different domains, different continents, wildly different constraints.\n        </p>\n\n        <p>\n          What follows are observations — not rules. They're the things I notice I actually believe,\n          now that I've had enough time to see patterns repeat.\n        </p>\n\n        <h2>Shipping is the skill</h2>\n\n        <p>\n          The first lesson, and the one I keep relearning: <strong>the ability to ship is a skill in\n          itself</strong>, separate from the ability to code well. Some of the most technically talented\n          engineers I've worked with were terrible at shipping. They would endlessly refine, re-architect,\n          add abstraction layers that \"would be useful later.\" Later rarely came.\n        </p>\n\n        <p>\n          Shipping doesn't mean cutting corners. It means making a concrete decision about what \"good\n          enough\" looks like for this context, right now, and executing on it. The next version will be\n          better because you'll know things you can't know until it's running in production with real users\n          making real decisions with it.\n        </p>\n\n        <blockquote>\n          The gap between what you imagine your software does and what it actually does collapses the\n          moment it hits production.\n        </blockquote>\n\n        <h2>Your users will always surprise you</h2>\n\n        <p>\n          Whether it was UN agency staff in New York or school administrators in Kenya, users consistently\n          do things with your software that you never imagined. This sounds obvious, but it has a practical\n          consequence: don't build for the user you assume you have. Build for the user who will find your\n          system three years from now, in a context you couldn't predict.\n        </p>\n\n        <p>\n          At UNICEF's Project Connect, we built REST APIs that ended up being consumed by policy groups,\n          economists, and journalists — not just internal technical teams. We hadn't fully anticipated those\n          users. But because we took API-first design seriously — proper documentation, stable contracts,\n          predictable versioning — we could accommodate them without emergency refactors. Good default\n          decisions compound.\n        </p>\n\n        <h2>Boring technology is underrated</h2>\n\n        <p>\n          I've used many frameworks, platforms, and tools over 16 years. The things I reach for most are\n          rarely the newest. C#, .NET Core, SQL, REST APIs. Not because they're exciting, but because\n          they're boring in the right way — predictable, well-documented, with solutions to most problems\n          already written and indexed somewhere.\n        </p>\n\n        <p>\n          The geeky part of my brain is always interested in new tools. The part responsible for shipping\n          production systems that governments and NGOs depend on reaches for boring, stable, proven\n          technology. There's a time and place for experimentation — but that time is not when you're\n          migrating mission-critical enterprise systems.\n        </p>\n\n        <pre><code># The stack I keep reaching for\nC# / .NET Core  — predictable, performant, enterprise-grade\nAzure Functions  — serverless without the sharp edges\nSQL Server       — boring and reliable\nREST + OpenAPI   — universal, well-understood contracts</code></pre>\n\n        <p>Pick boring where it matters. Experiment on the edges.</p>\n\n        <h2>What hasn't changed</h2>\n\n        <p>\n          I started my career building desktop Windows apps in 2008. Since then, the industry has moved\n          through cloud, mobile, microservices, containers, serverless, and now AI. Despite all of it,\n          the core problems remain identical:\n        </p>\n\n        <ul>\n          <li>Understanding what the user actually needs (not what they said they need)</li>\n          <li>Decomposing a complex problem into pieces teams can actually build</li>\n          <li>Communicating clearly with people who don't think in systems</li>\n          <li>Resisting the urge to over-engineer for requirements you don't have yet</li>\n        </ul>\n\n        <p>\n          The tools change. The problems don't. This is either reassuring or frustrating depending on\n          the day.\n        </p>\n\n        <h2>On AI and what's next</h2>\n\n        <p>\n          I've spent the last few years integrating AI into enterprise systems — Azure Cognitive Services,\n          generative AI pipelines, intelligent document processing. The capability jump is real. I'm not\n          a skeptic.\n        </p>\n\n        <p>\n          But I'm wary of a specific failure mode I'm starting to see: engineers using AI to ship bad\n          code faster. The leverage is genuine, but so is the risk of losing fluency with the fundamentals\n          that make systems maintainable and trustworthy over time. If you can't read and reason about\n          the code your AI assistant generates, you don't own it — you're just a conduit.\n        </p>\n\n        <p>\n          Use the tools. Stay curious. Understand what's happening underneath.\n        </p>\n\n        <hr>\n\n        <p>\n          This is a first post on what I hope will be a regular space for thinking out loud about\n          software, architecture, and building things that outlast the hype cycle. Not trying to be\n          authoritative — these are working notes from 16 years of stumbling through the industry.\n        </p>\n\n        <p>More soon.</p>"
  },
  {
    "id": "replacing-sisense-with-powerbi",
    "slug": "replacing-sisense-with-powerbi",
    "title": "Replacing Sisense with Power BI: A Migration Story",
    "date": "2025-03-01",
    "readTime": "4 min read",
    "published": false,
    "excerpt": "UNAIDS ran its entire BI ecosystem on Sisense. Dashboards, semantic models, data connections — everything. When the decision came to migrate to Power BI, it wasn't because Sisense was bad. It was about data governance, c...",
    "body": "UNAIDS ran its entire BI ecosystem on Sisense. Dashboards, semantic models, data connections — everything. When the decision came to migrate to Power BI, it wasn't because Sisense was bad. It was about data governance, cost consolidation with Microsoft licensing, and aligning with the broader Azure migration.\n\nNobody migrates BI platforms because it's fun.\n\nDashboards aren't portable\n\nThe first thing you learn in a BI migration is that you can't just \"move\" a dashboard. Every platform has its own charting engine, its own DAX equivalent, its own way of modeling relationships. A Sisense dashboard that took two hours to build will take six hours to rebuild in Power BI because nothing translates 1:1.\n\nWe audited every dashboard. Some were actively used. Some hadn't been opened in a year. Some were built by people who left the organization three years ago and nobody knew if they were still needed. The migration was a good excuse to clean house.\n\nSemantic models are where the complexity hides\n\nDashboards are the visible part. Semantic models — the data relationships, calculated columns, measures, and business logic underneath — are where the real migration pain lives. Sisense ElastiCubes and Power BI datasets model data differently. The join logic doesn't always translate. Calculated fields that were simple in one tool require DAX gymnastics in the other.\n\nWe rebuilt semantic models from scratch rather than trying to translate them. It was more work upfront, but it gave us the opportunity to clean up years of accumulated data modeling shortcuts. Some models had circular references, unused tables, and columns that nobody could explain. Starting fresh was the right call.\n\nData governance as a forcing function\n\nThe silver lining of the migration was data governance. Sisense had grown organically — anyone could create a dashboard, connect to any data source, and publish to whoever. Power BI, deployed properly with row-level security, workspace permissions, and certified datasets, gave us actual governance.\n\nNow there's a single source of truth for key metrics. Dashboards use certified datasets instead of connecting directly to production databases. Access is controlled. Refresh schedules are monitored. It's less flexible than the Wild West of Sisense, but the numbers are trustworthy, and in an organization making decisions about HIV response, trustworthy numbers matter.\n\nWhat I'd tell someone starting a BI migration\n\nDon't migrate everything. Audit first. Kill the dashboards nobody uses. Rebuild the ones that matter properly instead of trying to automate the translation. And budget twice as much time for the semantic model layer as you think you need.",
    "bodyHtml": "<p>UNAIDS ran its entire BI ecosystem on Sisense. Dashboards, semantic models, data connections — everything. When the decision came to migrate to Power BI, it wasn't because Sisense was bad. It was about data governance, cost consolidation with Microsoft licensing, and aligning with the broader Azure migration.</p>\n\n<p>Nobody migrates BI platforms because it's fun.</p>\n\n<h2>Dashboards aren't portable</h2>\n\n<p>The first thing you learn in a BI migration is that you can't just \"move\" a dashboard. Every platform has its own charting engine, its own DAX equivalent, its own way of modeling relationships. A Sisense dashboard that took two hours to build will take six hours to rebuild in Power BI because nothing translates 1:1.</p>\n\n<p>We audited every dashboard. Some were actively used. Some hadn't been opened in a year. Some were built by people who left the organization three years ago and nobody knew if they were still needed. The migration was a good excuse to clean house.</p>\n\n<h2>Semantic models are where the complexity hides</h2>\n\n<p>Dashboards are the visible part. Semantic models — the data relationships, calculated columns, measures, and business logic underneath — are where the real migration pain lives. Sisense ElastiCubes and Power BI datasets model data differently. The join logic doesn't always translate. Calculated fields that were simple in one tool require DAX gymnastics in the other.</p>\n\n<p>We rebuilt semantic models from scratch rather than trying to translate them. It was more work upfront, but it gave us the opportunity to clean up years of accumulated data modeling shortcuts. Some models had circular references, unused tables, and columns that nobody could explain. Starting fresh was the right call.</p>\n\n<h2>Data governance as a forcing function</h2>\n\n<p>The silver lining of the migration was data governance. Sisense had grown organically — anyone could create a dashboard, connect to any data source, and publish to whoever. Power BI, deployed properly with row-level security, workspace permissions, and certified datasets, gave us actual governance.</p>\n\n<p>Now there's a single source of truth for key metrics. Dashboards use certified datasets instead of connecting directly to production databases. Access is controlled. Refresh schedules are monitored. It's less flexible than the Wild West of Sisense, but the numbers are trustworthy, and in an organization making decisions about HIV response, trustworthy numbers matter.</p>\n\n<h2>What I'd tell someone starting a BI migration</h2>\n\n<p>Don't migrate everything. Audit first. Kill the dashboards nobody uses. Rebuild the ones that matter properly instead of trying to automate the translation. And budget twice as much time for the semantic model layer as you think you need.</p>"
  },
  {
    "id": "boring-parts-of-cloud-migration",
    "slug": "boring-parts-of-cloud-migration",
    "title": "The Boring Parts of Cloud Migration",
    "date": "2024-08-01",
    "readTime": "4 min read",
    "published": false,
    "excerpt": "Cloud migration blog posts usually focus on the architecture: Azure Functions here, API Management there, containers, microservices, the whole diagram. Those posts aren't wrong. But they skip the 80% of the work that's n...",
    "body": "Cloud migration blog posts usually focus on the architecture: Azure Functions here, API Management there, containers, microservices, the whole diagram. Those posts aren't wrong. But they skip the 80% of the work that's not architecture. It's the boring parts.\n\nInventory is the actual first step\n\nBefore you migrate anything, you need to know what you have. At UNAIDS, \"20+ enterprise systems\" was the official count. The real count was higher because nobody counted the spreadsheet-based workflows, the Access databases on someone's desktop, and the SharePoint sites that had quietly become mission-critical applications.\n\nI spent weeks doing discovery. Not exciting weeks. Weeks of meetings, screenshots, \"who owns this?\", \"what happens if this goes down?\", and \"wait, that's still running?\" The inventory doc was the most important deliverable of the entire migration, and it had zero technical complexity.\n\nDependencies are invisible until they break\n\nSystem A sends a nightly file to System B. System B triggers a Logic App that updates System C. System C has a hardcoded reference to the on-prem SQL Server IP address. Nobody documented any of this because it all just worked.\n\nYou find out about these dependencies when you migrate System A and System C stops working two days later. The connection between them went through System B, which you weren't planning to migrate until next quarter. Now you're debugging a production issue that spans three systems you've never seen together.\n\nThe fix: map dependencies before you move anything. Talk to users. Check connection strings. Look at scheduled tasks. Assume every system talks to at least two others and that none of it is documented.\n\nPeople migration is harder than system migration\n\nMoving a .NET app to Azure App Service is a solved problem. Getting the finance team to trust the new system — that's the unsolved one. People who've used the same application for five years don't care that it's now running on Azure. They care that the button they click every Monday morning is in the same place and works the same way.\n\nWe learned to do migration in phases with extensive parallel running. The old system stays live while the new one proves itself. Only when the users are comfortable do we cut over. It's slower and more expensive than a hard cutover, but it's the only approach that doesn't generate a flood of support tickets and erode trust.\n\nThe payoff is real but delayed\n\nAfter a year of migration work, the improvements are tangible. Better reliability, actual disaster recovery capability, proper monitoring, cost visibility. But the satisfaction comes slowly. Nobody celebrates a migration that went smoothly — they only notice when things break. The best outcome is that nothing happens, and \"nothing happened\" is a hard thing to take credit for.",
    "bodyHtml": "<p>Cloud migration blog posts usually focus on the architecture: Azure Functions here, API Management there, containers, microservices, the whole diagram. Those posts aren't wrong. But they skip the 80% of the work that's not architecture. It's the boring parts.</p>\n\n<h2>Inventory is the actual first step</h2>\n\n<p>Before you migrate anything, you need to know what you have. At UNAIDS, \"20+ enterprise systems\" was the official count. The real count was higher because nobody counted the spreadsheet-based workflows, the Access databases on someone's desktop, and the SharePoint sites that had quietly become mission-critical applications.</p>\n\n<p>I spent weeks doing discovery. Not exciting weeks. Weeks of meetings, screenshots, \"who owns this?\", \"what happens if this goes down?\", and \"wait, that's still running?\" The inventory doc was the most important deliverable of the entire migration, and it had zero technical complexity.</p>\n\n<h2>Dependencies are invisible until they break</h2>\n\n<p>System A sends a nightly file to System B. System B triggers a Logic App that updates System C. System C has a hardcoded reference to the on-prem SQL Server IP address. Nobody documented any of this because it all just worked.</p>\n\n<p>You find out about these dependencies when you migrate System A and System C stops working two days later. The connection between them went through System B, which you weren't planning to migrate until next quarter. Now you're debugging a production issue that spans three systems you've never seen together.</p>\n\n<p>The fix: map dependencies <em>before</em> you move anything. Talk to users. Check connection strings. Look at scheduled tasks. Assume every system talks to at least two others and that none of it is documented.</p>\n\n<h2>People migration is harder than system migration</h2>\n\n<p>Moving a .NET app to Azure App Service is a solved problem. Getting the finance team to trust the new system — that's the unsolved one. People who've used the same application for five years don't care that it's now running on Azure. They care that the button they click every Monday morning is in the same place and works the same way.</p>\n\n<p>We learned to do migration in phases with extensive parallel running. The old system stays live while the new one proves itself. Only when the users are comfortable do we cut over. It's slower and more expensive than a hard cutover, but it's the only approach that doesn't generate a flood of support tickets and erode trust.</p>\n\n<h2>The payoff is real but delayed</h2>\n\n<p>After a year of migration work, the improvements are tangible. Better reliability, actual disaster recovery capability, proper monitoring, cost visibility. But the satisfaction comes slowly. Nobody celebrates a migration that went smoothly — they only notice when things break. The best outcome is that nothing happens, and \"nothing happened\" is a hard thing to take credit for.</p>"
  },
  {
    "id": "three-un-agencies",
    "slug": "three-un-agencies",
    "title": "What Three UN Agencies Taught Me",
    "date": "2023-12-01",
    "readTime": "4 min read",
    "published": false,
    "excerpt": "I just started at UNAIDS. That makes three UN agencies — UNOCHA, UNICEF, and now UNAIDS. Different mandates, different cultures, same blue logo on the building. Here's what I've noticed across all three.",
    "body": "I just started at UNAIDS. That makes three UN agencies — UNOCHA, UNICEF, and now UNAIDS. Different mandates, different cultures, same blue logo on the building. Here's what I've noticed across all three.\n\nLegacy systems are everywhere, and they're load-bearing\n\nEvery agency has systems that someone built ten years ago that nobody fully understands but everyone depends on. These aren't just \"legacy\" in the deprecating sense tech people use the word — they're institutional infrastructure. Ripping them out and replacing them sounds clean until you realize that the weird behavior you thought was a bug is actually a policy requirement from 2014 that nobody documented.\n\nAt UNAIDS, I'm now leading the migration of 20+ enterprise systems from on-premise to Azure. The technical migration is the easy part. Understanding what each system actually does — and who breaks if it changes — that's the work.\n\nEngineers are translators\n\nThe most useful skill I've developed isn't a programming language. It's the ability to sit in a room with a program officer, a data analyst, a finance director, and an IT manager, and translate between all of them. Each person has a valid mental model of the system that's completely incompatible with everyone else's.\n\nThe program officer thinks in workflows. The data analyst thinks in schemas. The finance director thinks in compliance requirements. The IT manager thinks in infrastructure. My job is to build something that satisfies all four without requiring any of them to learn the others' language.\n\nSpeed is a choice, not a constraint\n\nUN agencies have a reputation for moving slowly. Some of that is deserved — procurement, approval chains, and stakeholder alignment take time. But a lot of the slowness is cultural inertia, not actual constraint. I've seen the same organization move at completely different speeds depending on who's driving the project and how much bureaucracy they're willing to route around.\n\nThe engineers who make the most impact in these environments are the ones who figure out what actually requires approval and what just requires confidence. You'd be surprised how many things that \"need approval\" really just need someone to do them and show the result.\n\nThe work matters in a way that's hard to explain\n\nI could earn more in private sector. I know this. Most people who work at the UN know this. But there's something about opening a dashboard and seeing data that's actively informing HIV response in sub-Saharan Africa, or school connectivity in Southeast Asia, or humanitarian funding allocation for an active crisis. It connects the code to something real in a way that enterprise SaaS just doesn't.\n\nThat's not a judgment. It's just the reason I keep coming back.",
    "bodyHtml": "<p>I just started at UNAIDS. That makes three UN agencies — UNOCHA, UNICEF, and now UNAIDS. Different mandates, different cultures, same blue logo on the building. Here's what I've noticed across all three.</p>\n\n<h2>Legacy systems are everywhere, and they're load-bearing</h2>\n\n<p>Every agency has systems that someone built ten years ago that nobody fully understands but everyone depends on. These aren't just \"legacy\" in the deprecating sense tech people use the word — they're institutional infrastructure. Ripping them out and replacing them sounds clean until you realize that the weird behavior you thought was a bug is actually a policy requirement from 2014 that nobody documented.</p>\n\n<p>At UNAIDS, I'm now leading the migration of 20+ enterprise systems from on-premise to Azure. The technical migration is the easy part. Understanding what each system actually does — and who breaks if it changes — that's the work.</p>\n\n<h2>Engineers are translators</h2>\n\n<p>The most useful skill I've developed isn't a programming language. It's the ability to sit in a room with a program officer, a data analyst, a finance director, and an IT manager, and translate between all of them. Each person has a valid mental model of the system that's completely incompatible with everyone else's.</p>\n\n<p>The program officer thinks in workflows. The data analyst thinks in schemas. The finance director thinks in compliance requirements. The IT manager thinks in infrastructure. My job is to build something that satisfies all four without requiring any of them to learn the others' language.</p>\n\n<h2>Speed is a choice, not a constraint</h2>\n\n<p>UN agencies have a reputation for moving slowly. Some of that is deserved — procurement, approval chains, and stakeholder alignment take time. But a lot of the slowness is cultural inertia, not actual constraint. I've seen the same organization move at completely different speeds depending on who's driving the project and how much bureaucracy they're willing to route around.</p>\n\n<p>The engineers who make the most impact in these environments are the ones who figure out what actually requires approval and what just requires confidence. You'd be surprised how many things that \"need approval\" really just need someone to do them and show the result.</p>\n\n<h2>The work matters in a way that's hard to explain</h2>\n\n<p>I could earn more in private sector. I know this. Most people who work at the UN know this. But there's something about opening a dashboard and seeing data that's actively informing HIV response in sub-Saharan Africa, or school connectivity in Southeast Asia, or humanitarian funding allocation for an active crisis. It connects the code to something real in a way that enterprise SaaS just doesn't.</p>\n\n<p>That's not a judgment. It's just the reason I keep coming back.</p>"
  },
  {
    "id": "etl-school-data-openstreetmap",
    "title": "Extracting School Data from OpenStreetMap: Building an ETL Pipeline at Global Scale",
    "slug": "etl-school-data-openstreetmap",
    "date": "2022-08-01",
    "readTime": "6 min read",
    "excerpt": "At UNICEF's Project Connect, one of our core challenges was building a comprehensive global database of schools. Government data was often incomplete, outdated, or simply unavailable. OpenStreetMap (OSM) — the crowd-sour...",
    "bodyHtml": "<p>\n          At UNICEF's Project Connect, one of our core challenges was building a comprehensive global\n          database of schools. Government data was often incomplete, outdated, or simply unavailable.\n          OpenStreetMap (OSM) — the crowd-sourced geographic database — turned out to be one of the\n          richest supplementary sources. But getting usable school data out of OSM is harder than it sounds.\n        </p>\n\n        <p>\n          This article documents the ETL pipeline I built to extract, transform, and load school data\n          from OSM across multiple countries — including Albania and Ukraine — and the technical\n          challenges that came with it.\n        </p>\n\n        <h2>The problem</h2>\n\n        <p>\n          OSM data is messy by nature. It's contributed by thousands of volunteer mappers with\n          varying standards. School data specifically suffers from:\n        </p>\n\n        <ul>\n          <li><strong>Inconsistent classification</strong> — a school might be tagged as <code>amenity=school</code>, <code>amenity=kindergarten</code>, <code>amenity=college</code>, or even just <code>building=school</code></li>\n          <li><strong>Multilingual content</strong> — school names in Arabic, Cyrillic, Latin scripts, sometimes multiple names per entry</li>\n          <li><strong>Duplicate records</strong> — the same school appearing as a node, a way, and a relation</li>\n          <li><strong>Missing or inconsistent metadata</strong> — education levels, student counts, and contact info are rarely standardized</li>\n        </ul>\n\n        <h2>The approach</h2>\n\n        <p>\n          The pipeline follows a three-phase methodology:\n        </p>\n\n        <p>\n          <strong>Extract:</strong> Data is pulled via the Overpass API, OSM's query interface.\n          We query for all elements tagged with education-related keys across a target country's\n          bounding box. This returns nodes (points), ways (polygons), and relations (grouped elements)\n          — each representing schools differently.\n        </p>\n\n        <p>\n          <strong>Transform:</strong> This is where most of the complexity lives. The pipeline\n          normalizes different OSM element types into a single schema, handles multilingual name\n          fields, classifies education levels from inconsistent tags, runs duplicate detection\n          using geographic proximity and name similarity, and validates coordinates and metadata.\n        </p>\n\n        <p>\n          <strong>Load:</strong> Clean records are loaded into a standardized database format\n          compatible with Project Connect's global school registry. The schema accommodates\n          regional variations while enforcing a minimum set of required fields.\n        </p>\n\n        <h2>Key findings</h2>\n\n        <p>\n          Data quality varies dramatically by region. Countries with active OSM mapping communities\n          had significantly better coverage and consistency. Duplicate detection was essential —\n          without it, school counts were inflated by 15-30% in some regions.\n        </p>\n\n        <p>\n          The education level classifier achieved reasonable accuracy but exposed a fundamental\n          limitation: OSM's tagging schema wasn't designed for the granularity that educational\n          planning requires. A \"school\" in OSM could be anything from a preschool to a university.\n        </p>\n\n        <p>\n          Multilingual handling proved critical for countries like Ukraine, where school names\n          appear in both Ukrainian and Russian, and for countries in the MENA region with\n          Arabic-script names that need careful normalization.\n        </p>\n\n        <h2>Takeaway</h2>\n\n        <p>\n          Crowd-sourced geographic data is powerful but requires significant engineering to\n          make usable at scale. The pipeline processed school records across multiple nations\n          and became part of Project Connect's broader data infrastructure — contributing\n          to the platform that now tracks connectivity for 2.1M+ schools globally.\n        </p>\n\n        <p>\n          The full technical article is available on\n          <a href=\"https://zenodo.org/records/15468243\" target=\"_blank\" rel=\"noopener noreferrer\">Zenodo</a>.\n        </p>",
    "body": "At UNICEF's Project Connect, one of our core challenges was building a comprehensive global database of schools. Government data was often incomplete, outdated, or simply unavailable. OpenStreetMap (OSM) — the crowd-sourced geographic database — turned out to be one of the richest supplementary sources. But getting usable school data out of OSM is harder than it sounds. This article documents the ETL pipeline I built to extract, transform, and load school data from OSM across multiple countries — including Albania and Ukraine — and the technical challenges that came with it. The problem OSM data is messy by nature. It's contributed by thousands of volunteer mappers with varying standards. School data specifically suffers from: Inconsistent classification — a school might be tagged as amenity=school , amenity=kindergarten , amenity=college , or even just building=school Multilingual content — school names in Arabic, Cyrillic, Latin scripts, sometimes multiple names per entry Duplicate records — the same school appearing as a node, a way, and a relation Missing or inconsistent metadata — education levels, student counts, and contact info are rarely standardized The approach The pipeline follows a three-phase methodology: Extract: Data is pulled via the Overpass API, OSM's query interface. We query for all elements tagged with education-related keys across a target country's bounding box. This returns nodes (points), ways (polygons), and relations (grouped elements) — each representing schools differently. Transform: This is where most of the complexity lives. The pipeline normalizes different OSM element types into a single schema, handles multilingual name fields, classifies education levels from inconsistent tags, runs duplicate detection using geographic proximity and name similarity, and validates coordinates and metadata. Load: Clean records are loaded into a standardized database format compatible with Project Connect's global school registry. The schema accommodates regional variations while enforcing a minimum set of required fields. Key findings Data quality varies dramatically by region. Countries with active OSM mapping communities had significantly better coverage and consistency. Duplicate detection was essential — without it, school counts were inflated by 15-30% in some regions. The education level classifier achieved reasonable accuracy but exposed a fundamental limitation: OSM's tagging schema wasn't designed for the granularity that educational planning requires. A \"school\" in OSM could be anything from a preschool to a university. Multilingual handling proved critical for countries like Ukraine, where school names appear in both Ukrainian and Russian, and for countries in the MENA region with Arabic-script names that need careful normalization. Takeaway Crowd-sourced geographic data is powerful but requires significant engineering to make usable at scale. The pipeline processed school records across multiple nations and became part of Project Connect's broader data infrastructure — contributing to the platform that now tracks connectivity for 2.1M+ schools globally. The full technical article is available on Zenodo .",
    "legacyPath": "posts/etl-school-data-openstreetmap.html",
    "published": true
  },
  {
    "id": "connecting-two-million-schools",
    "slug": "connecting-two-million-schools",
    "title": "Connecting 2.1 Million Schools to the Map",
    "date": "2021-01-01",
    "readTime": "5 min read",
    "published": false,
    "excerpt": "I just started at UNICEF's Giga initiative — Project Connect. The mission is simple to state and absurdly hard to execute: map every school in the world and connect them to the internet.",
    "body": "I just started at UNICEF's Giga initiative — Project Connect. The mission is simple to state and absurdly hard to execute: map every school in the world and connect them to the internet.\n\nWhen I joined, the platform had data on about 1.2 million schools. By the time I left in 2023, it was 2.1 million. This is the technical story of how we got there.\n\nThe data problem\n\nSchool data comes from everywhere: government ministries of education, OpenStreetMap, satellite imagery analysis, field surveys, partner organizations. Each source has different formats, different levels of accuracy, different definitions of what constitutes a \"school.\"\n\nA ministry might report 15,000 schools in a country. OSM has 12,000 mapped. A satellite analysis finds 18,000 buildings that look like schools. Which number is right? Usually none of them. The real answer is somewhere in between, and figuring out the overlap and gaps is a full-time job.\n\nWhat I built\n\nI architected the platform's data pipeline — the system that ingests school records from multiple sources, validates them, deduplicates across datasets, reconciles conflicting information, and maintains a clean master record for each school.\n\nThe validation pipeline alone was a major piece of work. Every incoming record gets checked for coordinate plausibility (is this school actually in the country it claims to be in?), duplicate detection against the existing database, metadata completeness, and conformance to the data schema. Records that fail validation don't get rejected — they get flagged for human review, because sometimes the outlier is the real school.\n\nThe REST APIs we built served both internal tools and external consumers. Governments used them to access their own school data. Researchers used them for connectivity analysis. ISPs used them to plan network buildouts. The API had to be stable, well-documented, and performant at scale — you can't break a downstream integration that a ministry of education depends on.\n\nThe Daily Check App\n\nOne project I'm particularly fond of is the Daily Check App. It monitored real-time internet connectivity across 89,000+ schools. Every day, the system checked whether each school's connection was up, measured speed, and flagged anomalies. When connectivity dropped across a region, it showed up on the dashboard within hours, not weeks.\n\nThe technical implementation was relatively straightforward — scheduled pings, data aggregation, threshold-based alerting. The impact was outsized because it turned \"school X has no internet\" from a complaint that takes months to investigate into a data point that shows up automatically.\n\nWorking across boundaries\n\nThe hardest part of this job wasn't the code. It was coordinating across teams that spoke completely different languages — not literally (though sometimes that too) but professionally. Engineers, economists, policy advisors, government officials, telecom vendors. Each group had different priorities, different definitions of success, and different timelines.\n\nMy job was to build systems that served all of them without being designed by committee. That meant making a lot of architectural decisions that were implicitly political, even if they didn't look like it from the code.",
    "bodyHtml": "<p>I just started at UNICEF's Giga initiative — Project Connect. The mission is simple to state and absurdly hard to execute: map every school in the world and connect them to the internet.</p>\n\n<p>When I joined, the platform had data on about 1.2 million schools. By the time I left in 2023, it was 2.1 million. This is the technical story of how we got there.</p>\n\n<h2>The data problem</h2>\n\n<p>School data comes from everywhere: government ministries of education, OpenStreetMap, satellite imagery analysis, field surveys, partner organizations. Each source has different formats, different levels of accuracy, different definitions of what constitutes a \"school.\"</p>\n\n<p>A ministry might report 15,000 schools in a country. OSM has 12,000 mapped. A satellite analysis finds 18,000 buildings that look like schools. Which number is right? Usually none of them. The real answer is somewhere in between, and figuring out the overlap and gaps is a full-time job.</p>\n\n<h2>What I built</h2>\n\n<p>I architected the platform's data pipeline — the system that ingests school records from multiple sources, validates them, deduplicates across datasets, reconciles conflicting information, and maintains a clean master record for each school.</p>\n\n<p>The validation pipeline alone was a major piece of work. Every incoming record gets checked for coordinate plausibility (is this school actually in the country it claims to be in?), duplicate detection against the existing database, metadata completeness, and conformance to the data schema. Records that fail validation don't get rejected — they get flagged for human review, because sometimes the outlier is the real school.</p>\n\n<p>The REST APIs we built served both internal tools and external consumers. Governments used them to access their own school data. Researchers used them for connectivity analysis. ISPs used them to plan network buildouts. The API had to be stable, well-documented, and performant at scale — you can't break a downstream integration that a ministry of education depends on.</p>\n\n<h2>The Daily Check App</h2>\n\n<p>One project I'm particularly fond of is the Daily Check App. It monitored real-time internet connectivity across 89,000+ schools. Every day, the system checked whether each school's connection was up, measured speed, and flagged anomalies. When connectivity dropped across a region, it showed up on the dashboard within hours, not weeks.</p>\n\n<p>The technical implementation was relatively straightforward — scheduled pings, data aggregation, threshold-based alerting. The impact was outsized because it turned \"school X has no internet\" from a complaint that takes months to investigate into a data point that shows up automatically.</p>\n\n<h2>Working across boundaries</h2>\n\n<p>The hardest part of this job wasn't the code. It was coordinating across teams that spoke completely different languages — not literally (though sometimes that too) but professionally. Engineers, economists, policy advisors, government officials, telecom vendors. Each group had different priorities, different definitions of success, and different timelines.</p>\n\n<p>My job was to build systems that served all of them without being designed by committee. That meant making a lot of architectural decisions that were implicitly political, even if they didn't look like it from the code.</p>"
  },
  {
    "id": "starting-over-new-country",
    "slug": "starting-over-new-country",
    "title": "Starting Over in a New Country",
    "date": "2019-04-01",
    "readTime": "3 min read",
    "published": false,
    "excerpt": "In February 2019, I moved to Canada and started at Dillon Consulting in London, Ontario. After two years at a UN agency in New York building global humanitarian platforms, I was now building an internal scorecard applica...",
    "body": "In February 2019, I moved to Canada and started at Dillon Consulting in London, Ontario. After two years at a UN agency in New York building global humanitarian platforms, I was now building an internal scorecard application for a Canadian engineering firm's 25 offices.\n\nOn paper, a massive downshift. In practice, a different kind of useful.\n\nThe adjustment\n\nThe UN operates at a weird scale — global scope, bureaucratic pace, enormous stakes but long timelines. Consulting is the opposite in almost every way. Smaller scope, faster pace, clients who want results last week, and budgets that actually run out.\n\nAt Dillon, I had to re-learn how to be fast. At UNOCHA, a six-month timeline for a feature was normal. At Dillon, if the scorecard app wasn't working by quarter-end, the business impact was immediate and visible. No hiding behind complexity.\n\nWhat the scorecard taught me\n\nThe Associate Score Card sounds mundane — it measured performance metrics across the company's offices. But it was one of those projects where the simplicity of the concept hides real complexity: different offices had different KPIs, some metrics were subjective, the aggregation logic was political, and everyone had opinions about what \"performance\" meant.\n\nI built it with C#, .NET, and SQL Server — familiar tools. The interesting part wasn't the tech. It was learning to navigate a Canadian corporate environment after years in international organizations. Different meeting culture, different communication norms, different assumptions about how decisions get made.\n\nThe value of smaller scope\n\nThere's something clarifying about building a system that 200 people use instead of 200,000. You can talk to most of your users. You can watch them use it. You can ship a fix and get feedback the same day. After years of working at global scale where feedback loops were measured in months, this was refreshing.\n\nI don't regret the move. Every environment teaches you something different. The UN taught me scale, stakeholder complexity, and data responsibility. Dillon taught me speed, pragmatism, and the value of shipping small things well.",
    "bodyHtml": "<p>In February 2019, I moved to Canada and started at Dillon Consulting in London, Ontario. After two years at a UN agency in New York building global humanitarian platforms, I was now building an internal scorecard application for a Canadian engineering firm's 25 offices.</p>\n\n<p>On paper, a massive downshift. In practice, a different kind of useful.</p>\n\n<h2>The adjustment</h2>\n\n<p>The UN operates at a weird scale — global scope, bureaucratic pace, enormous stakes but long timelines. Consulting is the opposite in almost every way. Smaller scope, faster pace, clients who want results last week, and budgets that actually run out.</p>\n\n<p>At Dillon, I had to re-learn how to be fast. At UNOCHA, a six-month timeline for a feature was normal. At Dillon, if the scorecard app wasn't working by quarter-end, the business impact was immediate and visible. No hiding behind complexity.</p>\n\n<h2>What the scorecard taught me</h2>\n\n<p>The Associate Score Card sounds mundane — it measured performance metrics across the company's offices. But it was one of those projects where the simplicity of the concept hides real complexity: different offices had different KPIs, some metrics were subjective, the aggregation logic was political, and everyone had opinions about what \"performance\" meant.</p>\n\n<p>I built it with C#, .NET, and SQL Server — familiar tools. The interesting part wasn't the tech. It was learning to navigate a Canadian corporate environment after years in international organizations. Different meeting culture, different communication norms, different assumptions about how decisions get made.</p>\n\n<h2>The value of smaller scope</h2>\n\n<p>There's something clarifying about building a system that 200 people use instead of 200,000. You can talk to most of your users. You can watch them use it. You can ship a fix and get feedback the same day. After years of working at global scale where feedback loops were measured in months, this was refreshing.</p>\n\n<p>I don't regret the move. Every environment teaches you something different. The UN taught me scale, stakeholder complexity, and data responsibility. Dillon taught me speed, pragmatism, and the value of shipping small things well.</p>"
  },
  {
    "id": "redesigning-unocha-org",
    "slug": "redesigning-unocha-org",
    "title": "Redesigning unocha.org",
    "date": "2017-09-01",
    "readTime": "5 min read",
    "published": false,
    "excerpt": "When I joined UNOCHA in New York, the corporate website was due for a complete overhaul. New information architecture, new CMS, new everything. This was the main public-facing presence for the UN's coordination of humani...",
    "body": "When I joined UNOCHA in New York, the corporate website was due for a complete overhaul. New information architecture, new CMS, new everything. This was the main public-facing presence for the UN's coordination of humanitarian response worldwide. No pressure.\n\nThe scale of the thing\n\nunocha.org isn't a simple brochure site. It pulls data from multiple UN systems — ReliefWeb for situation reports, the Financial Tracking Service for funding data, humanitarian response plans, country-level pages with real-time crisis information. All of it had to be surfaced coherently to audiences ranging from diplomats to journalists to aid workers in the field.\n\nThe previous site had grown organically over years. Content was scattered across inconsistent templates. The information architecture had no clear hierarchy. Finding a specific country situation report required knowing exactly where to look, which defeated the purpose.\n\nWhat I worked on\n\nI was responsible for the technical implementation — building the new site architecture, migrating content, integrating the external data sources, and implementing the data visualizations. We used Drupal as the CMS backbone, with custom modules for the API integrations.\n\nThe data viz work was particularly interesting. The Global Humanitarian Overview needed interactive charts and maps showing funding gaps, people in need, and response coverage across every humanitarian crisis globally. These weren't static images — they pulled live data and had to work on everything from a desktop in Geneva to a phone on a 2G connection in South Sudan.\n\nSecurity and access\n\nUN websites are targets. We implemented a complete overhaul of the authentication system, hardened the CMS against common attack vectors, and set up monitoring. This was my first real exposure to security as a primary concern rather than an afterthought, and it changed how I approach every project since.\n\nMoving from local to global\n\nThis was my first international role after eight years working exclusively in Ethiopia. The contrast was enormous. Different development practices, different team dynamics, different stakeholder expectations. I went from being the most senior person in the room to being the newest. That was humbling and exactly what I needed.\n\nThe site launched. It's been redesigned again since — that's the nature of the web. But building something that humanitarian workers actually used during active crises was a different kind of satisfying than anything I'd done before.",
    "bodyHtml": "<p>When I joined UNOCHA in New York, the corporate website was due for a complete overhaul. New information architecture, new CMS, new everything. This was the main public-facing presence for the UN's coordination of humanitarian response worldwide. No pressure.</p>\n\n<h2>The scale of the thing</h2>\n\n<p>unocha.org isn't a simple brochure site. It pulls data from multiple UN systems — ReliefWeb for situation reports, the Financial Tracking Service for funding data, humanitarian response plans, country-level pages with real-time crisis information. All of it had to be surfaced coherently to audiences ranging from diplomats to journalists to aid workers in the field.</p>\n\n<p>The previous site had grown organically over years. Content was scattered across inconsistent templates. The information architecture had no clear hierarchy. Finding a specific country situation report required knowing exactly where to look, which defeated the purpose.</p>\n\n<h2>What I worked on</h2>\n\n<p>I was responsible for the technical implementation — building the new site architecture, migrating content, integrating the external data sources, and implementing the data visualizations. We used Drupal as the CMS backbone, with custom modules for the API integrations.</p>\n\n<p>The data viz work was particularly interesting. The Global Humanitarian Overview needed interactive charts and maps showing funding gaps, people in need, and response coverage across every humanitarian crisis globally. These weren't static images — they pulled live data and had to work on everything from a desktop in Geneva to a phone on a 2G connection in South Sudan.</p>\n\n<h2>Security and access</h2>\n\n<p>UN websites are targets. We implemented a complete overhaul of the authentication system, hardened the CMS against common attack vectors, and set up monitoring. This was my first real exposure to security as a primary concern rather than an afterthought, and it changed how I approach every project since.</p>\n\n<h2>Moving from local to global</h2>\n\n<p>This was my first international role after eight years working exclusively in Ethiopia. The contrast was enormous. Different development practices, different team dynamics, different stakeholder expectations. I went from being the most senior person in the room to being the newest. That was humbling and exactly what I needed.</p>\n\n<p>The site launched. It's been redesigned again since — that's the nature of the web. But building something that humanitarian workers actually used during active crises was a different kind of satisfying than anything I'd done before.</p>"
  },
  {
    "id": "customizing-dspace-uneca",
    "title": "Customizing DSpace for Multimedia: Building UNECA's Institutional Repository",
    "slug": "customizing-dspace-uneca",
    "date": "2016-05-01",
    "readTime": "5 min read",
    "excerpt": "The United Nations Economic Commission for Africa (UNECA) needed an institutional repository that could handle more than just PDFs and text documents. Their knowledge base included photos, audio recordings, video archive...",
    "bodyHtml": "<p>\n          The United Nations Economic Commission for Africa (UNECA) needed an institutional\n          repository that could handle more than just PDFs and text documents. Their knowledge\n          base included photos, audio recordings, video archives, and maps — all sitting in\n          scattered storage with no unified access layer.\n        </p>\n\n        <p>\n          DSpace, the most widely used open-source repository platform in the academic world,\n          was the logical starting point. But out of the box, DSpace treats every file the\n          same: upload it, attach Dublin Core metadata, and offer a download link. For\n          multimedia content, that experience is inadequate. Users shouldn't have to download\n          a 500MB video just to see if it's the right one.\n        </p>\n\n        <h2>What we built</h2>\n\n        <p>\n          The customized system — the Multimedia Institutional Repository (MIR) — extends\n          DSpace to handle items based on their content type. Images render inline with\n          proper previews. Audio and video content streams through integrated players\n          directly in the browser, without requiring downloads. The metadata schema was\n          extended beyond standard Dublin Core to capture multimedia-specific attributes\n          like duration, resolution, codec information, and geographic context.\n        </p>\n\n        <p>\n          This matters more than it sounds in a UN context, where offices across Africa\n          often operate on limited bandwidth. Previewing a document's metadata and\n          streaming a 30-second audio clip is fundamentally different from downloading\n          a full archive just to check its contents.\n        </p>\n\n        <h2>Metadata design</h2>\n\n        <p>\n          Standard Dublin Core gives you 15 elements — title, creator, subject, description,\n          and so on. For text documents, that's usually sufficient. For multimedia, it's not.\n          A video recording of a conference session needs fields for speakers, duration,\n          language of the recording, related documents, and session context.\n        </p>\n\n        <p>\n          We developed an extended metadata framework that maintained Dublin Core compatibility\n          (critical for interoperability with other institutional repositories) while adding\n          the multimedia-specific fields UNECA needed. The goal was to let users preview\n          content details and make relevance decisions before committing to a download.\n        </p>\n\n        <h2>Results</h2>\n\n        <p>\n          User acceptance testing showed strong agreement that MIR provided meaningful\n          benefits over the base DSpace experience, particularly around navigation and\n          content discovery. The bandwidth savings from inline previewing and streaming\n          were significant for offices with constrained connectivity.\n        </p>\n\n        <p>\n          The full thesis is published at Addis Ababa University and available on\n          <a href=\"https://zenodo.org/records/11954023\" target=\"_blank\" rel=\"noopener noreferrer\">Zenodo</a>.\n          The source code is on\n          <a href=\"https://github.com/sanoylab/Customizing-DSpace-for-Managing-Multimedia-Institutional-Repository\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>.\n        </p>",
    "body": "The United Nations Economic Commission for Africa (UNECA) needed an institutional repository that could handle more than just PDFs and text documents. Their knowledge base included photos, audio recordings, video archives, and maps — all sitting in scattered storage with no unified access layer. DSpace, the most widely used open-source repository platform in the academic world, was the logical starting point. But out of the box, DSpace treats every file the same: upload it, attach Dublin Core metadata, and offer a download link. For multimedia content, that experience is inadequate. Users shouldn't have to download a 500MB video just to see if it's the right one. What we built The customized system — the Multimedia Institutional Repository (MIR) — extends DSpace to handle items based on their content type. Images render inline with proper previews. Audio and video content streams through integrated players directly in the browser, without requiring downloads. The metadata schema was extended beyond standard Dublin Core to capture multimedia-specific attributes like duration, resolution, codec information, and geographic context. This matters more than it sounds in a UN context, where offices across Africa often operate on limited bandwidth. Previewing a document's metadata and streaming a 30-second audio clip is fundamentally different from downloading a full archive just to check its contents. Metadata design Standard Dublin Core gives you 15 elements — title, creator, subject, description, and so on. For text documents, that's usually sufficient. For multimedia, it's not. A video recording of a conference session needs fields for speakers, duration, language of the recording, related documents, and session context. We developed an extended metadata framework that maintained Dublin Core compatibility (critical for interoperability with other institutional repositories) while adding the multimedia-specific fields UNECA needed. The goal was to let users preview content details and make relevance decisions before committing to a download. Results User acceptance testing showed strong agreement that MIR provided meaningful benefits over the base DSpace experience, particularly around navigation and content discovery. The bandwidth savings from inline previewing and streaming were significant for offices with constrained connectivity. The full thesis is published at Addis Ababa University and available on Zenodo . The source code is on GitHub .",
    "legacyPath": "posts/customizing-dspace-uneca.html",
    "published": true
  },
  {
    "id": "cms-that-actually-got-used",
    "slug": "cms-that-actually-got-used",
    "title": "Building a CMS That Actually Got Used",
    "date": "2015-06-01",
    "readTime": "3 min read",
    "published": false,
    "excerpt": "At Necom, I led the engineering team building a CMS platform for government intranet systems. Multiple agencies across Addis Ababa adopted it. That sentence sounds simple but getting there was anything but.",
    "body": "At Necom, I led the engineering team building a CMS platform for government intranet systems. Multiple agencies across Addis Ababa adopted it. That sentence sounds simple but getting there was anything but.\n\nThe common failure mode for government CMS projects is this: you build a beautiful system, deploy it, train 30 people, and come back six months later to find everyone still emailing Word documents around. The CMS sits empty.\n\nWhat we did differently\n\nWe made the CMS the path of least resistance. Instead of asking people to learn a new tool on top of their existing workflow, we made the CMS the workflow. Document approvals went through it. Internal announcements had to go through it. If you wanted to publish anything on the intranet, the CMS was the only way.\n\nThat sounds coercive, but it's really just product design. People don't adopt tools because the tools are good. They adopt them because the tools are easier than the alternative. Our job was to make the CMS easier than email.\n\nThe technical side\n\nNothing revolutionary. ASP.NET, SQL Server, standard role-based access control. The interesting engineering was in the permissions model — government agencies have complex hierarchies and the content approval chain had to mirror the organizational chart exactly. A document published by the wrong person in the wrong department was a political problem, not just a UX issue.\n\nWe also had to handle Amharic content properly — Unicode, search indexing, sorting. In 2015, this was still painful in ways that English-only developers never think about.\n\nThe real lesson\n\nThis was my first time leading a team, and the biggest thing I learned was that my job was no longer writing the best code. It was making sure the team could ship together. Code reviews, architecture decisions, mentoring junior devs, shielding the team from scope creep in meetings — that was the actual work. I still wrote code, but the leverage had shifted.\n\nThe CMS ran across multiple agencies. People used it every day. It wasn't glamorous, but it worked, and in government software, \"it works and people use it\" is the highest compliment.",
    "bodyHtml": "<p>At Necom, I led the engineering team building a CMS platform for government intranet systems. Multiple agencies across Addis Ababa adopted it. That sentence sounds simple but getting there was anything but.</p>\n\n<p>The common failure mode for government CMS projects is this: you build a beautiful system, deploy it, train 30 people, and come back six months later to find everyone still emailing Word documents around. The CMS sits empty.</p>\n\n<h2>What we did differently</h2>\n\n<p>We made the CMS the path of least resistance. Instead of asking people to learn a new tool on top of their existing workflow, we made the CMS <em>the</em> workflow. Document approvals went through it. Internal announcements had to go through it. If you wanted to publish anything on the intranet, the CMS was the only way.</p>\n\n<p>That sounds coercive, but it's really just product design. People don't adopt tools because the tools are good. They adopt them because the tools are easier than the alternative. Our job was to make the CMS easier than email.</p>\n\n<h2>The technical side</h2>\n\n<p>Nothing revolutionary. ASP.NET, SQL Server, standard role-based access control. The interesting engineering was in the permissions model — government agencies have complex hierarchies and the content approval chain had to mirror the organizational chart exactly. A document published by the wrong person in the wrong department was a political problem, not just a UX issue.</p>\n\n<p>We also had to handle Amharic content properly — Unicode, search indexing, sorting. In 2015, this was still painful in ways that English-only developers never think about.</p>\n\n<h2>The real lesson</h2>\n\n<p>This was my first time leading a team, and the biggest thing I learned was that my job was no longer writing the best code. It was making sure the team could ship together. Code reviews, architecture decisions, mentoring junior devs, shielding the team from scope creep in meetings — that was the actual work. I still wrote code, but the leverage had shifted.</p>\n\n<p>The CMS ran across multiple agencies. People used it every day. It wasn't glamorous, but it worked, and in government software, \"it works and people use it\" is the highest compliment.</p>"
  },
  {
    "id": "twenty-apps-five-years",
    "slug": "twenty-apps-five-years",
    "title": "20 Apps in 5 Years: What I'd Do Differently",
    "date": "2013-07-01",
    "readTime": "3 min read",
    "published": false,
    "excerpt": "I'm leaving Cybersoft next year after almost six years. I counted the other day — I've shipped over 20 applications. HR systems, finance modules, fixed asset trackers, construction management tools, various MIS platforms...",
    "body": "I'm leaving Cybersoft next year after almost six years. I counted the other day — I've shipped over 20 applications. HR systems, finance modules, fixed asset trackers, construction management tools, various MIS platforms. All for government agencies in and around Addis.\n\nMost of them are still running. Some of them I'm proud of. A few of them I'd rewrite from scratch if I could.\n\nWhat I got wrong\n\nCopy-paste architecture. When you're shipping a new app every few months, you start copying patterns from the last one. Same data layer, same form layout, same stored procedure structure. It works until it doesn't. I built too many things that were structurally identical but conceptually different, and when one needed to evolve, the copy-paste foundation made it painful.\n\nNot enough abstraction, then too much. Early on, I wrote everything inline. No separation, no reusable components. Then I overcorrected and started building \"frameworks\" that nobody else on the team could understand. The sweet spot — clean, simple, reusable where it matters — took years to find.\n\nDocumentation I wrote for myself. I documented things in a way that made sense to me in that moment. Six months later, it made sense to nobody, including me. I've since learned that documentation is for the person who comes after you, not for you right now.\n\nWhat I got right\n\nFinishing. Every single one shipped. Not all of them were pretty, but they all went live and real people used them to do real work. In an environment where half of government IT projects get abandoned, shipping was the most important skill I developed.\n\nLearning SQL properly. Years of writing T-SQL for complex business logic gave me a deep understanding of databases that still pays off. When everyone around me was excited about ORMs, I was the person who could look at the generated SQL and explain why the query was slow.\n\nStaying close to users. I spent time in government offices watching people use my software. Uncomfortable sometimes — nobody likes seeing someone struggle with something you built. But it made every subsequent version better.\n\nI'm moving on to Necom next. New team, new problems. But the instincts I built here — ship it, watch it, fix it — those are coming with me.",
    "bodyHtml": "<p>I'm leaving Cybersoft next year after almost six years. I counted the other day — I've shipped over 20 applications. HR systems, finance modules, fixed asset trackers, construction management tools, various MIS platforms. All for government agencies in and around Addis.</p>\n\n<p>Most of them are still running. Some of them I'm proud of. A few of them I'd rewrite from scratch if I could.</p>\n\n<h2>What I got wrong</h2>\n\n<p><strong>Copy-paste architecture.</strong> When you're shipping a new app every few months, you start copying patterns from the last one. Same data layer, same form layout, same stored procedure structure. It works until it doesn't. I built too many things that were structurally identical but conceptually different, and when one needed to evolve, the copy-paste foundation made it painful.</p>\n\n<p><strong>Not enough abstraction, then too much.</strong> Early on, I wrote everything inline. No separation, no reusable components. Then I overcorrected and started building \"frameworks\" that nobody else on the team could understand. The sweet spot — clean, simple, reusable where it matters — took years to find.</p>\n\n<p><strong>Documentation I wrote for myself.</strong> I documented things in a way that made sense to me in that moment. Six months later, it made sense to nobody, including me. I've since learned that documentation is for the person who comes after you, not for you right now.</p>\n\n<h2>What I got right</h2>\n\n<p><strong>Finishing.</strong> Every single one shipped. Not all of them were pretty, but they all went live and real people used them to do real work. In an environment where half of government IT projects get abandoned, shipping was the most important skill I developed.</p>\n\n<p><strong>Learning SQL properly.</strong> Years of writing T-SQL for complex business logic gave me a deep understanding of databases that still pays off. When everyone around me was excited about ORMs, I was the person who could look at the generated SQL and explain why the query was slow.</p>\n\n<p><strong>Staying close to users.</strong> I spent time in government offices watching people use my software. Uncomfortable sometimes — nobody likes seeing someone struggle with something you built. But it made every subsequent version better.</p>\n\n<p>I'm moving on to Necom next. New team, new problems. But the instincts I built here — ship it, watch it, fix it — those are coming with me.</p>"
  },
  {
    "id": "government-software-is-hard",
    "slug": "government-software-is-hard",
    "title": "Why Government Software Is Hard",
    "date": "2011-11-01",
    "readTime": "3 min read",
    "published": false,
    "excerpt": "I've been building applications for Ethiopian government agencies for three years now. MIS platforms, financial systems, asset tracking, construction project management. I keep running into the same problems, and almost...",
    "body": "I've been building applications for Ethiopian government agencies for three years now. MIS platforms, financial systems, asset tracking, construction project management. I keep running into the same problems, and almost none of them are technical.\n\nThe real constraints\n\nInternet connectivity is unreliable. Power goes out. Computers are shared between three or four people. The person who approved the requirements left the agency six months ago and their replacement has different priorities. Training sessions happen once, and the manual you wrote gets filed somewhere nobody will ever look.\n\nYou can't design around these constraints by pretending they don't exist. Web apps that require constant connectivity don't work when the connection drops twice a day. UIs that assume each user has their own machine don't work when four people share one desktop and forget to log out.\n\nTwo things I've changed\n\nOffline-first thinking. Even for web apps, I've started building in local caching and graceful degradation. If the connection drops mid-form, don't lose the data. Queue it. Sync later. Users shouldn't have to understand network architecture to do their job.\n\nTraining the trainers. Instead of running training sessions for end users — who rotate, get reassigned, or simply forget — I've started focusing on finding one person in each department who actually cares about the system. Train them deeply. Give them the tools to train others. One invested champion in the finance office is worth twenty people who sat through a mandatory two-hour session.\n\nIt's not a tech problem\n\nThe hardest part of government software isn't the code. It's understanding that your software lives in an ecosystem of bureaucracy, staff turnover, infrastructure limitations, and competing priorities. You can write the cleanest code in the world, and it won't matter if nobody uses it because the person who was supposed to enter the data got transferred to another office.\n\nI don't have solutions for all of this. But acknowledging the constraints honestly has made me a better engineer than any framework or design pattern.",
    "bodyHtml": "<p>I've been building applications for Ethiopian government agencies for three years now. MIS platforms, financial systems, asset tracking, construction project management. I keep running into the same problems, and almost none of them are technical.</p>\n\n<h2>The real constraints</h2>\n\n<p>Internet connectivity is unreliable. Power goes out. Computers are shared between three or four people. The person who approved the requirements left the agency six months ago and their replacement has different priorities. Training sessions happen once, and the manual you wrote gets filed somewhere nobody will ever look.</p>\n\n<p>You can't design around these constraints by pretending they don't exist. Web apps that require constant connectivity don't work when the connection drops twice a day. UIs that assume each user has their own machine don't work when four people share one desktop and forget to log out.</p>\n\n<h2>Two things I've changed</h2>\n\n<p><strong>Offline-first thinking.</strong> Even for web apps, I've started building in local caching and graceful degradation. If the connection drops mid-form, don't lose the data. Queue it. Sync later. Users shouldn't have to understand network architecture to do their job.</p>\n\n<p><strong>Training the trainers.</strong> Instead of running training sessions for end users — who rotate, get reassigned, or simply forget — I've started focusing on finding one person in each department who actually cares about the system. Train them deeply. Give them the tools to train others. One invested champion in the finance office is worth twenty people who sat through a mandatory two-hour session.</p>\n\n<h2>It's not a tech problem</h2>\n\n<p>The hardest part of government software isn't the code. It's understanding that your software lives in an ecosystem of bureaucracy, staff turnover, infrastructure limitations, and competing priorities. You can write the cleanest code in the world, and it won't matter if nobody uses it because the person who was supposed to enter the data got transferred to another office.</p>\n\n<p>I don't have solutions for all of this. But acknowledging the constraints honestly has made me a better engineer than any framework or design pattern.</p>"
  },
  {
    "id": "erp-module-that-taught-me",
    "slug": "erp-module-that-taught-me",
    "title": "The ERP Module That Taught Me Software Engineering",
    "date": "2009-03-01",
    "readTime": "4 min read",
    "published": false,
    "excerpt": "Six months into my first real job at Cybersoft in Addis Ababa, I got assigned to the HR module of a government ERP system. I had a CS diploma, a fresh B.Sc., and zero idea how payroll actually worked.",
    "body": "Six months into my first real job at Cybersoft in Addis Ababa, I got assigned to the HR module of a government ERP system. I had a CS diploma, a fresh B.Sc., and zero idea how payroll actually worked.\n\nThe spec was straightforward on paper: calculate monthly salary, deductions, pension contributions, and generate payslips for a few hundred civil servants. I figured two weeks, maybe three.\n\nIt took four months.\n\nPayroll is not math\n\nThe first thing you learn about payroll is that it's not a math problem — it's a policy problem. Every edge case is a human story. This employee is on unpaid leave for two weeks but gets housing allowance for the full month. That one transferred between departments mid-cycle and each department has a different overtime rate. Someone else retired on the 14th and their pension calculation uses a different formula than the standard deduction.\n\nNone of this was in the spec. The spec said \"calculate salary.\" The actual rules lived in binders on a shelf in the finance office, and half of them contradicted each other.\n\nSQL Server and stored procedures everywhere\n\nThis was 2009. The architecture was classic n-tier: Windows Forms front end, a thick business logic layer, and SQL Server doing the heavy lifting. We wrote a lot of stored procedures. Not \"some\" — a lot. Complex payroll calculations lived in T-SQL because that's where the data was and because the senior devs said so.\n\nI hated debugging stored procedures. I still do. But I learned something important from that experience: the database isn't just storage. Understanding what happens at the data layer — joins, transactions, isolation levels — matters more than any framework you'll ever learn.\n\nWhat stuck\n\nThree things from that first module shaped how I think about software:\n\nFirst, talk to the people who actually use the system. The finance team knew things the project manager didn't. They had workarounds, informal rules, and institutional knowledge that no document captured. I started sitting with them during payroll runs, watching where they got stuck, and that changed everything.\n\nSecond, edge cases aren't edge cases. If your payroll system can't handle a mid-month transfer, it can't handle payroll. The \"80% case\" is the easy part. The remaining 20% is the actual job.\n\nThird, shipping something imperfect is better than perfecting something unshipped. We released with known limitations and fixed them over the next two cycles. The finance team got their payslips on time. That mattered more than elegant code.\n\nI went on to build 20+ applications at Cybersoft over the next five years. But that HR module was the one that made me an engineer.",
    "bodyHtml": "<p>Six months into my first real job at Cybersoft in Addis Ababa, I got assigned to the HR module of a government ERP system. I had a CS diploma, a fresh B.Sc., and zero idea how payroll actually worked.</p>\n\n<p>The spec was straightforward on paper: calculate monthly salary, deductions, pension contributions, and generate payslips for a few hundred civil servants. I figured two weeks, maybe three.</p>\n\n<p>It took four months.</p>\n\n<h2>Payroll is not math</h2>\n\n<p>The first thing you learn about payroll is that it's not a math problem — it's a policy problem. Every edge case is a human story. This employee is on unpaid leave for two weeks but gets housing allowance for the full month. That one transferred between departments mid-cycle and each department has a different overtime rate. Someone else retired on the 14th and their pension calculation uses a different formula than the standard deduction.</p>\n\n<p>None of this was in the spec. The spec said \"calculate salary.\" The actual rules lived in binders on a shelf in the finance office, and half of them contradicted each other.</p>\n\n<h2>SQL Server and stored procedures everywhere</h2>\n\n<p>This was 2009. The architecture was classic n-tier: Windows Forms front end, a thick business logic layer, and SQL Server doing the heavy lifting. We wrote a lot of stored procedures. Not \"some\" — a lot. Complex payroll calculations lived in T-SQL because that's where the data was and because the senior devs said so.</p>\n\n<p>I hated debugging stored procedures. I still do. But I learned something important from that experience: the database isn't just storage. Understanding what happens at the data layer — joins, transactions, isolation levels — matters more than any framework you'll ever learn.</p>\n\n<h2>What stuck</h2>\n\n<p>Three things from that first module shaped how I think about software:</p>\n\n<p>First, <strong>talk to the people who actually use the system</strong>. The finance team knew things the project manager didn't. They had workarounds, informal rules, and institutional knowledge that no document captured. I started sitting with them during payroll runs, watching where they got stuck, and that changed everything.</p>\n\n<p>Second, <strong>edge cases aren't edge cases</strong>. If your payroll system can't handle a mid-month transfer, it can't handle payroll. The \"80% case\" is the easy part. The remaining 20% is the actual job.</p>\n\n<p>Third, <strong>shipping something imperfect is better than perfecting something unshipped</strong>. We released with known limitations and fixed them over the next two cycles. The finance team got their payslips on time. That mattered more than elegant code.</p>\n\n<p>I went on to build 20+ applications at Cybersoft over the next five years. But that HR module was the one that made me an engineer.</p>"
  },
  {
    "id": "localized-portal-four-languages",
    "title": "Building a Multilingual Portal in 4 Ethiopian Languages",
    "slug": "localized-portal-four-languages",
    "date": "2008-08-01",
    "readTime": "4 min read",
    "excerpt": "In 2008, web services in Ethiopia were almost exclusively English-only. Email providers had partial localization at best. Job boards and business directories were fragmented and inaccessible to anyone who didn't read Eng...",
    "bodyHtml": "<p>\n          In 2008, web services in Ethiopia were almost exclusively English-only. Email providers\n          had partial localization at best. Job boards and business directories were fragmented\n          and inaccessible to anyone who didn't read English fluently. For a country with\n          dozens of languages and three widely spoken ones beyond English — Amharic, Oromifa,\n          and Tigrinya — this was a real barrier.\n        </p>\n\n        <p>\n          This project was my B.Sc. thesis at HiLCoE: a unified web portal called addisportal.com\n          that consolidated three services — email, job recruitment, and a business yellow pages —\n          with full support for all four languages.\n        </p>\n\n        <h2>The challenge</h2>\n\n        <p>\n          Building multilingual web applications in 2008 was a different world. Unicode support\n          was inconsistent. Ethiopic script (Ge'ez) rendering varied wildly across browsers.\n          Right-to-left considerations didn't apply (Ethiopic is left-to-right), but character\n          encoding, font availability, and database storage for non-Latin scripts were constant\n          obstacles.\n        </p>\n\n        <p>\n          The bigger design challenge was making the interface genuinely usable in each language —\n          not just translated labels, but culturally appropriate navigation patterns, date\n          formatting, and search that worked across scripts.\n        </p>\n\n        <h2>What it did</h2>\n\n        <ul>\n          <li><strong>Mailing</strong> — webmail with text and file attachment support, fully localized UI in all 4 languages</li>\n          <li><strong>Recruitment</strong> — vacancy announcements that employers could post and job seekers could browse, search, and filter in their preferred language</li>\n          <li><strong>Yellow pages</strong> — a business directory for Addis Ababa service providers, searchable 24/7 in any of the supported languages</li>\n        </ul>\n\n        <h2>The tech</h2>\n\n        <p>\n          The stack was very much of its era: Visual Basic 2005, SQL Server, MySQL, Apache/IIS,\n          with Rational Rose for UML modeling and Macromedia Dreamweaver for front-end work.\n          Object-oriented methodology throughout.\n        </p>\n\n        <p>\n          Looking at this from 2026, the technology choices feel dated — but the core problem\n          hasn't gone away. Localization and accessibility for non-English-speaking populations\n          remains one of the most underserved areas in software engineering. The project was\n          an early attempt at something I'd spend much of my career working on: making\n          software genuinely useful for people who aren't the default user persona.\n        </p>\n\n        <p>\n          The full publication is available on\n          <a href=\"https://zenodo.org/records/11965718\" target=\"_blank\" rel=\"noopener noreferrer\">Zenodo</a>.\n        </p>",
    "body": "In 2008, web services in Ethiopia were almost exclusively English-only. Email providers had partial localization at best. Job boards and business directories were fragmented and inaccessible to anyone who didn't read English fluently. For a country with dozens of languages and three widely spoken ones beyond English — Amharic, Oromifa, and Tigrinya — this was a real barrier. This project was my B.Sc. thesis at HiLCoE: a unified web portal called addisportal.com that consolidated three services — email, job recruitment, and a business yellow pages — with full support for all four languages. The challenge Building multilingual web applications in 2008 was a different world. Unicode support was inconsistent. Ethiopic script (Ge'ez) rendering varied wildly across browsers. Right-to-left considerations didn't apply (Ethiopic is left-to-right), but character encoding, font availability, and database storage for non-Latin scripts were constant obstacles. The bigger design challenge was making the interface genuinely usable in each language — not just translated labels, but culturally appropriate navigation patterns, date formatting, and search that worked across scripts. What it did Mailing — webmail with text and file attachment support, fully localized UI in all 4 languages Recruitment — vacancy announcements that employers could post and job seekers could browse, search, and filter in their preferred language Yellow pages — a business directory for Addis Ababa service providers, searchable 24/7 in any of the supported languages The tech The stack was very much of its era: Visual Basic 2005, SQL Server, MySQL, Apache/IIS, with Rational Rose for UML modeling and Macromedia Dreamweaver for front-end work. Object-oriented methodology throughout. Looking at this from 2026, the technology choices feel dated — but the core problem hasn't gone away. Localization and accessibility for non-English-speaking populations remains one of the most underserved areas in software engineering. The project was an early attempt at something I'd spend much of my career working on: making software genuinely useful for people who aren't the default user persona. The full publication is available on Zenodo .",
    "legacyPath": "posts/localized-portal-four-languages.html",
    "published": true
  },
  {
    "id": "my-honest-take-on-vibe-coding",
    "title": "My Honest Take on \"Vibe Coding\"",
    "slug": "my-honest-take-on-vibe-coding",
    "date": "2026-04-01",
    "readTime": "4 min read",
    "excerpt": "I’ll start with this - I’m not against AI in development. Quite the opposite.",
    "bodyHtml": "<p>I’ll start with this - I’m not against AI in development. Quite the opposite.</p>\n        \n        <p>I use AI every day. In some of my projects, a large portion of the code is generated with AI assistance. It has dramatically improved my productivity. Tasks that used to take hours can now be done in minutes. Repetitive work, boilerplate, and low-level implementation details being handled by machines is a huge win. This is real progress, and I fully embrace it.</p>\n        \n        <p>But here’s where I draw the line.</p>\n        \n        <p><strong>Coding is only one part of software engineering.</strong></p>\n        \n        <p>Software engineering is a combination of many things: understanding the problem space, designing systems, making trade-offs, thinking about scalability, handling edge cases, debugging complex issues, and maintaining systems over time. It requires context, judgment, and responsibility.</p>\n        \n        <p>When we reduce all of that to “vibe coding,” we risk oversimplifying what it actually takes to build reliable, production-grade systems.</p>\n        \n        <p>The term itself isn’t the problem. It made sense in the original context - quick experiments, prototypes, or weekend projects where speed matters more than perfection.</p>\n        \n        <p>But the concern is when that mindset is applied to everything.</p>\n        \n        <p>If “vibe coding” becomes the default approach to serious engineering work, we start losing something important - craftsmanship. We risk trusting outputs without understanding them. We risk building systems that work today but are fragile tomorrow. And over time, we risk weakening the very skills that allow us to reason, design, and solve hard problems.</p>\n        \n        <p><strong>AI should amplify engineers, not replace engineering thinking.</strong></p>\n        \n        <p>For me, the ideal balance is clear:</p>\n        \n        <ul>\n          <li>Use AI aggressively to move faster</li>\n          <li>Automate the boring and repetitive parts</li>\n          <li>But stay deeply involved in the thinking, design, and ownership</li>\n        </ul>\n        \n        <p>That’s where the real value is.</p>\n        \n        <p>Maybe I’m a bit old school, but I still believe great software comes from thoughtful engineering, not just good vibes.</p>\n        \n        <p>Curious how others are navigating this balance.</p>",
    "body": "I’ll start with this - I’m not against AI in development. Quite the opposite. I use AI every day. In some of my projects, a large portion of the code is generated with AI assistance. It has dramatically improved my productivity. Tasks that used to take hours can now be done in minutes. Repetitive work, boilerplate, and low-level implementation details being handled by machines is a huge win. This is real progress, and I fully embrace it. But here’s where I draw the line. Coding is only one part of software engineering. Software engineering is a combination of many things: understanding the problem space, designing systems, making trade-offs, thinking about scalability, handling edge cases, debugging complex issues, and maintaining systems over time. It requires context, judgment, and responsibility. When we reduce all of that to “vibe coding,” we risk oversimplifying what it actually takes to build reliable, production-grade systems. The term itself isn’t the problem. It made sense in the original context - quick experiments, prototypes, or weekend projects where speed matters more than perfection. But the concern is when that mindset is applied to everything. If “vibe coding” becomes the default approach to serious engineering work, we start losing something important - craftsmanship. We risk trusting outputs without understanding them. We risk building systems that work today but are fragile tomorrow. And over time, we risk weakening the very skills that allow us to reason, design, and solve hard problems. AI should amplify engineers, not replace engineering thinking. For me, the ideal balance is clear: Use AI aggressively to move faster Automate the boring and repetitive parts But stay deeply involved in the thinking, design, and ownership That’s where the real value is. Maybe I’m a bit old school, but I still believe great software comes from thoughtful engineering, not just good vibes. Curious how others are navigating this balance.",
    "legacyPath": "posts/my-honest-take-on-vibe-coding.html",
    "published": true
  }
]