<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Williams Ashibuogwu</title>
    <description>The latest articles on DEV Community by Williams Ashibuogwu (@wbizmo).</description>
    <link>https://dev.to/wbizmo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4110384%2Fa0d98bc6-40a2-4e24-b4e6-9cb5d1000f79.png</url>
      <title>DEV Community: Williams Ashibuogwu</title>
      <link>https://dev.to/wbizmo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wbizmo"/>
    <language>en</language>
    <item>
      <title>I’m Almost Done Building the Biggest Financial Platform I’ve Worked On</title>
      <dc:creator>Williams Ashibuogwu</dc:creator>
      <pubDate>Sat, 05 Sep 2026 06:10:57 +0000</pubDate>
      <link>https://dev.to/wbizmo/im-almost-done-building-the-biggest-financial-platform-ive-worked-on-47ko</link>
      <guid>https://dev.to/wbizmo/im-almost-done-building-the-biggest-financial-platform-ive-worked-on-47ko</guid>
      <description>&lt;p&gt;I kept telling myself I would write about this project when I finished it.&lt;/p&gt;

&lt;p&gt;I'm still not completely finished.&lt;/p&gt;

&lt;p&gt;There are a few user journeys I'm checking again, some UI things I keep noticing at annoying screen sizes, a performance problem I deliberately don't want to "fix" before measuring it properly, and a few pieces of technical debt I already know I'll be coming back for.&lt;/p&gt;

&lt;p&gt;But I'm close.&lt;/p&gt;

&lt;p&gt;And I think I'd rather write this now, while I'm still inside the build, than six months from now when my brain has conveniently turned the whole experience into a clean sequence of sensible engineering decisions.&lt;/p&gt;

&lt;p&gt;Because it wasn't that clean.&lt;/p&gt;

&lt;p&gt;I've been shipping almost every day on a financial platform through a pretty intense stretch of development.&lt;/p&gt;

&lt;p&gt;I can't name the company, the product, or the financial infrastructure behind it, and there are obviously security and production details that don't belong in a public article.&lt;/p&gt;

&lt;p&gt;But I can talk about what it has actually been like to build.&lt;/p&gt;

&lt;p&gt;At one point, a repository snapshot looked roughly like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;700+ commits&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1,000+ tracked files&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;200,000+ tracked lines&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 applications&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;13 shared packages&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;500+ tests&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't think I ever had a day where it felt like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Today I am contributing to a 200,000-line system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It felt more like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why is this route sending an authenticated user back to login?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This notification should open the thing it is notifying them about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why can this worker be delayed by marketing jobs?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This transfer timed out. Did the money actually move?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This looks terrible on a 390px screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enough of those days happen and suddenly you have a platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the project stopped being "an app"
&lt;/h2&gt;

&lt;p&gt;The repository eventually settled into a TypeScript monorepo with three main applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/
  api/
  web/
  worker/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and shared packages handling things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
  cache/
  config/
  database/
  documents/
  ledger/
  providers/
  public-api/
  queues/
  safety/
  security/
  shared/
  storage/
  tooling/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend is React and TypeScript.&lt;/p&gt;

&lt;p&gt;The HTTP layer is Fastify.&lt;/p&gt;

&lt;p&gt;PostgreSQL is the primary database.&lt;/p&gt;

&lt;p&gt;PgBouncer handles connection pooling.&lt;/p&gt;

&lt;p&gt;Valkey sits behind caching and short-lived coordination.&lt;/p&gt;

&lt;p&gt;BullMQ runs asynchronous workloads.&lt;/p&gt;

&lt;p&gt;Around that are object storage, Caddy, systemd, structured logging, health checks, deployment tooling, API schemas, background workers, and the other things that become necessary once "run the server" stops being a deployment strategy.&lt;/p&gt;

&lt;p&gt;None of those technology choices are particularly exotic.&lt;/p&gt;

&lt;p&gt;The interesting part was what the product gradually demanded from them.&lt;/p&gt;

&lt;p&gt;What started as one financial application eventually included customer onboarding, account approval, KYC, financial accounts, transfers, FX, cards, bill payments, statements, notifications, partner workflows, internal operations, support tooling, developer-facing APIs, public documentation, a PWA, and some separately privileged product areas.&lt;/p&gt;

&lt;p&gt;Different users.&lt;/p&gt;

&lt;p&gt;Different access boundaries.&lt;/p&gt;

&lt;p&gt;Different kinds of state.&lt;/p&gt;

&lt;p&gt;Different kinds of money movement.&lt;/p&gt;

&lt;p&gt;Different failure modes.&lt;/p&gt;

&lt;p&gt;One system.&lt;/p&gt;

&lt;p&gt;At the beginning, I was still mostly thinking in features.&lt;/p&gt;

&lt;p&gt;Build onboarding.&lt;/p&gt;

&lt;p&gt;Build the dashboard.&lt;/p&gt;

&lt;p&gt;Add transfers.&lt;/p&gt;

&lt;p&gt;Add notifications.&lt;/p&gt;

&lt;p&gt;Wire up the provider.&lt;/p&gt;

&lt;p&gt;Make the mobile version work.&lt;/p&gt;

&lt;p&gt;The problem is that every one of those features brought more than a screen with it.&lt;/p&gt;

&lt;p&gt;Transfers brought retries, timeouts, provider state, reconciliation, authorization, limits, notifications, and audit.&lt;/p&gt;

&lt;p&gt;Notifications brought preferences, retries, delivery channels, deep links, and several applications generating different kinds of events.&lt;/p&gt;

