<?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: Awaluddin</title>
    <description>The latest articles on DEV Community by Awaluddin (@awaluddin).</description>
    <link>https://dev.to/awaluddin</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%2F2895475%2Fab9117b5-f18b-428e-b83e-59482ac0928a.jpg</url>
      <title>DEV Community: Awaluddin</title>
      <link>https://dev.to/awaluddin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/awaluddin"/>
    <language>en</language>
    <item>
      <title>I Built a Multi-Provider LLM Router for My AI Worker - Here's What I Learned</title>
      <dc:creator>Awaluddin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 11:59:37 +0000</pubDate>
      <link>https://dev.to/awaluddin/i-built-a-multi-provider-llm-router-for-my-ai-worker-heres-what-i-learned-l8d</link>
      <guid>https://dev.to/awaluddin/i-built-a-multi-provider-llm-router-for-my-ai-worker-heres-what-i-learned-l8d</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Multi-Provider LLM Router for My AI Worker — Here's What I Learned
&lt;/h1&gt;

&lt;p&gt;When I started integrating LLMs into my side project AuraFlow AI, I made the same mistake most backend engineers make: I hardcoded a single provider.&lt;/p&gt;

&lt;p&gt;One week in, Gemini free tier hit its rate limit at 11 PM while I was testing. Everything stopped. I had to manually swap the API key, restart the worker, and lose 20 minutes of debugging momentum.&lt;/p&gt;

&lt;p&gt;That was the last time I let a single LLM provider be a single point of failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is AuraFlow AI?
&lt;/h2&gt;

&lt;p&gt;AuraFlow AI is a distributed data cleaning system I built for my portfolio. The architecture is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /jobs (NestJS/Fastify/Bun)
    → BullMQ job pushed to Redis
        → Python LangGraph worker picks up job
            → Parser Agent: clean raw/malformed data using LLM
            → Validator Agent: verify output, loop back if invalid
        → HTTP callback with retry + idempotency
    → Result persisted to PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9k99v2jAQx_-VUx6qVh1DW7cXNE0KAVIQjBRQ-2B4MMmRuCR2ajut0qb_--yQrfxS_XSKv3efb-58b04oInQ64GxS8RImVGpY9JYczFHFOpY0T8BLGXINZOk00ZiWKJfOaqezxyXN1QyfClR6Ba3W7yqYzhfQfhRrVUGXuMEQfKrxhZYd-GNEozm0YUCVZpvSRN2CNxWRR0t-ZKIrxRalNTFBpWiMzZcDG90dtlAJjMS6Ao9czjBiCq5N9TSd3F19QngQsiEEpU4EN7_JY3_v6gDl7VAs3Coocni0uB5xY9sEK7f93JN_RP95tVZZXhNdwHg8gZko9BHLnp7lQZ8EVCrjsk45kvRrS16KlGMEPappBQNyT1MWUS3O5wyg9dUkzVDLEtgGhvzZ6ivoH-qOSRcm85d15BNjeue5Y4aZpmsabsFLKON7qLrfp7X8mg6334hXKC0y6PMoF8y0sP3vqaxO1d-JL0ScIviYMc7OKG6IL8WTKTIe04kLN2ckP8g0R-4Ojch9LSSekfwkLteJFDkLzQ7QIsJPXs-00HlRL0mAUjGlkYdo2tTDlD2jLA8GOqgndb_r9JBcBkLpWOL8bgy97tWJ8HaxCMBrelvBiDzgOhFia7YtRFu9A7sBXsMwwiwXFl5-mHW-gJOhzCiLzKq_OTrBrF76CDe0SLXz_v4XGTUwlw%3D%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNp9k99v2jAQx_-VUx6qVh1DW7cXNE0KAVIQjBRQ-2B4MMmRuCR2ajut0qb_--yQrfxS_XSKv3efb-58b04oInQ64GxS8RImVGpY9JYczFHFOpY0T8BLGXINZOk00ZiWKJfOaqezxyXN1QyfClR6Ba3W7yqYzhfQfhRrVUGXuMEQfKrxhZYd-GNEozm0YUCVZpvSRN2CNxWRR0t-ZKIrxRalNTFBpWiMzZcDG90dtlAJjMS6Ao9czjBiCq5N9TSd3F19QngQsiEEpU4EN7_JY3_v6gDl7VAs3Coocni0uB5xY9sEK7f93JN_RP95tVZZXhNdwHg8gZko9BHLnp7lQZ8EVCrjsk45kvRrS16KlGMEPappBQNyT1MWUS3O5wyg9dUkzVDLEtgGhvzZ6ivoH-qOSRcm85d15BNjeue5Y4aZpmsabsFLKON7qLrfp7X8mg6334hXKC0y6PMoF8y0sP3vqaxO1d-JL0ScIviYMc7OKG6IL8WTKTIe04kLN2ckP8g0R-4Ojch9LSSekfwkLteJFDkLzQ7QIsJPXs-00HlRL0mAUjGlkYdo2tTDlD2jLA8GOqgndb_r9JBcBkLpWOL8bgy97tWJ8HaxCMBrelvBiDzgOhFia7YtRFu9A7sBXsMwwiwXFl5-mHW-gJOhzCiLzKq_OTrBrF76CDe0SLXz_v4XGTUwlw%3D%3D" alt="AuraFlow AI Architecture" width="1764" height="1239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The LLM is at the core of both agents. If the LLM provider goes down, the entire system stops. That's the problem I needed to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Naive Version