&lt;p&gt;Partner features brought attribution, permissions, workflow state, and the problem of showing enough information to make a relationship useful without leaking private customer data.&lt;/p&gt;

&lt;p&gt;Eventually the implementation had grown enough that I stopped trusting the shape of the original application.&lt;/p&gt;

&lt;p&gt;Not because every part was bad.&lt;/p&gt;

&lt;p&gt;That actually made the decision harder.&lt;/p&gt;

&lt;p&gt;There is a point in a project where almost every problem still looks individually fixable.&lt;/p&gt;

&lt;p&gt;A route is broken.&lt;/p&gt;

&lt;p&gt;Fix it.&lt;/p&gt;

&lt;p&gt;An authorization check is awkward.&lt;/p&gt;

&lt;p&gt;Fix it.&lt;/p&gt;

&lt;p&gt;One provider call is living in the wrong place.&lt;/p&gt;

&lt;p&gt;Move it.&lt;/p&gt;

&lt;p&gt;A page breaks on mobile.&lt;/p&gt;

&lt;p&gt;Patch the CSS.&lt;/p&gt;

&lt;p&gt;A workflow has a weird state.&lt;/p&gt;

&lt;p&gt;Add another condition.&lt;/p&gt;

&lt;p&gt;Every individual fix can be reasonable while the overall shape of the application gets worse.&lt;/p&gt;

&lt;p&gt;Eventually I reached the point where I didn't trust enough of the assumptions underneath the implementation.&lt;/p&gt;

&lt;p&gt;So I froze that development state.&lt;/p&gt;

&lt;p&gt;I kept the infrastructure, but started the application layer again on a clean development environment.&lt;/p&gt;

&lt;p&gt;Not because rewrites are fun.&lt;/p&gt;

&lt;p&gt;They're not.&lt;/p&gt;

&lt;p&gt;It was more:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I don't want to keep making stronger promises on top of a foundation I no longer fully trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The original implementation plan had 24 engineering sprints.&lt;/p&gt;

&lt;p&gt;That eventually became 30.&lt;/p&gt;

&lt;p&gt;Not because I suddenly wanted six more sprints, but because areas like partner operations, developer APIs, documentation, PWA behaviour, and final hardening turned out to be much larger domains when I tried to make them &lt;strong&gt;complete&lt;/strong&gt; instead of merely present.&lt;/p&gt;

&lt;p&gt;That gave me one of the first lessons from the project that really stuck:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sometimes scope grows because people keep adding things. Sometimes scope grows because you finally understand what the original sentence actually implied.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are very different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding the foundation changed what I meant by "reliable"
&lt;/h2&gt;

&lt;p&gt;One of the first clean-build deployments failed.&lt;/p&gt;

&lt;p&gt;I like that story now.&lt;/p&gt;

&lt;p&gt;I did not like it when it happened.&lt;/p&gt;

&lt;p&gt;The useful part wasn't that the deployment failed.&lt;/p&gt;

&lt;p&gt;The useful part was that the release machinery did what it was supposed to do.&lt;/p&gt;

&lt;p&gt;The new state wasn't copied over the running application and left there half-working. The release stopped and the previous known-good state came back.&lt;/p&gt;

&lt;p&gt;The real deployment tooling is more involved, but conceptually the release flow became something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;create_release&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

build_into &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
run_checks &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

switch_atomically &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; healthcheck&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;rollback
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had already written "rollback" into the architecture.&lt;/p&gt;

&lt;p&gt;That failure was the moment I knew rollback actually existed.&lt;/p&gt;

&lt;p&gt;There is a difference.&lt;/p&gt;

&lt;p&gt;I also learned a smaller operational detail by watching enough deployments: the first health probe after switching a release doesn't always return &lt;code&gt;200&lt;/code&gt; immediately.&lt;/p&gt;

&lt;p&gt;Sometimes the process needs another attempt or two while it finishes warming up.&lt;/p&gt;

&lt;p&gt;The first few times that happened, I treated it like something was broken.&lt;/p&gt;

&lt;p&gt;Eventually the deployment tooling learned what normal startup behaviour actually looked like.&lt;/p&gt;

&lt;p&gt;That's one of those boring things you don't really learn from drawing an architecture diagram.&lt;/p&gt;

&lt;p&gt;Architecture tells you how you think the system behaves.&lt;/p&gt;

&lt;p&gt;Operations tells you how it actually behaves.&lt;/p&gt;

&lt;p&gt;That experience also made me stricter about apparently small changes.&lt;/p&gt;

&lt;p&gt;At one point CI went red because Prettier wanted a multi-line import collapsed back onto one line.&lt;/p&gt;

&lt;p&gt;That was basically the entire behavioural impact of the commit.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;AppSessionLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PushPermissionDecision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;RequestCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./MemberPortal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppSessionLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PushPermissionDecision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RequestCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;icon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./MemberPortal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No business behaviour changed.&lt;/p&gt;

&lt;p&gt;The gate was still red.&lt;/p&gt;

&lt;p&gt;So the formatting change got synced, built, deployed, and verified like everything else.&lt;/p&gt;

&lt;p&gt;At the time I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is ridiculous.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I thought about the alternative.&lt;/p&gt;

&lt;p&gt;You start deciding that some changes are too small to deserve the normal release process.&lt;/p&gt;

&lt;p&gt;Then somebody has to decide what "small" means.&lt;/p&gt;

&lt;p&gt;Then exceptions start accumulating.&lt;/p&gt;

&lt;p&gt;So yes.&lt;/p&gt;

&lt;p&gt;The one-line import got a proper deployment.&lt;/p&gt;

&lt;p&gt;The Git setup behind all of this is less elegant than I'd like.&lt;/p&gt;

&lt;p&gt;I deliberately separated the environment where commits are authored from the checkout that handles deployment and remote repository authentication.&lt;/p&gt;

&lt;p&gt;That buys credential isolation.&lt;/p&gt;

&lt;p&gt;It also means synchronization occasionally takes more thought than a normal one-checkout workflow should. I've had moments where the deployment checkout already contained an uncommitted copy of the exact change I was trying to pull as a commit, forcing me to verify the diffs matched before pulling cleanly.&lt;/p&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;I know why it exists.&lt;/p&gt;

&lt;p&gt;I still want to simplify it.&lt;/p&gt;

&lt;p&gt;That's debt. Not hidden debt. Just something useful enough to tolerate for now.&lt;/p&gt;

&lt;p&gt;The bigger shift was that I started thinking about reliability less as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this code work?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and more as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does the system do when this code doesn't get the outcome it expected?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question became much more serious once money movement entered the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Money movement forced me to stop thinking in success and failure
&lt;/h2&gt;

&lt;p&gt;From the UI, a transfer looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recipient
amount
confirm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend version is much less pleasant.&lt;/p&gt;

&lt;p&gt;Suppose I submit a transfer to an external financial provider.&lt;/p&gt;

&lt;p&gt;Then my connection disappears.&lt;/p&gt;

&lt;p&gt;Did it fail?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;Did the provider receive the request?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;Did they execute it successfully and the response disappear somewhere between their system and mine?&lt;/p&gt;

&lt;p&gt;Also maybe.&lt;/p&gt;

&lt;p&gt;The one thing I definitely cannot do is immediately submit the transfer again.&lt;/p&gt;

&lt;p&gt;A naïve version of the flow wants to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Transfer failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That code wants the world to have two states.&lt;/p&gt;

&lt;p&gt;Success.&lt;/p&gt;

&lt;p&gt;Failure.&lt;/p&gt;

&lt;p&gt;Financial systems occasionally respond:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;lol, no.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The application eventually needed stable operation IDs, idempotency keys, request hashes, provider references, and recoverable operation states.&lt;/p&gt;

&lt;p&gt;The mental model became much closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OperationState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submitting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reversed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reconciling&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state I care about most there is &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Earlier in my career, I think I would have treated &lt;code&gt;unknown&lt;/code&gt; as a failure to finish implementing the workflow.&lt;/p&gt;

&lt;p&gt;Now I think it can be one of the most honest states in a distributed financial system.&lt;/p&gt;

&lt;p&gt;If I genuinely do not know whether money moved yet, the application should not invent an answer because red or green is easier to render.&lt;/p&gt;

&lt;p&gt;That same problem changed how I thought about webhooks.&lt;/p&gt;

&lt;p&gt;A webhook implementation initially looks very simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you spend five minutes thinking like someone trying to break it.&lt;/p&gt;

&lt;p&gt;Who sent that body?&lt;/p&gt;

&lt;p&gt;Has it been modified?&lt;/p&gt;

&lt;p&gt;How old is it?&lt;/p&gt;

&lt;p&gt;Have I seen this event before?&lt;/p&gt;

&lt;p&gt;Is it from the right environment?&lt;/p&gt;

&lt;p&gt;What if the provider sends it three times?&lt;/p&gt;

&lt;p&gt;What if they retry because my server took too long to answer?&lt;/p&gt;

&lt;p&gt;What if the webhook says a transaction completed but the amount or reference does not match what I expected?&lt;/p&gt;

&lt;p&gt;Eventually the flow looked much more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receive
  ↓
validate request shape
  ↓
verify signature + timestamp
  ↓
reject replay
  ↓
persist event
  ↓
acknowledge quickly
  ↓
process asynchronously
  ↓
verify against provider when necessary
  ↓
apply financial effect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And reconciliation stopped pretending that every incoming event could immediately become success or failure.&lt;/p&gt;

&lt;p&gt;A shortened, anonymised version of a pattern in the codebase looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ReconcileOutcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;postingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skipped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;needs_review&lt;/code&gt; state matters.&lt;/p&gt;

&lt;p&gt;For example, if I receive one reference and the authoritative lookup comes back with something else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verifiedReference&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;needs_review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Provider verification reference mismatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't guess.&lt;/p&gt;

&lt;p&gt;Don't force it through because it probably belongs to that transaction.&lt;/p&gt;

&lt;p&gt;Don't silently throw it away either.&lt;/p&gt;

&lt;p&gt;Put it somewhere the system can reason about it safely.&lt;/p&gt;

&lt;p&gt;The more I worked on this, the more I realised that a financial platform needs to be comfortable saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I have received information.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;without automatically saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Therefore this is now financial truth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are different statements.&lt;/p&gt;

&lt;p&gt;And that distinction led directly to another architectural problem.&lt;/p&gt;

&lt;p&gt;The more financial operations I added, the easier it would have been to let the external provider define the whole application.&lt;/p&gt;

&lt;p&gt;Need an account?&lt;/p&gt;

&lt;p&gt;Call their account endpoint.&lt;/p&gt;

&lt;p&gt;Need a transfer?&lt;/p&gt;

&lt;p&gt;Call their transfer endpoint.&lt;/p&gt;

&lt;p&gt;Need FX?&lt;/p&gt;

&lt;p&gt;Call that endpoint.&lt;/p&gt;