&lt;/h2&gt;

&lt;p&gt;My first implementation looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_google_genai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatGoogleGenerativeAI&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatGoogleGenerativeAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;google_api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;parse_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cleaned_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Works. Completely fragile.&lt;/p&gt;

&lt;p&gt;The problems were obvious once I started thinking about production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limit on Gemini free tier? System down.&lt;/li&gt;
&lt;li&gt;Want to switch to a cheaper provider? Change code, redeploy.&lt;/li&gt;
&lt;li&gt;Want to test with a local model? Swap the entire implementation.&lt;/li&gt;
&lt;li&gt;Gemini API has an outage? System down until I wake up and notice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed a proper abstraction layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design
&lt;/h2&gt;

&lt;p&gt;I wanted three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multiple providers&lt;/strong&gt; — Gemini, OpenAI, Claude, Groq, and any OpenAI-compatible custom endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic fallback&lt;/strong&gt; — if provider A fails, try B, then C, without any code change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-code configuration&lt;/strong&gt; — swap providers and priority via environment variables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is &lt;code&gt;LLMRouter&lt;/code&gt;: a registry-based provider abstraction with a configurable fallback chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LLMRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseChatModel&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_chain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Priority order from env — change without touching code
&lt;/span&gt;        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LLM_PROVIDER_ORDER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini,groq,openai,claude,custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;provider_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PROVIDER_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_MODEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provider loaded: %s (model: %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provider skipped (no API key): %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No LLM provider available.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fallback chain: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Provider %s failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All providers failed. Last error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider has its own loader function that returns &lt;code&gt;None&lt;/code&gt; if the API key is not set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseChatModel&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# Skip silently — not an error
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_google_genai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatGoogleGenerativeAI&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChatGoogleGenerativeAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;google_api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gemini load failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry maps provider names to loader functions and default models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PROVIDER_REGISTRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_gemini&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.6-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_claude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4-5-20251001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_groq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/gpt-oss-20b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;azure_openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_azure_openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_load_custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.2&lt;/span&gt;&lt;span class="sh"&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;h2&gt;
  
  
  The Custom Endpoint — The Part I Didn't Expect to Need
&lt;/h2&gt;

&lt;p&gt;The most useful feature ended up being one I almost didn't build: the custom OpenAI-compatible endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_custom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseChatModel&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOM_LLM_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUSTOM_LLM_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom&lt;/span&gt;&lt;span class="sh"&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;This works with anything that implements the OpenAI chat completions spec: Ollama, OpenRouter, vLLM, or a third-party API gateway. In my case, I'm using a custom API gateway as primary provider with Gemini as fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env
&lt;/span&gt;&lt;span class="py"&gt;LLM_PROVIDER_ORDER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;custom,gemini&lt;/span&gt;
&lt;span class="py"&gt;CUSTOM_LLM_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://my-api-gateway.example.com/v1&lt;/span&gt;
&lt;span class="py"&gt;CUSTOM_LLM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;my_key&lt;/span&gt;
&lt;span class="py"&gt;CUSTOM_LLM_MODEL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-8&lt;/span&gt;
&lt;span class="py"&gt;GEMINI_MODEL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;gemini-3.6-flash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Startup log confirms the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Provider loaded: custom (model: claude-opus-4-8)
Provider loaded: gemini (model: gemini-3.6-flash)
Provider skipped (no API key): groq
Provider skipped (no API key): openai
Fallback chain: custom -&amp;gt; gemini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Actually Happened in Production (Accidental Stress Test)
&lt;/h2&gt;

&lt;p&gt;I didn't plan this, but it happened during testing and it validated the entire design.&lt;/p&gt;

&lt;p&gt;I submitted a job while Docker Compose was running. The gateway crashed mid-flight (&lt;code&gt;exited with code 137&lt;/code&gt; — OOM killed). Here's the exact log sequence from the worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;job_received job_id=cmsi1m2dn000001pmnnhwojpf
parse_node attempt=1
Invoking provider: custom
parse_node output={"name": "Test Retry1", ...}
validate_node attempt=1
validate_node is_valid=False reason=name must be two words with each word
&lt;/span&gt;&lt;span class="gp"&gt;    capitalized and contain only letters;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Retry1'&lt;/span&gt; contains a digit
&lt;span class="go"&gt;graph_decision result=retry attempt=1

parse_node attempt=2
parse_node output={"name": "Test Retry", ...}
validate_node is_valid=True reason=OK
graph_decision result=valid attempts=2

job_finished job_id=cmsi1m2dn000001pmnnhwojpf is_valid=True attempts=2

callback_connection_error ... attempt=1/5
callback_retry_scheduled delay=1.0s next_attempt=2
callback_connection_error ... attempt=2/5
callback_retry_scheduled delay=2.0s next_attempt=3
callback_connection_error ... attempt=3/5
callback_retry_scheduled delay=4.0s next_attempt=4
callback_sent status_code=200 attempt=4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happened simultaneously that I didn't orchestrate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. LangGraph retry loop worked.&lt;/strong&gt; The validator correctly rejected &lt;code&gt;Test Retry1&lt;/code&gt; because &lt;code&gt;Retry1&lt;/code&gt; contains a digit. The parser received the rejection reason and fixed it on attempt 2. The feedback loop between validator and parser is exactly what makes this more than a simple LLM call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Callback retry with exponential backoff worked.&lt;/strong&gt; The gateway was down. The worker kept retrying with 1s → 2s → 4s delays. When the gateway came back up, attempt 4 succeeded. No data was lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The worker kept running.&lt;/strong&gt; Because LangGraph and the callback retry are separate from the gateway's lifecycle, the worker finished its job correctly even though the service it was trying to reach was down.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration — 5 Providers, Zero Code Changes
&lt;/h2&gt;

&lt;p&gt;This is what I was aiming for. To switch from custom + Gemini to OpenAI + Groq:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Before
LLM_PROVIDER_ORDER=custom,gemini

# After — no code change, just env update
LLM_PROVIDER_ORDER=openai,groq
OPENAI_API_KEY=sk-...
GROQ_API_KEY=gsk_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To override the model for a specific provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GEMINI_MODEL=gemini-3.6-flash
OPENAI_MODEL=gpt-4o
GROQ_MODEL=openai/gpt-oss-20b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router reads these at startup and builds the chain. Five providers configured, but only the ones with valid credentials get loaded.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Health check before adding to chain.&lt;/strong&gt; Right now, &lt;code&gt;_load_custom&lt;/code&gt; succeeds if &lt;code&gt;CUSTOM_LLM_BASE_URL&lt;/code&gt; is set — even if the endpoint is unreachable. A lightweight ping at startup would catch misconfigured endpoints before the first real job comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider-level metrics.&lt;/strong&gt; I want to know which provider is being invoked most often and which ones are failing. Right now this lives in logs. It should be structured data I can query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breaker per provider.&lt;/strong&gt; If Gemini returns 5xx three times in a row, stop trying Gemini for the next 60 seconds before retrying. Right now every invoke attempt goes through the full retry before moving to the next provider in the chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Single provider = single point of failure. This is obvious in hindsight, but it took an 11 PM rate limit hit to make me actually fix it.&lt;/p&gt;

&lt;p&gt;The pattern is simple: registry of loaders, environment-configured priority, iterate and fallback on exception. About 100 lines of Python. The complexity is low; the resilience gain is significant.&lt;/p&gt;

&lt;p&gt;If you're building any backend system that calls an LLM, this abstraction is worth the hour it takes to implement.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;AuraFlow AI source code:&lt;/strong&gt; &lt;a href="https://github.com/awaluddin-dev/auraflow-ai" rel="noopener noreferrer"&gt;github.com/awaluddin-dev/auraflow-ai&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Awaluddin — Backend Engineer &amp;amp; AI Integrator based in Jakarta, currently consulting at an enterprise automotive company. Building toward a fully remote role. You can find my work at &lt;a href="https://awaluddin-dev.vercel.app" rel="noopener noreferrer"&gt;awaluddin-dev.vercel.app&lt;/a&gt; or connect on &lt;a href="https://linkedin.com/in/awaluddin0001" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>langchain</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Rewrote a Fintech Platform Alone — No Handover, No Team, No Docs</title>
      <dc:creator>Awaluddin</dc:creator>
      <pubDate>Fri, 26 Jun 2026 08:30:19 +0000</pubDate>
      <link>https://dev.to/awaluddin/i-rewrote-a-fintech-platform-alone-no-handover-no-team-no-docs-590p</link>
      <guid>https://dev.to/awaluddin/i-rewrote-a-fintech-platform-alone-no-handover-no-team-no-docs-590p</guid>
      <description>&lt;p&gt;The OJK auditor was on the call. I was sharing my screen. And the app crashed.&lt;br&gt;
No senior engineer to ping. No team lead to escalate to. Just me, a system I had rebuilt from scratch over the past several months, and a government regulator watching in real time.&lt;br&gt;
That moment is burned into my memory — not as the worst day of my career, but as the most clarifying one.&lt;/p&gt;

&lt;p&gt;The Situation I Walked Into&lt;br&gt;
I joined a fintech startup as the sole backend developer. What I found on day one: no handover document, no architecture diagram, no README, no one to ask. The previous developer had already left.&lt;br&gt;
The existing system was a PHP Laravel monolith running on CentOS, with a React Native mobile app, MySQL, and two Alibaba Cloud instances — one primary, one for mirroring. It had been running for years. Nobody fully understood how it worked anymore.&lt;br&gt;
The company held a P2P lending license and was under pressure from OJK (Indonesia's financial regulator) and AFPI to bring the system up to compliance standards. The director gave me the brief and one key piece of information: most of the core operations had been running manually.&lt;br&gt;
Within months, the director stopped coming in. I later learned the company was facing serious financial difficulties. By that point, I was the only person in the office — me and one admin.&lt;br&gt;
I kept building.&lt;/p&gt;

&lt;p&gt;First Decision: Don't Rewrite Everything at Once&lt;br&gt;
The legacy codebase was beyond patching. A full rewrite was necessary. But rewriting an entire fintech platform alone is not a sprint — it's a months-long commitment with regulatory deadlines attached.&lt;br&gt;
My decision: rewrite incrementally. Start with authentication only. Prove the new stack works. Then expand.&lt;br&gt;
New stack: NestJS on the backend, Next.js for the web frontend, Expo for mobile, Ubuntu replacing CentOS, Docker for containerization.&lt;br&gt;
The auth system was completed. Clean, working, deployable.&lt;br&gt;
Then I got the feedback from OJK: a partial system is not acceptable for audit. The entire platform needed to be functional — not just authentication.&lt;br&gt;
I had made a technically sound decision with a faulty assumption. I had not validated the audit scope with the regulator before committing to the incremental approach.&lt;br&gt;
That was the first real lesson.&lt;/p&gt;

&lt;p&gt;What I Built, Alone&lt;br&gt;
Over the following months, working without a team, without guidance, and with a paycheck that arrived increasingly late, I built:&lt;/p&gt;

&lt;p&gt;Two-factor authentication (password + OTP via email and phone)&lt;br&gt;
KYC flow with face photo upload&lt;br&gt;
DigiSign integration for digital signatures&lt;br&gt;
Full migration from monolith to microservices&lt;br&gt;
AFPI Data Center integration for regulatory reporting&lt;br&gt;
Loan application flow with approval stages, interest calculation, tenor management, and insurance integration&lt;br&gt;
BNI sandbox integration, tested through to live transaction&lt;br&gt;
Mobile app rebuilt in Expo&lt;br&gt;
CI/CD pipeline with Jenkins and Docker on Ubuntu&lt;/p&gt;

&lt;p&gt;All of it, in parallel, without a senior engineer to ask.&lt;/p&gt;

&lt;p&gt;The Audit Day&lt;br&gt;
When the OJK audit session came, the system had most of its core features working. But during the live demo, there was a bug. The kind that only appears under pressure, in front of the people you least want to see it.&lt;br&gt;
The audit did not proceed.&lt;br&gt;
I am not going to reframe this as a hidden success. It was a hard moment. The system I had poured months into — alone, under financial stress, in an increasingly unstable company — did not make it through the finish line that day.&lt;br&gt;
But here is what I know: the system ran. It processed real transactions. It integrated with government data centers. It had security layers that the original system never had.&lt;br&gt;
It just wasn't enough, on that day.&lt;/p&gt;

&lt;p&gt;What I Actually Learned&lt;br&gt;
Validate assumptions with your stakeholders before committing to an architecture decision.&lt;/p&gt;

&lt;p&gt;Incremental migration was the right technical call. But I assumed OJK would accept a partial system without confirming it first. That assumption cost months of work. Now, before I make any significant architectural decision, I ask the hard question upfront: what does "done" mean to the person who will evaluate this?&lt;br&gt;
One database per service is not optional in microservices.&lt;/p&gt;

&lt;p&gt;Early in the build, I made a mistake I only recognized later: I created a dedicated Express service for the service bus, but gave each service its own MySQL instance rather than its own database on a shared instance. The RAM cost was immediate and painful. I fixed it — but I learned the principle properly only by feeling the consequence.&lt;br&gt;
Working without a team teaches you things a team never would.&lt;/p&gt;

&lt;p&gt;When there is no one else, every architectural decision is yours. Every bug is yours. Every late-night debugging session is yours. It is uncomfortable in ways that are difficult to describe. But the outcome is a developer who can read an unfamiliar codebase, make decisions without guidance, and stay functional when the environment is unstable.&lt;/p&gt;

&lt;p&gt;The platform never went live. The company did not survive its challenges.&lt;br&gt;
But every system I have built since — at Telkomsel, at Astra Group — carries the decision-making instincts I developed in that office, alone, with a legacy codebase and a deadline I could not fully control.&lt;br&gt;
That is what the hardest projects give you. Not a success story. A foundation.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>career</category>
      <category>node</category>
      <category>microservices</category>
    </item>
  </channel>
</rss>