&lt;p&gt;Need a card?&lt;/p&gt;

&lt;p&gt;Same thing.&lt;/p&gt;

&lt;p&gt;That is extremely productive at the beginning.&lt;/p&gt;

&lt;p&gt;Then one day your controllers know provider-specific field names, your workers know provider-specific status strings, your frontend knows which provider can do what, and half your business logic is written in the vocabulary of a company you don't control.&lt;/p&gt;

&lt;p&gt;I didn't want that.&lt;/p&gt;

&lt;p&gt;So financial integrations ended up behind a provider boundary.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application/domain logic
          ↓
provider capability + routing layer
          ↓
provider-specific adapter
          ↓
external infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An early implementation could easily have ended up with logic like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showTransferButton&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except "does the provider support USD?" is not actually a useful question.&lt;/p&gt;

&lt;p&gt;USD for what?&lt;/p&gt;

&lt;p&gt;Account issuance?&lt;/p&gt;

&lt;p&gt;Holding a balance?&lt;/p&gt;

&lt;p&gt;Inbound transfer?&lt;/p&gt;

&lt;p&gt;Outbound transfer?&lt;/p&gt;

&lt;p&gt;FX?&lt;/p&gt;

&lt;p&gt;Cards?&lt;/p&gt;

&lt;p&gt;Bills?&lt;/p&gt;

&lt;p&gt;A provider can support one and not another.&lt;/p&gt;

&lt;p&gt;Or expose something technically while that capability is not enabled for your account.&lt;/p&gt;

&lt;p&gt;So support had to become contextual.&lt;/p&gt;

&lt;p&gt;The provider layer can ask something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;banking_rail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customerType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the underlying logic checking capability, geography, currency, customer type, and whether that declaration is enabled.&lt;/p&gt;

&lt;p&gt;Something roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capability&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countries&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ALL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currencies&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ALL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currencies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;declaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is another detail I particularly like.&lt;/p&gt;

&lt;p&gt;Once an existing financial relationship has been created with a particular provider, changing the routing preference for new relationships should not silently move that existing one somewhere else.&lt;/p&gt;

&lt;p&gt;So existing relationships can be pinned.&lt;/p&gt;

&lt;p&gt;New ones can follow the current routing rules.&lt;/p&gt;

&lt;p&gt;Old ones continue using the provider they were actually created with.&lt;/p&gt;

&lt;p&gt;That sounds obvious now.&lt;/p&gt;

&lt;p&gt;It wasn't something I was thinking about before I had to design a system that could survive changing providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once correctness mattered, infrastructure became part of product behaviour
&lt;/h2&gt;

&lt;p&gt;The financial workflows created more asynchronous work.&lt;/p&gt;

&lt;p&gt;Provider events.&lt;/p&gt;

&lt;p&gt;Reconciliation.&lt;/p&gt;

&lt;p&gt;Security messages.&lt;/p&gt;

&lt;p&gt;Notifications.&lt;/p&gt;

&lt;p&gt;Documents.&lt;/p&gt;

&lt;p&gt;Exports.&lt;/p&gt;

&lt;p&gt;Analytics.&lt;/p&gt;

&lt;p&gt;At first, the normal queue question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this need to happen outside the HTTP request?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Useful question.&lt;/p&gt;

&lt;p&gt;Not enough.&lt;/p&gt;

&lt;p&gt;The more important question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is this job allowed to wait behind?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Imagine thousands of marketing jobs enter the queue.&lt;/p&gt;

&lt;p&gt;At the same time, somebody is trying to log in and needs a security email.&lt;/p&gt;

&lt;p&gt;Should that security message sit behind the marketing campaign?&lt;/p&gt;

&lt;p&gt;Obviously not.&lt;/p&gt;

&lt;p&gt;But if they are all just "background jobs," you may have accidentally made exactly that decision.&lt;/p&gt;

&lt;p&gt;So the queue layer stopped treating every job as equal.&lt;/p&gt;

&lt;p&gt;A simplified version looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queueClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;security-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;provider-webhooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;financial-reconciliation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="na"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactional-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;push-notifications&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;documents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="na"&gt;deferrable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;analytics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marketing-bulk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maintenance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They don't all get the same retry behaviour either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;deferrable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specific numbers are not the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part is that queue design became a product decision.&lt;/p&gt;

&lt;p&gt;When the system is under pressure, &lt;strong&gt;what are we willing to make wait?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An analytics refresh?&lt;/p&gt;

&lt;p&gt;Sure.&lt;/p&gt;

&lt;p&gt;A login security message?&lt;/p&gt;

&lt;p&gt;Much less so.&lt;/p&gt;

&lt;p&gt;A financial reconciliation job?&lt;/p&gt;

&lt;p&gt;Definitely not because somebody requested a large export.&lt;/p&gt;

&lt;p&gt;That's how I think about queues now.&lt;/p&gt;

&lt;p&gt;They aren't just a place to put work you don't want inside the request.&lt;/p&gt;

&lt;p&gt;They encode priority.&lt;/p&gt;

&lt;p&gt;They encode which delays the product considers acceptable.&lt;/p&gt;

&lt;p&gt;The database went through a similar change in my head.&lt;/p&gt;

&lt;p&gt;PostgreSQL stopped being just "where the data goes."&lt;/p&gt;

&lt;p&gt;There are some bugs I don't want application-level &lt;code&gt;if&lt;/code&gt; statements to be solely responsible for preventing.&lt;/p&gt;

&lt;p&gt;Two requests trying to spend the same available amount.&lt;/p&gt;

&lt;p&gt;Two workers processing the same event.&lt;/p&gt;

&lt;p&gt;Two people trying to consume the last available unit of something.&lt;/p&gt;

&lt;p&gt;An update based on stale state.&lt;/p&gt;

&lt;p&gt;Several records that need to change together or not change at all.&lt;/p&gt;

&lt;p&gt;So the database became part of the correctness model.&lt;/p&gt;

&lt;p&gt;That meant actually using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unique constraints;&lt;/li&gt;
&lt;li&gt;foreign keys;&lt;/li&gt;
&lt;li&gt;check constraints;&lt;/li&gt;
&lt;li&gt;optimistic version columns;&lt;/li&gt;
&lt;li&gt;atomic conditional updates;&lt;/li&gt;
&lt;li&gt;row locks;&lt;/li&gt;
&lt;li&gt;short transactions;&lt;/li&gt;
&lt;li&gt;deterministic lock ordering;&lt;/li&gt;
&lt;li&gt;and stronger isolation where a specific invariant justified it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I knew what &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; did before this project.&lt;/p&gt;

&lt;p&gt;The project taught me something more useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;when I actually want it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same with serializable transactions.&lt;/p&gt;

&lt;p&gt;I knew they existed.&lt;/p&gt;

&lt;p&gt;Now I understand much better why I don't want everything running at serializable isolation, and why a few particularly sensitive invariants might actually justify paying that cost.&lt;/p&gt;

&lt;p&gt;Knowing the feature and knowing where it belongs are different levels of understanding.&lt;/p&gt;

&lt;p&gt;That pattern kept repeating.&lt;/p&gt;

&lt;p&gt;The system would grow.&lt;/p&gt;

&lt;p&gt;A problem I previously understood academically would become a real operational constraint.&lt;/p&gt;

&lt;p&gt;Then I would understand why the boring engineering primitive exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visible feature was almost never the whole feature
&lt;/h2&gt;

&lt;p&gt;Authentication started small too.&lt;/p&gt;

&lt;p&gt;At one point, "authentication" basically meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login
logout
forgot password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the actual requirements arrived.&lt;/p&gt;

&lt;p&gt;OTP.&lt;/p&gt;

&lt;p&gt;Passkeys.&lt;/p&gt;

&lt;p&gt;Device and session management.&lt;/p&gt;

&lt;p&gt;Session rotation.&lt;/p&gt;

&lt;p&gt;Step-up authentication.&lt;/p&gt;

&lt;p&gt;Recovery.&lt;/p&gt;

&lt;p&gt;Staff authentication.&lt;/p&gt;

&lt;p&gt;Privileged staff authentication.&lt;/p&gt;

&lt;p&gt;Trusted devices.&lt;/p&gt;

&lt;p&gt;Revocation.&lt;/p&gt;

&lt;p&gt;Different session policies.&lt;/p&gt;

&lt;p&gt;Authorization became even more important because the system eventually had several application surfaces.&lt;/p&gt;

&lt;p&gt;A customer.&lt;/p&gt;

&lt;p&gt;A partner.&lt;/p&gt;

&lt;p&gt;A developer.&lt;/p&gt;

&lt;p&gt;Normal staff.&lt;/p&gt;

&lt;p&gt;Privileged staff.&lt;/p&gt;

&lt;p&gt;Someone can be allowed to see a record without being allowed to modify it.&lt;/p&gt;

&lt;p&gt;A partner may need to know that a referral progressed without being allowed anywhere near that person's banking details.&lt;/p&gt;

&lt;p&gt;A customer interface may correctly hide an operation while an API route underneath it accidentally still allows the request.&lt;/p&gt;

&lt;p&gt;So I became increasingly annoying about one rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hidden or disabled button is not authorization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If someone is not allowed to perform an action, the server performing that action has to reject it.&lt;/p&gt;

&lt;p&gt;Not the menu.&lt;/p&gt;

&lt;p&gt;Not the React component.&lt;/p&gt;

&lt;p&gt;Not the disabled state.&lt;/p&gt;

&lt;p&gt;The actual boundary that changes or reveals the protected resource.&lt;/p&gt;

&lt;p&gt;Frontend visibility is UX.&lt;/p&gt;

&lt;p&gt;Authorization is policy.&lt;/p&gt;

&lt;p&gt;Notifications followed the same pattern.&lt;/p&gt;

&lt;p&gt;What began as "we need notifications" became in-app events, email, push, preferences, retries, deduplication, deep links, and failure handling across several application surfaces.&lt;/p&gt;

&lt;p&gt;The useful model was to treat the internal notification as the durable record and email or push as delivery attempts around it.&lt;/p&gt;

&lt;p&gt;A failed delivery does not undo the event that caused it, and it definitely does not change financial truth.&lt;/p&gt;

&lt;p&gt;And while all of this was happening on the backend, the frontend was teaching me a different version of the same lesson:&lt;/p&gt;

&lt;p&gt;software can be technically correct and still be bad.&lt;/p&gt;

&lt;p&gt;The mobile topbar, for example, had several lives.&lt;/p&gt;

&lt;p&gt;It went from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;full wordmark
      ↓
personalised greeting
      ↓
small icon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The greeting version wasn't a mock-up.&lt;/p&gt;

&lt;p&gt;It actually loaded the member's name.&lt;/p&gt;

&lt;p&gt;It had responsive typography.&lt;/p&gt;

&lt;p&gt;It was built and styled.&lt;/p&gt;

&lt;p&gt;Then I looked at it again and the smaller icon was cleaner.&lt;/p&gt;

&lt;p&gt;So the greeting disappeared.&lt;/p&gt;

&lt;p&gt;I used to think of that kind of work as wasted effort.&lt;/p&gt;

&lt;p&gt;I don't anymore.&lt;/p&gt;

&lt;p&gt;Sometimes the fastest way to know whether a visual idea works is to build the thing and look at it.&lt;/p&gt;

&lt;p&gt;One of my favourite frontend bugs had nothing to do with code correctness.&lt;/p&gt;

&lt;p&gt;A dashboard section was redesigned.&lt;/p&gt;

&lt;p&gt;The code compiled.&lt;/p&gt;

&lt;p&gt;Types passed.&lt;/p&gt;

&lt;p&gt;Routes worked.&lt;/p&gt;

&lt;p&gt;The layout worked.&lt;/p&gt;

&lt;p&gt;Then I looked at the screen and realised that the same phrase appeared three times in the same panel.&lt;/p&gt;

&lt;p&gt;Once as the eyebrow.&lt;/p&gt;

&lt;p&gt;Once as the heading.&lt;/p&gt;

&lt;p&gt;Once again on the button.&lt;/p&gt;

&lt;p&gt;Perfectly valid software.&lt;/p&gt;

&lt;p&gt;Terrible interface.&lt;/p&gt;

&lt;p&gt;No type system is going to catch that.&lt;/p&gt;

&lt;p&gt;No unit test is going to tell you your copy hierarchy looks stupid.&lt;/p&gt;

&lt;p&gt;Sometimes the correct QA process is literally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why does this look stupid?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mobile made this even more obvious.&lt;/p&gt;

&lt;p&gt;There were pages that were technically responsive.&lt;/p&gt;

&lt;p&gt;Nothing overflowed.&lt;/p&gt;

&lt;p&gt;Every element was visible.&lt;/p&gt;

&lt;p&gt;They were still bad mobile interfaces.&lt;/p&gt;

&lt;p&gt;Authentication was the clearest example.&lt;/p&gt;

&lt;p&gt;On desktop, more explanatory text and a wider composition can look good.&lt;/p&gt;

&lt;p&gt;On a phone, especially inside an installed PWA, the same thing can feel like a desktop website somebody squeezed into a smaller rectangle.&lt;/p&gt;

&lt;p&gt;So I started removing things.&lt;/p&gt;

&lt;p&gt;Less surrounding chrome.&lt;/p&gt;

&lt;p&gt;Less unnecessary copy.&lt;/p&gt;

&lt;p&gt;Better viewport spacing.&lt;/p&gt;

&lt;p&gt;Forms that actually sit comfortably between the screen edges.&lt;/p&gt;

&lt;p&gt;Cleaner hierarchy.&lt;/p&gt;

&lt;p&gt;OTP fields that look like OTP fields instead of a normal text input.&lt;/p&gt;

&lt;p&gt;Navigation designed for thumbs instead of a desktop sidebar awkwardly collapsing.&lt;/p&gt;

&lt;p&gt;That changed the question I ask.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this fit on mobile?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If mobile were the only platform, would I have designed it this way?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, then I probably haven't actually designed the mobile version yet.&lt;/p&gt;

&lt;p&gt;The same idea connects all of these problems.&lt;/p&gt;

&lt;p&gt;A disabled button doesn't mean an action is forbidden.&lt;/p&gt;

&lt;p&gt;A page fitting inside the viewport doesn't mean it is a good mobile experience.&lt;/p&gt;

&lt;p&gt;A transfer button working doesn't mean the transfer journey is reliable.&lt;/p&gt;

&lt;p&gt;The thing I could see was often only the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Done" became a much harsher word
&lt;/h2&gt;

&lt;p&gt;Near the end of the build, I started discovering a lot of things that existed but weren't actually complete.&lt;/p&gt;

&lt;p&gt;A notification bell could exist without every product surface producing the right events.&lt;/p&gt;

&lt;p&gt;A developer portal could look finished while credential rotation, scopes, idempotency, or webhook management were still incomplete.&lt;/p&gt;

&lt;p&gt;An admin page could render perfectly while the underlying action lacked the right authorization, audit trail, or failure behaviour.&lt;/p&gt;

&lt;p&gt;That was when my working definition of a complete feature became much harsher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data model
backend action
authorization
validation
state transitions
audit
notifications
failure states
empty states
responsive behaviour
tests
documentation
operational controls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That definition is painful when somebody says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;But the screen is already there. Aren't we almost done?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes.&lt;/p&gt;

&lt;p&gt;Sometimes the screen means you're 60% done.&lt;/p&gt;

&lt;p&gt;Testing changed for me too.&lt;/p&gt;

&lt;p&gt;The project has more than 500 tests, and substantial behavioural changes still get the appropriate suites.&lt;/p&gt;

&lt;p&gt;But I stopped treating "run everything after everything" as synonymous with discipline.&lt;/p&gt;

&lt;p&gt;For a presentation-only change, typecheck, build, and actual visual inspection may tell me more than hundreds of unrelated domain tests.&lt;/p&gt;

&lt;p&gt;My rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pay for the verification that can realistically catch the class of mistake you just introduced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I changed a ledger invariant, visual QA isn't going to save me.&lt;/p&gt;

&lt;p&gt;If I changed a heading from &lt;code&gt;18px&lt;/code&gt; to &lt;code&gt;20px&lt;/code&gt;, hundreds of domain tests probably aren't going to save me either.&lt;/p&gt;

&lt;p&gt;And despite all of this, the project isn't magically debt-free because I'm near the end.&lt;/p&gt;

&lt;p&gt;The main frontend bundle is still larger than I want it to be.&lt;/p&gt;

&lt;p&gt;The bundler complains about it.&lt;/p&gt;

&lt;p&gt;Correctly.&lt;/p&gt;

&lt;p&gt;I have deliberately not responded by throwing &lt;code&gt;lazy()&lt;/code&gt; around random components and declaring the performance problem solved.&lt;/p&gt;

&lt;p&gt;There is a proper performance investigation queued.&lt;/p&gt;

&lt;p&gt;The bundle is an obvious place to start, but I want to measure the actual user-visible latency before deciding what needs to change.&lt;/p&gt;

&lt;p&gt;There is also an authenticated dashboard component that has accumulated too many responsibilities, an icon fallback that is resilient but can hide misspelled glyph names, and some operational cleanup I deliberately postponed until the feature pressure drops.&lt;/p&gt;

&lt;p&gt;I have debt.&lt;/p&gt;

&lt;p&gt;I know where it is.&lt;/p&gt;

&lt;p&gt;I know why it exists.&lt;/p&gt;

&lt;p&gt;I've become much more comfortable with the difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;unknown, accidental debt&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;known, bounded debt that I deliberately chose not to fix today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second kind still needs paying.&lt;/p&gt;

&lt;p&gt;But at least I know what I owe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that changed how I engineer
&lt;/h2&gt;

&lt;p&gt;I've tried to work out which part of the project had the biggest impact on me.&lt;/p&gt;

&lt;p&gt;The transfer system?&lt;/p&gt;

&lt;p&gt;The provider abstraction?&lt;/p&gt;

&lt;p&gt;The ledger?&lt;/p&gt;

&lt;p&gt;Security?&lt;/p&gt;

&lt;p&gt;Partner workflows?&lt;/p&gt;

&lt;p&gt;Developer APIs?&lt;/p&gt;

&lt;p&gt;Deployment?&lt;/p&gt;

&lt;p&gt;The PWA?&lt;/p&gt;

&lt;p&gt;The queue architecture?&lt;/p&gt;

&lt;p&gt;I don't think it is one feature.&lt;/p&gt;

&lt;p&gt;The bigger job was repeatedly taking vague product statements and turning them into rules a system could actually enforce.&lt;/p&gt;

&lt;p&gt;Take:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Users should be able to transfer money.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay.&lt;/p&gt;

&lt;p&gt;Which users?&lt;/p&gt;

&lt;p&gt;From which accounts?&lt;/p&gt;

&lt;p&gt;At what account state?&lt;/p&gt;

&lt;p&gt;Which currencies?&lt;/p&gt;

&lt;p&gt;Which rails?&lt;/p&gt;

&lt;p&gt;Which provider capabilities?&lt;/p&gt;

&lt;p&gt;Which verification state?&lt;/p&gt;

&lt;p&gt;What limits?&lt;/p&gt;

&lt;p&gt;How is the recipient resolved?&lt;/p&gt;

&lt;p&gt;How long is that resolution trusted?&lt;/p&gt;

&lt;p&gt;Can somebody transfer to themselves?&lt;/p&gt;

&lt;p&gt;What happens if they submit twice?&lt;/p&gt;

&lt;p&gt;What happens if the connection disappears?&lt;/p&gt;

&lt;p&gt;What if the provider actually processed the transfer?&lt;/p&gt;

&lt;p&gt;What if the webhook arrives first?&lt;/p&gt;

&lt;p&gt;What if it arrives twice?&lt;/p&gt;

&lt;p&gt;What does the user see while we genuinely don't know?&lt;/p&gt;

&lt;p&gt;What does support see?&lt;/p&gt;

&lt;p&gt;What gets logged?&lt;/p&gt;

&lt;p&gt;What gets audited?&lt;/p&gt;

&lt;p&gt;Who gets notified?&lt;/p&gt;

&lt;p&gt;What happens if a dependency is unavailable?&lt;/p&gt;

&lt;p&gt;Then repeat that exercise for cards.&lt;/p&gt;

&lt;p&gt;FX.&lt;/p&gt;

&lt;p&gt;KYC.&lt;/p&gt;

&lt;p&gt;Bill payments.&lt;/p&gt;

&lt;p&gt;Notifications.&lt;/p&gt;

&lt;p&gt;Partner referrals.&lt;/p&gt;

&lt;p&gt;Developer applications.&lt;/p&gt;

&lt;p&gt;Documents.&lt;/p&gt;

&lt;p&gt;Authentication.&lt;/p&gt;

&lt;p&gt;Administration.&lt;/p&gt;

&lt;p&gt;At some point I realised that a huge part of my work had become finding every unanswered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Okay, but what happens if...?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and making the system choose an answer deliberately.&lt;/p&gt;

&lt;p&gt;That's probably the best description I have for the project.&lt;/p&gt;

&lt;p&gt;Earlier in the build, I was much more likely to fix the visible problem.&lt;/p&gt;

&lt;p&gt;Authenticated user gets kicked back to login?&lt;/p&gt;

&lt;p&gt;Fix the redirect.&lt;/p&gt;

&lt;p&gt;Button doesn't work?&lt;/p&gt;

&lt;p&gt;Fix the button.&lt;/p&gt;

&lt;p&gt;Mobile page looks broken?&lt;/p&gt;

&lt;p&gt;Fix that page.&lt;/p&gt;

&lt;p&gt;Later I became more annoying.&lt;/p&gt;

&lt;p&gt;Why did an authorization failure become an authentication failure?&lt;/p&gt;

&lt;p&gt;Does another route use the same middleware pattern?&lt;/p&gt;

&lt;p&gt;Why was the button able to request an operation the server should have rejected anyway?&lt;/p&gt;

&lt;p&gt;Is the responsive bug local to this component, or is the same broken primitive used on twelve other pages?&lt;/p&gt;

&lt;p&gt;Somewhere in that process, the way I debug changed.&lt;/p&gt;

&lt;p&gt;I moved from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fix the bug&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fix the invariant that allowed this class of bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second one can take longer.&lt;/p&gt;

&lt;p&gt;It tends to pay back.&lt;/p&gt;

&lt;p&gt;There are definitely things I would do differently if I were starting again.&lt;/p&gt;

&lt;p&gt;I would define important state machines much earlier.&lt;/p&gt;

&lt;p&gt;Before the UI.&lt;/p&gt;

&lt;p&gt;Before controllers.&lt;/p&gt;

&lt;p&gt;Before three different parts of the application all have their own interpretation of what &lt;code&gt;pending&lt;/code&gt; means.&lt;/p&gt;

&lt;p&gt;I would establish provider boundaries earlier.&lt;/p&gt;

&lt;p&gt;It is much easier to prevent provider-specific knowledge from spreading than to remove it later.&lt;/p&gt;

&lt;p&gt;I would design mobile as its own interaction problem from day one.&lt;/p&gt;

&lt;p&gt;I would establish stronger conventions around notifications, authorization, and operational states before several domains needed them.&lt;/p&gt;

&lt;p&gt;I would define "done" much more harshly at the beginning.&lt;/p&gt;

&lt;p&gt;And yes, I would simplify the Git setup.&lt;/p&gt;

&lt;p&gt;Very much that last one.&lt;/p&gt;

&lt;p&gt;I like the stack I'm using.&lt;/p&gt;

&lt;p&gt;But I don't think "I learned Fastify" or "I used BullMQ" is what I'll remember from this project.&lt;/p&gt;

&lt;p&gt;What I'll remember is how often I ended up asking:&lt;/p&gt;

&lt;p&gt;Where does truth live?&lt;/p&gt;

&lt;p&gt;Who owns this state?&lt;/p&gt;

&lt;p&gt;Who is allowed to change it?&lt;/p&gt;

&lt;p&gt;What happens if this runs twice?&lt;/p&gt;

&lt;p&gt;What happens if it succeeds externally but fails internally?&lt;/p&gt;

&lt;p&gt;Can I safely retry it?&lt;/p&gt;

&lt;p&gt;If I can't retry it, how do I recover?&lt;/p&gt;

&lt;p&gt;Does the database need to enforce this?&lt;/p&gt;

&lt;p&gt;Should this piece of work be allowed to wait?&lt;/p&gt;

&lt;p&gt;What evidence do we leave behind?&lt;/p&gt;

&lt;p&gt;Can somebody else figure out what happened when I'm not there?&lt;/p&gt;

&lt;p&gt;What does the user see while the system genuinely doesn't know yet?&lt;/p&gt;

&lt;p&gt;I knew what idempotency was before this project.&lt;/p&gt;

&lt;p&gt;I knew what queues were.&lt;/p&gt;

&lt;p&gt;I knew what database locking was.&lt;/p&gt;

&lt;p&gt;I knew what RBAC was.&lt;/p&gt;

&lt;p&gt;I knew what webhooks were.&lt;/p&gt;

&lt;p&gt;I knew what reconciliation meant.&lt;/p&gt;

&lt;p&gt;I just understand &lt;strong&gt;why those things exist&lt;/strong&gt; much better now.&lt;/p&gt;

&lt;p&gt;And that's probably the part I'll remember longest.&lt;/p&gt;

&lt;p&gt;I'm still inside the build.&lt;/p&gt;

&lt;p&gt;I still find awkward UI.&lt;/p&gt;

&lt;p&gt;I still find journeys where a state exists without a good recovery path.&lt;/p&gt;

&lt;p&gt;I still open files and think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why did I let you get this big?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I still have the performance investigation waiting for me.&lt;/p&gt;

&lt;p&gt;There are refactors I've deliberately postponed until the feature pressure drops.&lt;/p&gt;

&lt;p&gt;But when I compare what I thought I was building at the beginning with what exists now, the difference is kind of absurd.&lt;/p&gt;

&lt;p&gt;More than 700 commits.&lt;/p&gt;

&lt;p&gt;More than 200,000 tracked lines.&lt;/p&gt;

&lt;p&gt;Three applications.&lt;/p&gt;

&lt;p&gt;Thirteen shared packages.&lt;/p&gt;

&lt;p&gt;More than 500 tests.&lt;/p&gt;

&lt;p&gt;And considerably more time thinking about failure than I expected when I started.&lt;/p&gt;

&lt;p&gt;I started the project thinking mostly about how to implement features.&lt;/p&gt;

&lt;p&gt;I'm finishing it thinking much more about what the system is actually allowed to promise.&lt;/p&gt;

&lt;p&gt;That's a much more useful change than another framework on my CV.&lt;/p&gt;

&lt;p&gt;I'm almost done.&lt;/p&gt;

&lt;p&gt;For now, back to figuring out why that last thing is still broken.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;If you've built systems where retries, partial failure, or uncertain state became real problems, I'd genuinely like to hear what changed in the way you engineer.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>fintech</category>
      <category>typescript</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
