<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.questionpro.com/engineering/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.questionpro.com/engineering/" rel="alternate" type="text/html" /><updated>2026-06-18T04:36:47+00:00</updated><id>https://www.questionpro.com/engineering/feed.xml</id><title type="html">Engineering Blog</title><subtitle>Discover QuestionPro&apos;s engineering culture, technical challenges, and career opportunities. Learn how our engineering team builds scalable survey platforms serving millions of users.</subtitle><author><name>Your Name</name></author><entry><title type="html">Auto Translate - Every word to every language</title><link href="https://www.questionpro.com/engineering/software%20engineering/frontend%20architecture/react/vite/localization%20(i18n)/ai/auto-translate-every-word-to-every-language/" rel="alternate" type="text/html" title="Auto Translate - Every word to every language" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/software%20engineering/frontend%20architecture/react/vite/localization%20(i18n)/ai/auto-translate-every-word-to-every-language</id><content type="html" xml:base="https://www.questionpro.com/engineering/software%20engineering/frontend%20architecture/react/vite/localization%20(i18n)/ai/auto-translate-every-word-to-every-language/"><![CDATA[<p>We recently rolled out a new translation system across our applications. The goal was simple: if an app needs to support a new language, developers shouldn’t have to spend days wiring up translation files, managing keys, or updating components manually.</p>

<p>Instead, translations should happen automatically. This is the story of how we built that system, which… wasn’t simple at all!</p>

<h2 id="the-problem">The Problem</h2>

<p>Over the years, we accumulated a large number of apps. Like most products, they were filled with buttons, labels, placeholders, tooltips, and messages written directly in English. Whenever a team wanted to support another language, the process was always the same:</p>

<ol>
  <li>Find the strings</li>
  <li>Extract them</li>
  <li>Create translation keys</li>
  <li>Update the UI</li>
  <li>Maintain translation files forever</li>
</ol>

<p>It worked, but nobody enjoyed doing it. We started wondering whether the build process could do most of that work for us. That idea eventually became <code class="language-plaintext highlighter-rouge">wick-ui-i18n</code>.</p>

<h2 id="build-time-translation-extraction">Build-Time Translation Extraction</h2>

<p>At the center of the system is a Vite plugin built specifically for our internal component library, <code class="language-plaintext highlighter-rouge">wick-ui</code>. The plugin scans application code during the build, finds text inside supported components, extracts it automatically, and generates a translation dictionary.</p>

<p><strong>Developers keep writing components normally. No translation keys. No manual extraction.</strong></p>

<h3 id="how-it-works">How It Works</h3>

<p>The plugin runs during Vite’s <code class="language-plaintext highlighter-rouge">transform</code> phase. Before doing any expensive parsing, it performs a quick check. If a file doesn’t contain a <code class="language-plaintext highlighter-rouge">wick-ui</code> component, it gets skipped immediately. Files that pass the check are parsed into a <strong>Babel AST (Abstract Syntax Tree)</strong>, and the plugin walks through the tree looking for translatable content.</p>

<h3 id="handling-different-types-of-text">Handling Different Types of Text</h3>

<p>One of the first challenges was that text can appear in several different forms:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span>Hello<span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span><span class="si">{</span><span class="dl">'</span><span class="s1">Hello</span><span class="dl">'</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span>Hello <span class="si">{</span><span class="nx">name</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span><span class="si">{</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nc">WuButton</span> <span class="na">children</span><span class="p">=</span><span class="s">"Hello"</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Did you know?</strong> The expressions inside <code class="language-plaintext highlighter-rouge">${}</code> in JavaScript template literals are formally called <em>quasis</em>.</p>
</blockquote>

<p>Each case produces a different AST structure, so they need different handling. Plain JSX text is straightforward. Static string expressions are straightforward too. Template literals are more interesting. For example:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="s2">`Hello </span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">, how are you doing?`</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Since part of the text is dynamic, it can’t become a single translation key. Instead, the plugin extracts the static portions and leaves runtime values untouched. The result looks like this:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;&gt;</span>
  <span class="p">&lt;</span><span class="nc">WuTranslate</span> <span class="na">__i18nKey</span><span class="p">=</span><span class="s">"Hello"</span> <span class="p">/&gt;</span>
  <span class="si">{</span><span class="nx">name</span><span class="si">}</span>
  <span class="p">&lt;</span><span class="nc">WuTranslate</span> <span class="na">__i18nKey</span><span class="p">=</span><span class="s">", how are you doing?"</span> <span class="p">/&gt;</span>
<span class="p">&lt;/&gt;</span>
</code></pre></div></div>

<p><em>(Note: In this output, <code class="language-plaintext highlighter-rouge">WuTranslate</code> acts as a React fragment. More on this later.)</em></p>

<p>The same idea applies to translatable props such as <code class="language-plaintext highlighter-rouge">placeholder</code>, <code class="language-plaintext highlighter-rouge">title</code>, and <code class="language-plaintext highlighter-rouge">aria-label</code>. For example:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuInput</span> <span class="na">placeholder</span><span class="p">=</span><span class="s">"Search..."</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p>Becomes:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuInput</span> <span class="na">placeholder</span><span class="p">=</span><span class="si">{</span><span class="nf">wt</span><span class="p">(</span><span class="dl">"</span><span class="s2">Search...</span><span class="dl">"</span><span class="p">)</span><span class="si">}</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<h3 id="the-html-entity-surprise">The HTML Entity Surprise</h3>

<p>One of the more unexpected bugs appeared after the initial rollout. A string containing <code class="language-plaintext highlighter-rouge">&amp;amp;</code> displayed correctly in one place but showed the literal text <code class="language-plaintext highlighter-rouge">&amp;amp;</code> somewhere else.</p>

<p>The root cause was <strong>Babel</strong>.</p>

<p>When Babel parses JSX text, it automatically decodes HTML entities. By the time the plugin sees the value, <code class="language-plaintext highlighter-rouge">&amp;amp;</code> has already become <code class="language-plaintext highlighter-rouge">&amp;</code>. Using that decoded value as a translation key caused inconsistencies when the code was rewritten.</p>

<p>The fix was to inspect the original source text instead of Babel’s decoded value. Whenever an entity is found, the plugin preserves it exactly as written and only extracts the surrounding text. This means:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span>Hello <span class="ni">&amp;amp;</span> World<span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>Becomes:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuButton</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">WuTranslate</span> <span class="na">__i18nKey</span><span class="p">=</span><span class="s">"Hello"</span> <span class="p">/&gt;</span>
  <span class="ni">&amp;amp;</span>
  <span class="p">&lt;</span><span class="nc">WuTranslate</span> <span class="na">__i18nKey</span><span class="p">=</span><span class="s">"World"</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">WuButton</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>The entity remains untouched while the surrounding text stays translatable.</p>

<h3 id="the-wt-helper">The <code class="language-plaintext highlighter-rouge">wt()</code> Helper</h3>

<p>Not every string lives inside JSX. Sometimes translations are needed in configuration objects, event handlers, utility functions, or hooks. For those situations, we added <code class="language-plaintext highlighter-rouge">wt()</code>:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">placeholder</span><span class="o">=</span><span class="p">{</span><span class="nf">wt</span><span class="p">(</span><span class="dl">'</span><span class="s1">Enter your name</span><span class="dl">'</span><span class="p">)}</span>
</code></pre></div></div>

<p>The plugin records these keys during the build process but leaves the code itself unchanged. From a developer’s perspective, it’s just another function call.</p>

<p>However, a standard function is not reactive. To solve this, we also introduced the <code class="language-plaintext highlighter-rouge">useWt()</code> hook to make text reactive when translations change dynamically.</p>

<h3 id="generated-output">Generated Output</h3>

<p>As files are processed, the plugin builds a dictionary of translation keys. At the end of the build, that dictionary is written to <code class="language-plaintext highlighter-rouge">wick-ui-i18n.json</code>.</p>

<p>During development, Vite serves this file dynamically so newly added keys appear immediately without requiring a rebuild. The plugin also injects any required imports automatically.</p>

<h2 id="runtime-translation">Runtime Translation</h2>

<p>The build step only creates the dictionary. Runtime translation is handled by <code class="language-plaintext highlighter-rouge">wick-ui-lib</code>.</p>

<h3 id="wutranslateprovider"><code class="language-plaintext highlighter-rouge">WuTranslateProvider</code></h3>

<p>Applications wrap themselves with <code class="language-plaintext highlighter-rouge">WuTranslateProvider</code>:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuTranslateProvider</span> <span class="na">defaultLocale</span><span class="p">=</span><span class="s">"es_LA"</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">App</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nc">WuTranslateProvider</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>When the app starts—or when the locale changes—the provider loads the generated dictionary. For English, that’s all that’s required.</p>

<p>For any other language, the provider sends the dictionary to our translation service and receives translated values in return. If the translation fails, the system falls back to the original English strings instead of breaking the UI.</p>

<p>With the hook, we can change the language dynamically. On change, we call the backend, sending the JSON file with the target language code, and it instantly returns the translation for the respective keys. The UI update is instantaneous. Although the performance tab might show multiple renders, they are lightweight text updates—so no worries!</p>

<h3 id="wutranslate"><code class="language-plaintext highlighter-rouge">WuTranslate</code></h3>

<p>Text extracted by the plugin is rewritten into:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">WuTranslate</span> <span class="na">__i18nKey</span><span class="p">=</span><span class="s">"Hello"</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p>The component simply looks up the translated value and falls back to the original key when a translation is missing. This component actually replaces the text with a <strong>React Fragment</strong>. We could have used a <code class="language-plaintext highlighter-rouge">span</code> or another HTML node, but we chose <code class="language-plaintext highlighter-rouge">&lt;Fragment&gt;</code> so the plugin wouldn’t inject any unnecessary DOM nodes. It also makes debugging significantly cleaner.</p>

<blockquote>
  <p><strong>Implementation Note:</strong> We used <code class="language-plaintext highlighter-rouge">magic-string</code> during the compilation phase to ensure that original line numbers persist for accurate debugging.</p>
</blockquote>

<h3 id="wt-and-usewt"><code class="language-plaintext highlighter-rouge">wt()</code> and <code class="language-plaintext highlighter-rouge">useWt()</code></h3>

<p>For non-React code, <code class="language-plaintext highlighter-rouge">wt()</code> reads from a shared dictionary store and returns the translated value when available. When reactivity is required inside components, <code class="language-plaintext highlighter-rouge">useWt()</code> can be used instead. This allows us to fully support translations inside plain <code class="language-plaintext highlighter-rouge">.ts</code> files (like constant arrays we want to loop through).</p>

<h2 id="translation-management">Translation Management</h2>

<p>Translations are managed through the <strong>Admin2</strong> dashboard. The interface is split into two primary backend sections:</p>

<h3 id="1-automatic-translation-generation">1. Automatic Translation Generation</h3>

<p>Not every translation exists when an application first requests it. When the translation API receives a dictionary, any missing keys for the target language are automatically queued for processing.</p>

<p>A background service batches these missing entries and sends them through our <strong>AI translation pipeline</strong>, powered by our internal AI infrastructure and Google Translate. The generated translations are then stored in the database for future use.</p>

<p>While a translation is being generated, the API simply falls back to the original English text, ensuring the UI remains fully functional. Once generated, future requests are served directly from storage.</p>

<h3 id="2-translations--languages-workflow">2. Translations &amp; Languages Workflow</h3>

<ul>
  <li><strong>Translations Section:</strong> This is where translation entries are reviewed and edited. Users can:
    <ul>
      <li>Search translation keys</li>
      <li>Edit translations for individual languages</li>
      <li>Identify missing translations</li>
      <li>Save only modified values</li>
      <li>Import translations in bulk</li>
      <li>Test translations directly against the API</li>
    </ul>
  </li>
  <li><strong>Languages Section:</strong> This section manages supported languages. Each language stores metadata such as:
    <ul>
      <li>Language code &amp; Display name</li>
      <li>AI router code
        <ul>
          <li>Character set &amp; RTL (Right-to-Left) configuration</li>
          <li>Auto-translation support</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>The system currently supports <strong>~200 languages</strong>, and the entire workflow—from key discovery to translation generation and storage—is fully automated.</p>

<h2 id="end-to-end-flow">End-to-End Flow</h2>

<p>Here is exactly what the completed developer and user workflow looks like:</p>

<pre><code class="language-txt">[Developer writes raw JSX]
       │
       ▼
 1. &lt;WuButton&gt;Save changes&lt;/WuButton&gt;
       │
       ▼ (Vite Build Phase via Babel AST)
 2. Plugin extracts text to `wick-ui-i18n.json`
    Component is rewritten to:
    &lt;WuButton&gt;&lt;WuTranslate __i18nKey="Save changes" /&gt;&lt;/WuButton&gt;
       │
       ▼ (Translations managed/automated through Orion)
 3. User opens the application in French
       │
       ▼ (WuTranslateProvider fetches keys)
 4. UI instantly renders:
    "Enregistrer les modifications"
</code></pre>

<p>The developer never created a translation key, never maintained a translation file, and never added any i18n plumbing. <strong>They simply wrote a button.</strong></p>]]></content><author><name>Salauddin Omar Sifat</name></author><category term="Software Engineering" /><category term="Frontend Architecture" /><category term="React" /><category term="Vite" /><category term="Localization (i18n)" /><category term="AI" /><category term="automation" /><category term="ast parsing" /><category term="babel" /><category term="build tools" /><category term="component library" /><category term="developer experience" /><category term="internationalization" /><category term="plugins" /><category term="react" /><category term="ui components" /><category term="vite" /><summary type="html"><![CDATA[We recently rolled out a new translation system across our applications. The goal was simple: if an app needs to support a new language, developers shouldn’t have to spend days wiring up translation files, managing keys, or updating components manually.]]></summary></entry><entry><title type="html">Stop Asking Claude to Agree With You</title><link href="https://www.questionpro.com/engineering/engineering/developer%20tools/ai%20&%20machine%20learning/stop-asking-claude-to-agree-with-you/" rel="alternate" type="text/html" title="Stop Asking Claude to Agree With You" /><published>2026-05-29T00:00:00+00:00</published><updated>2026-05-29T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/engineering/developer%20tools/ai%20&amp;%20machine%20learning/stop-asking-claude-to-agree-with-you</id><content type="html" xml:base="https://www.questionpro.com/engineering/engineering/developer%20tools/ai%20&amp;%20machine%20learning/stop-asking-claude-to-agree-with-you/"><![CDATA[<p><em>The best claude skill I’ve found writes no code. It just won’t let me start until I know what I’m building.</em></p>

<hr />

<h2 id="why-plan-mode-isnt-enough">Why Plan Mode isn’t enough</h2>

<p>For a long while, my default move with any non-trivial feature was Claude Code’s plan mode. Sketch the idea, let it draft a plan, skim it, hit go.</p>

<p>The cracks showed up fast.</p>

<ul>
  <li><strong>A fuzzy ask produces a fuzzy plan.</strong> The output looks structured and confident, but it’s quietly inheriting every gap in your thinking.</li>
  <li><strong>The agent drifts mid-execution.</strong> You approve a plan, switch to execute, and it quietly adds files and patterns the plan never mentioned. You end up appending <em>“implement exactly as written, nothing extra”</em> to every run.</li>
  <li><strong>It never challenges the idea itself.</strong> Plan mode answers <em>how</em> Claude would build your thing. It never asks whether the thing is worth building, or whether you’ve actually thought it through. You can burn three days producing a clean, well-structured mistake.</li>
</ul>

<p>That last one is the real gap. Plan mode assumes your idea is already sound and your only problem is execution. Most of the time, that assumption is wrong — and that’s the exact gap <code class="language-plaintext highlighter-rouge">/grill-me</code> fills.</p>

<hr />

<h2 id="the-skill-that-argues-back">The skill that argues back</h2>

<p>Matt Pocock — the TypeScript educator behind Total TypeScript — opened up his personal <code class="language-plaintext highlighter-rouge">.claude</code> directory as a public <code class="language-plaintext highlighter-rouge">skills</code> repo on February 3, 2026. Within three months it had crossed 50,000 GitHub stars, peaking near the top of GitHub’s trending charts. Not a framework, not a wrapper — just the actual markdown files he uses every day.</p>

<p>The repo targets one failure mode: <em>misalignment</em>. You think the agent understood you. Then you see what it built, and it clearly didn’t. The same communication gap you get with a human dev, now at machine speed.</p>

<p>The headline fix is <code class="language-plaintext highlighter-rouge">/grill-me</code>. It removes vagueness before any code exists, by interrogating your plan one decision at a time. The entire skill is three sentences:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Interview me relentlessly about every aspect of this
plan until we reach a shared understanding. Walk down
each branch of the design tree, resolving dependencies
between decisions one by one.
</code></pre></div></div>

<p>That’s it. No clever machinery — the value is entirely in being forced to answer.</p>

<p><strong>Getting it running:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx skills add mattpocock/skills
</code></pre></div></div>

<p>Then run <code class="language-plaintext highlighter-rouge">/setup-matt-pocock-skills</code> once per repo to wire up your issue tracker and config.</p>

<hr />

<h2 id="the-questions-you-didnt-want-to-answer">The questions you didn’t want to answer</h2>

<p>Take a real feature — per-workspace billing limits in a multi-tenant SaaS. You sketch the idea and type <code class="language-plaintext highlighter-rouge">/grill-me</code>.</p>

<blockquote>
  <p><strong>Claude:</strong> When a workspace hits its limit, does the whole workspace get blocked, or just new usage?</p>

  <p><strong>Me:</strong> Just new usage, I think.</p>

  <p><strong>Claude:</strong> Who gets notified — the user who triggered it, the workspace admin, or both? Through what channel?</p>

  <p><strong>Me:</strong> Admin. Probably email.</p>

  <p><strong>Claude:</strong> If the admin ignores that email and a user hits the wall mid-session, what’s the experience? Hard error? Degraded mode? Graceful message?</p>

  <p><strong>Me:</strong> …I hadn’t defined that.</p>

  <p><strong>Claude:</strong> That’s your entire degradation story. It shapes the API contract, the frontend, and what the billing page has to show. Let’s keep going.</p>
</blockquote>

<p>One rough idea. A handful of decisions that reshape the architecture <em>before</em> implementation, instead of surfacing as bugs three days in.</p>

<blockquote>
  <p><strong><em>Every answer you give in a grill session is a decision you won’t have to reverse in code review.</em></strong></p>
</blockquote>

<hr />

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

<p><img src="/engineering/assets/images/claude-grill-me-workflow.png" alt="The four-step workflow pipeline" /></p>

<p align="center"><sub><b>FIG. 01</b> · The four-step workflow.</sub></p>

<p>The whole thing runs as a single continuous session, each step feeding the next.</p>

<p><strong><code class="language-plaintext highlighter-rouge">/grill-me</code> → <code class="language-plaintext highlighter-rouge">/to-prd</code> → <code class="language-plaintext highlighter-rouge">/to-issues</code> → <code class="language-plaintext highlighter-rouge">/afk</code></strong></p>

<p>The rule that makes it work: <strong>never <code class="language-plaintext highlighter-rouge">/clear</code> between steps.</strong> The PRD skill leans on everything from the grill — your answers, your reasoning, the edge cases you named, the trade-offs you accepted. A PRD generated cold is a template. A PRD generated right after a grill is a spec with a point of view.</p>

<p>Tight PRD in, tight tickets out — <code class="language-plaintext highlighter-rouge">/to-issues</code> produces tasks with real context and acceptance criteria, not “implement auth.” Tight tickets mean <code class="language-plaintext highlighter-rouge">/afk</code> can execute without guessing.</p>

<p>There’s a second payoff that shows up later. The PRD and the issues are written artifacts — they live in your tracker, not just in a chat window. So when a session dies halfway, or you come back to the same feature a week later, you’re not reconstructing intent from memory. You point a fresh session at the PRD and the open issues, and it picks up with the full reasoning already in hand: what you decided, why, and what’s left. The grill happens once; the context it produces keeps paying out across every session that touches the feature.</p>

<blockquote>
  <p><strong><em>Output quality at the end is a direct function of honesty at the beginning.</em></strong></p>
</blockquote>

<hr />

<h2 id="when-the-grill-gets-hard-handoff">When the grill gets hard: <code class="language-plaintext highlighter-rouge">/handoff</code></h2>

<p>Deep grill sessions burn context. You’re well into one when you realize you need to prototype something or check how an existing schema behaves. The instinct is to do it right there in the same session.</p>

<p>Don’t.</p>

<p>Pocock also wrote <code class="language-plaintext highlighter-rouge">/handoff</code>, built for exactly this. It compresses the current session into a document a fresh agent can pick up — context, decisions, intent, suggested next steps. Your grill stays clean; the side quest gets its own window.</p>

<p>Two patterns:</p>

<ul>
  <li><strong>Fire and forget</strong> — something out of scope appears. Hand it to a fresh agent, return to the grill.</li>
  <li><strong>Grill → handoff → prototype → handoff back</strong> — validate an assumption in a throwaway session, bring the learning home.</li>
</ul>

<p>The harder the questions, the more context a grill accumulates — and the more valuable it becomes to protect it. <code class="language-plaintext highlighter-rouge">/handoff</code> is how you keep going hard without drowning the session.</p>

<blockquote>
  <p><strong><em>The deeper the grill, the more there is to lose by derailing it. A handoff lets you chase a tangent and come back to a session that’s still sharp.</em></strong></p>
</blockquote>

<hr />

<h2 id="skills-worth-adding-to-the-stack">Skills worth adding to the stack</h2>

<p><strong><code class="language-plaintext highlighter-rouge">/tdd</code></strong> — a red-green-refactor loop that builds one vertical slice at a time: failing test, minimal implementation, refactor. If your grill and PRD were thorough, the acceptance criteria already exist — this turns them into tests Claude has to satisfy before it can claim something works.</p>

<p><strong><code class="language-plaintext highlighter-rouge">/zoom-out</code></strong> — when you land in an unfamiliar stretch of code and can’t see how it fits the bigger picture, this pulls the agent up a level: it explains the section’s role, its dependencies, and how it connects to the rest of the system. Useful when you inherit a module, come back to old code, or need the lay of the land before changing anything.</p>

<p><strong><code class="language-plaintext highlighter-rouge">/grill-with-docs</code></strong> — a <code class="language-plaintext highlighter-rouge">/grill-me</code> variant that tests your plan against the existing domain model and codebase. The right choice when you’re extending a live system rather than starting fresh; it ties new decisions to what already exists and updates context docs inline.</p>

<hr />

<h2 id="beyond-the-grill-getting-claude-code-to-run-lean">Beyond the grill: getting Claude Code to run lean</h2>

<p>The grill is the star of the workflow, but it only pays off if the rest of your setup isn’t quietly bleeding tokens and context. These are the habits that keep Claude Code fast, cheap, and sharp around it.</p>

<p><strong>Run grill sessions on the strongest model; implement on a cheaper one.</strong></p>

<p>A grill is pure exploration — no file writes, no tool-call overhead — so a frontier model like Opus earns its cost here by asking sharper questions and catching contradictions a smaller model misses, and the token bill stays low because it’s just conversation. Once you cross into implementation — <code class="language-plaintext highlighter-rouge">/afk</code>, <code class="language-plaintext highlighter-rouge">/tdd</code>, execution — a model like Sonnet does the heavy lifting at a fraction of the price.</p>

<blockquote>
  <p><strong><em>Spend your best model where decisions are made, not where code is typed.</em></strong></p>
</blockquote>

<p><strong><code class="language-plaintext highlighter-rouge">.claudeignore</code> cuts your context bill in one edit.</strong></p>

<p>By default Claude pulls everything in your project into scope. A <code class="language-plaintext highlighter-rouge">.claudeignore</code> file (same syntax as <code class="language-plaintext highlighter-rouge">.gitignore</code>) stops the dead weight from auto-loading — excluding <code class="language-plaintext highlighter-rouge">.next/</code> alone can trim 30–40% off context in a Next.js repo. It’s the highest-leverage two minutes you’ll spend.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node_modules/
dist/
build/
.next/
__pycache__/
*.lock
.git/
*.db
</code></pre></div></div>

<p>This doesn’t stop Claude from reading these files when you explicitly ask — it just keeps them out of automatic exploration.</p>

<p><strong>Keep files under 200–300 lines.</strong></p>

<p>Agents read whole files. When one reads a 1,500-line file to find a 20-line function, it pays for 1,480 lines of noise — and reasons worse for it. This isn’t a prompt trick; it’s a codebase discipline that compounds across every read in a session.</p>

<blockquote>
  <p><strong><em>A large file is a tax every agent pays, every time it looks.</em></strong></p>
</blockquote>

<p><strong>Teach Claude to grep, not read — in <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>.</strong></p>

<p><code class="language-plaintext highlighter-rouge">CLAUDE.md</code> is the one file Claude reads at the start of every session, so it’s where your project’s standing rules live — stack, conventions, and the things you never want to re-explain. The highest-value rule you can put there: tell it which files to <em>search</em> instead of <em>read</em>. We have a translation-keys file with over 2,000 entries; reading it whole would torch the context budget on noise. So <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> says:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>grep in <span class="sb">`packages/survey-type-defs/src/appTranslation/AppTranslationKeyEnum.ts`</span>
— 2000+ keys, never read the full file
</code></pre></div></div>

<p>One line, and Claude stops pulling a giant file into context every time it needs a single key. Do this for every oversized-but-unavoidable file in your repo.</p>

<p><strong>Disable MCP servers you aren’t using.</strong></p>

<p>Historically, Claude Code would load MCP tool definitions directly into context, and a few heavy servers (Playwright, GitHub, Gmail, etc.) could quietly consume thousands of tokens each — and a sprawling setup, tens of thousands per turn. Recent Claude Code versions mitigate this with Tool Search, now enabled by default: tools are discovered and loaded on demand instead of shipping every schema upfront. It helps, but large MCP setups still add overhead and complexity.
Run <code class="language-plaintext highlighter-rouge">/context</code> to inspect what each server is costing you, and prune unused servers with <code class="language-plaintext highlighter-rouge">/mcp</code>.</p>

<p><strong>Compact early — around half-full, not when it breaks.</strong></p>

<p>Quality degrades as the window fills; by the time responses feel hazy, you’re already deep in the rot zone. The practical sweet spot is compacting around 50–60% capacity, while Claude still has full, uncompressed context to summarize from. Waiting until 90% means you’re summarizing an already-degraded view.</p>

<p><strong>Git worktrees for parallel agents.</strong></p>

<p>Independent features don’t need to run one after another. Worktrees give each agent its own branch and isolated directory off the same repo, so three agents can do in an hour what would take three sequentially — no context collisions, no merge chaos.</p>

<hr />

<h2 id="thinking-is-the-new-bottleneck">Thinking is the new bottleneck</h2>

<p>The shape of building software has flipped.</p>

<p>Writing code used to be the bottleneck — now it’s the part agents are best at. What’s left for us is deciding what’s actually worth building and being precise enough that the implementation doesn’t drift.</p>

<p>That’s why the highest-leverage skill in this workflow is <code class="language-plaintext highlighter-rouge">/grill-me</code> — a skill that generates no code at all, yet creates more leverage than anything else.</p>

<p>The leverage has moved upstream. Specificity, edge-case honesty, and tight context management matter more than raw output.</p>

<p>Everything else — lean context, smaller files, the right model for the right phase — is just there to protect that thinking from noise.</p>

<hr />

<p><em>The grill comes first. The code comes second.</em></p>]]></content><author><name>Akash Karyakarte</name></author><category term="Engineering" /><category term="Developer Tools" /><category term="AI &amp; Machine Learning" /><category term="claude code" /><category term="grill-me" /><category term="skills" /><category term="ai-coding" /><category term="workflow" /><category term="productivity" /><category term="context management" /><category term="developer experience" /><summary type="html"><![CDATA[The best claude skill I've found writes no code. It just won't let me start until I know what I'm building.]]></summary></entry><entry><title type="html">Rewrites Don’t Fail at Release. They Fail in What Happens After.</title><link href="https://www.questionpro.com/engineering/engineering%20leadership/software%20engineering/rewrites-dont-fail-at-release/" rel="alternate" type="text/html" title="Rewrites Don’t Fail at Release. They Fail in What Happens After." /><published>2026-05-13T00:00:00+00:00</published><updated>2026-05-13T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/engineering%20leadership/software%20engineering/rewrites-dont-fail-at-release</id><content type="html" xml:base="https://www.questionpro.com/engineering/engineering%20leadership/software%20engineering/rewrites-dont-fail-at-release/"><![CDATA[<p>Every rewrite inherits invisible debt — edge cases the old system silently handled for years, unknown to anyone, until they break.</p>

<p>The original application earned its battle scars over time. Every quirky conditional, every oddly specific validation, every silent fallback — they exist because something went wrong once, someone fixed it, and the fix outlived the memory of why it was needed. When you rewrite, you ship clean code into a world that remembers none of that.</p>

<p>That’s not a failure of planning. It’s the nature of the work.</p>

<hr />

<h2 id="the-edge-case-problem">The Edge Case Problem</h2>

<blockquote>
  <p><em>“You can’t engineer that risk away.”</em></p>
</blockquote>

<p>Legacy systems accumulate institutional memory in the codebase itself. A rewrite starts from specs, from documentation, from what people <em>think</em> the system does — not from what it <em>actually</em> does in the 0.1% of cases that never made it into a ticket.</p>

<p>Those cases exist. They will surface. Usually at the worst time, usually for the most demanding customer.</p>

<p><img src="/engineering/assets/images/02-legacy-code-complexity.jpg" alt="Legacy systems accumulate invisible complexity over years" /></p>

<p>The teams that pretend otherwise — that spend energy projecting confidence in a flawless launch — are the same teams that get caught flat-footed when the inevitable happens. No incident response playbook. No communication template. No clear owner. Just scrambling.</p>

<hr />

<h2 id="what-actually-makes-the-difference">What Actually Makes the Difference</h2>

<p>A bug report after a rewrite is not evidence that the rewrite was a mistake. It’s evidence that the system is being used — by real users, in real environments, doing things no test suite anticipated.</p>

<p>What matters is what happens next.</p>

<p><img src="/engineering/assets/images/03-debugging-response.jpg" alt="Debugging — the response to a bug matters more than the bug itself" /></p>

<p>The teams that come out of these situations stronger share a few things:</p>

<p><strong>1. They communicate fast, even when the answer is incomplete.</strong>
“We’re aware, we’re on it, here’s what we know so far” is infinitely better than silence. Silence reads as ignorance or indifference. Neither is a good look.</p>

<p><strong>2. They show genuine urgency — not performance.</strong>
There’s a difference between a team that <em>looks</em> busy and a team that’s actually moving. Customers and support teams can tell. So can your engineering manager.</p>

<p><strong>3. They support the people closest to the customer.</strong>
Your support team is absorbing the heat directly. If they’re left without context, without a timeline, without someone to escalate to — you’ve made their job impossible. That erodes internal trust as much as it erodes customer trust.</p>

<p><strong>4. They own it without hedging.</strong>
Not “there may have been an issue with the legacy data migration path.” Just: “We got this wrong. Here’s what happened, here’s the fix, here’s what we’re doing so it doesn’t happen again.”</p>

<hr />

<h2 id="trust-is-the-long-game">Trust Is the Long Game</h2>

<p><img src="/engineering/assets/images/04-leadership-trust.jpg" alt="Engineering leadership — trust is built in how you handle hard moments" /></p>

<p>A perfect release is a nice thing. It’s also rare, fragile, and mostly luck-dependent once you’re past a certain scale of complexity.</p>

<p>Trust isn’t built in the good moments. It’s stress-tested in the bad ones.</p>

<p>When your stakeholders, your customers, and your team watch how you respond to a post-release bug — they’re forming a judgment about you that will outlast that bug by years. Did you hide? Did you deflect? Did you communicate clearly and move fast?</p>

<p>The engineering teams worth working with, and the engineering leaders worth following, are the ones who’ve internalized this:</p>

<blockquote>
  <p><strong>The bug is a moment. Your response to it is a reputation.</strong></p>
</blockquote>

<hr />

<h2 id="a-note-on-rewrites-specifically">A Note on Rewrites Specifically</h2>

<p>Rewrites deserve some defense here. They are often the right call — technically and organizationally. Legacy systems accrue complexity that eventually becomes load-bearing in ways that block every other improvement. Rewrites unlock velocity, maintainability, and the ability to hire engineers who don’t need six months of tribal knowledge before they’re productive.</p>

<p>The answer isn’t to avoid rewrites out of fear of post-launch bugs. The answer is to:</p>

<ul>
  <li>Build a robust incident response process <em>before</em> you ship</li>
  <li>Prepare your customer-facing teams with context and escalation paths</li>
  <li>Communicate proactively with high-risk customer segments at launch</li>
  <li>Treat the first 30 days post-launch as a separate, high-attention phase</li>
</ul>

<p>And when bugs happen — because they will — <strong>own them completely</strong>.</p>

<hr />

<p><em>That ownership is what builds the trust that makes your next rewrite easier to get approved, easier to staff, and easier to land.</em></p>]]></content><author><name>Kapil Karandikar</name></author><category term="Engineering Leadership" /><category term="Software Engineering" /><category term="rewrites" /><category term="ownership" /><category term="engineering-leadership" /><category term="incident-response" /><category term="engineering-culture" /><summary type="html"><![CDATA[Every rewrite inherits invisible debt. What separates great engineering teams isn't a perfect launch — it's how they own the inevitable bugs that follow.]]></summary></entry><entry><title type="html">Truncating LLM Context Is Not Retrieval: Why slice(0, N) Was the Wrong Fix</title><link href="https://www.questionpro.com/engineering/engineering/ai%20&%20machine%20learning/software%20architecture/truncation-is-not-a-solution/" rel="alternate" type="text/html" title="Truncating LLM Context Is Not Retrieval: Why slice(0, N) Was the Wrong Fix" /><published>2026-05-11T00:00:00+00:00</published><updated>2026-05-11T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/engineering/ai%20&amp;%20machine%20learning/software%20architecture/truncation-is-not-a-solution</id><content type="html" xml:base="https://www.questionpro.com/engineering/engineering/ai%20&amp;%20machine%20learning/software%20architecture/truncation-is-not-a-solution/"><![CDATA[<p>If you are building anything that pipes a user’s history into an LLM prompt, this story is probably waiting for you too.</p>

<p>For about six weeks one of our services was sending every user’s entire interaction history into the prompt with no cap on size. For most users that was fine. For our power users it was three to five minutes per call, our upstream LLM gateway timing out, jobs piling up in the queue, and demos blowing up in real time.</p>

<p>We had a demo on Tuesday. On Monday we shipped this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="nx">_000</span><span class="p">)</span>
</code></pre></div></div>

<p>Latency went from minutes to two seconds. We made the demo. Six days later we replaced that one line with what should have been there all along: per-user semantic retrieval on Postgres and pgvector.</p>

<p>This post is about both halves of that story. Why the hotfix worked and was still wrong, and what the real solution looks like.</p>

<hr />

<h2 id="the-shape-of-the-problem">The shape of the problem</h2>

<p>Briefly, so the details below make sense: we run an internal service that generates survey responses on behalf of users, for cases where we need realistic-looking respondents to seed a test, validate a new flow, or reproduce an edge case. To make a generated answer sound like a specific person and not a generic one, the service feeds the LLM that user’s prior question-and-answer history as context, then asks the model to fill the current survey page in their voice.</p>

<p>That detail aside, the lesson in this post is general. Any system that pipes a user’s full history into an LLM prompt has the same shape. The history is unbounded, the model is paid per token, and the data path is silent about what part of the history actually matters for the question being asked.</p>

<p>Worth naming what we were doing, because the rest of the post turns on it. We were doing <strong>context stuffing</strong>, not retrieval. Every Q&amp;A pair the user had ever answered, joined together, sent to the model in one shot. The irony is not lost on us: the <em>retrieval</em> in “retrieval-augmented generation” is exactly what we did not have. We had stuffing. The rest of this post is the story of how we built the retrieval part, six weeks later, after the stuffing broke.</p>

<p>For a user with a small history (a handful of past surveys), the blob is a few KB and the model takes a second or two to come back. That was the steady state for most users. It was the state we shipped at, and for a while, it held.</p>

<hr />

<h2 id="how-we-ended-up-sending-megabyte-sized-prompts">How we ended up sending megabyte-sized prompts</h2>

<p>The original implementation took the path of least resistance: dump everything the system knew about a user into the prompt and let the model figure out what to do with it. Every past Q&amp;A pair, joined together with newlines, in whatever order the rows had landed in the database. No cap on size, no relevance filtering, nothing fancy.</p>

<p>For about six weeks it worked. Then we onboarded a long-tenured user group, with people who had answered hundreds of surveys each. Their context blobs blew past a megabyte. The largest one in production was 2.66 million characters.</p>

<p>You can probably guess what the LLM gateway did with that.</p>

<p>LLM calls on those users started taking three to five minutes. Some hit the upstream timeout. The background job queue feeding the LLM started backing up. And worst of all, we had product demos lined up where the customer was watching the queue drain in real time and asking why.</p>

<p>There was no real mystery about <em>what</em> was wrong. One log line was enough to confirm it:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">debug</span><span class="p">(</span><span class="s2">`prompt length: </span><span class="p">${</span><span class="nx">prompt</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="c1">// prompt length: 2660142</span>
</code></pre></div></div>

<p>2.66 million characters into a chat completion endpoint will take minutes, in any model, in any region. The model has to read it.</p>

<blockquote>
  <p><strong>The cheapest debugging step for a slow third-party call is logging the size of what you are sending.</strong> It is also the one most people skip. Payload size belongs in the same log line as the URL and the response time, in every HTTP client wrapper you write. You will get it back, in saved time, the first time something goes wrong.</p>
</blockquote>

<hr />

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

<p>We had less than 24 hours before the demo. The hotfix went in around lunchtime:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">MAX_MEMBER_CONTEXT_CHARS</span> <span class="o">=</span> <span class="mi">10</span><span class="nx">_000</span>

<span class="k">private</span> <span class="nf">truncateContext</span><span class="p">(</span><span class="nx">memberContext</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">surveyId</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">memberContext</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">MAX_MEMBER_CONTEXT_CHARS</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">memberContext</span>
  <span class="p">}</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span>
    <span class="s2">`[CONTEXT-TRUNCATE] surveyId=</span><span class="p">${</span><span class="nx">surveyId</span><span class="p">}</span><span class="s2">: memberContext </span><span class="p">${</span><span class="nx">memberContext</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> chars truncated to </span><span class="p">${</span><span class="nx">MAX_MEMBER_CONTEXT_CHARS</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">)</span>
  <span class="k">return</span> <span class="nx">memberContext</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">MAX_MEMBER_CONTEXT_CHARS</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">slice</code>, a warning log so we could measure how often it tripped, and a <code class="language-plaintext highlighter-rouge">MAX_MEMBER_CONTEXT_CHARS</code> constant so the magic number had a name. End-to-end LLM latency went from three to five minutes back down to roughly two seconds. The demo went out the door.</p>

<p>Nobody on the team thought this was the answer. The PR description said in writing that this was a stopgap to keep our LLM path responsive, and that a proper retrieval implementation was the next ticket. But it bought us the breathing room to build that properly instead of in a panic.</p>

<p>This is also where most teams stop. The fire is out, the metric is green, the velocity board moves. The thing that made us not stop is that we already knew what was wrong with the hotfix, because it was obvious the moment we wrote it.</p>

<hr />

<h2 id="why-slice0-n-is-not-retrieval">Why slice(0, N) is not retrieval</h2>

<p>The blob we were truncating was stitched together in <strong>insertion order</strong>: the order Q&amp;A pairs happened to land in the database, which is roughly the chronological order in which the user took the surveys.
For our heaviest user, the first 10 KB was answers from surveys taken years ago. The page we were generating a response for was a recent satisfaction tracker.
The Q&amp;A pairs that would have actually helped the model, that user’s recent answers on the same product surface, sat at character offset 1.4 million. The hotfix was clipping them off and feeding the model old, mostly unrelated history.</p>

<p>To be honest about what we know and don’t know here: we did not measure a quality regression in production. We do not have an automated eval on generated-answer quality yet.
What we have is the structural argument, which is enough to take seriously. If the data you are clipping has no order with respect to the question being asked, the first N bytes of it are statistically no different from a random sample of N bytes.
The model is not failing in that case. It is succeeding loudly on the wrong input. That is a worse failure mode than slow latency, because there is no graph that lights up when it happens.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Unbounded prompt</th>
      <th>After hotfix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LLM latency</td>
      <td>3-5 min on power users</td>
      <td>1-3s across the board</td>
    </tr>
    <tr>
      <td>Context the model saw</td>
      <td>Everything, including the right pairs</td>
      <td>The oldest 10 KB, regardless of relevance</td>
    </tr>
    <tr>
      <td>What is at risk</td>
      <td>Throughput, visibly</td>
      <td><strong>Answer relevance, invisibly</strong></td>
    </tr>
    <tr>
      <td>Failure mode</td>
      <td>Loud, on every graph</td>
      <td>Silent, on no graph</td>
    </tr>
  </tbody>
</table>

<p>The dangerous column is the right one. The hotfix turned a problem we could see into a problem we couldn’t. That is a fine trade for two days while you ship a demo. It is not a fine steady state.</p>

<blockquote>
  <p><strong>Truncation is not a solution, it is an incident in disguise.</strong> It silences a metric you watch (latency) by creating a regression in something you don’t (answer quality). If the data you are clipping has no order relative to the query, slicing the first N bytes of it is no different statistically from picking N bytes at random. The fix is not a bigger N.</p>
</blockquote>

<hr />

<h2 id="the-right-question">The right question</h2>

<p>Under pressure on Monday we asked the small question: <em>how do we make this prompt smaller, today, in time for the demo?</em> Truncation answered it.</p>

<p>The bigger question was the one we walked into the design doc with: <em>which part of this user’s history is actually relevant to the page we are filling right now?</em> The honest answer to that is not “the first N bytes.” It is “the chunks that semantically match the question being asked.” The first is truncation. The second is retrieval. They look similar from outside. They behave nothing alike.</p>

<p>But before we describe the retrieval design, the more important framing: <strong>retrieval is not the default path.</strong> Most users in our system have small context blobs, a few KB of past answers. Sending the whole thing to the LLM works fine. There is no reason to embed, store, query, and reconstruct context for a user whose entire history would fit in a tweet.</p>

<p>The honest design is: keep the legacy blob path for everyone whose context fits inside the model’s effective attention window. Only build the retrieval pipeline for the heavy users where that path breaks. The threshold we landed on is <strong>200K tokens</strong> of user context, measured with <code class="language-plaintext highlighter-rouge">tiktoken</code> rather than <code class="language-plaintext highlighter-rouge">length / 4</code> since token counts vary 2-5× by content. Below that, the model gets the whole blob. Above it, retrieval kicks in.</p>

<p>This matters because it changes what the system looks like. We are not building a RAG system. We are building a <strong>gated escape hatch</strong> for the long tail of users whose history is too big for the cheap path. The 90%+ of users who stay under the threshold never touch the new infrastructure. Storage cost, embedding API cost, retrieval latency, all of it scales with the small fraction of heavy users, not with the whole user base.</p>

<p>The shape of the retrieval path itself, for heavy users:</p>

<ol>
  <li>At sync time, if the user’s blob crosses 200K tokens, split their Q&amp;A pairs into chunks of 10 (chronological order). Store one row per chunk in the vector table.</li>
  <li>A background worker fills in embeddings for each new chunk asynchronously.</li>
  <li>At fill time, embed the question on the current page, run a cosine similarity search over that user’s chunks, take top 5, send those into the prompt.</li>
</ol>

<p>We will come back to <em>why</em> 10 pairs per chunk and not 1 or 100 in a moment. That’s the part the design discussion actually argued about.</p>

<hr />

<h2 id="why-pgvector-not-a-new-vector-database">Why pgvector, not a new vector database</h2>

<p>We already run Postgres. Postgres has <a href="https://github.com/pgvector/pgvector">pgvector</a>. Pulling in a new vector database (Pinecone, Weaviate, Qdrant, whatever the current favourite is) for a single feature is the kind of architectural decision that looks free on a slide and is anything but free six months later in oncall. So we built on what we already operate.</p>

<p>Embeddings come from OpenAI’s <code class="language-plaintext highlighter-rouge">text-embedding-3-small</code> at 1536 dimensions, which is what the <code class="language-plaintext highlighter-rouge">vector(1536)</code> column type below is sized for. The model is cheap, fast, and accurate enough for our use case. We are not married to it; the column dimension is the only place the choice leaks into the schema, and a future migration to a different model is a backfill, not a rewrite.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">panel_member_answer_vectors</span> <span class="p">(</span>
  <span class="n">id</span> <span class="nb">SERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">synced_panel_member_id</span> <span class="nb">INT</span> <span class="k">NOT</span> <span class="k">NULL</span>
    <span class="k">REFERENCES</span> <span class="n">synced_panel_members</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">ON</span> <span class="k">DELETE</span> <span class="k">CASCADE</span><span class="p">,</span>
  <span class="n">chunk_text</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>      <span class="c1">-- concatenation of 10 Q&amp;A pairs</span>
  <span class="n">pair_count</span> <span class="nb">INT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>        <span class="c1">-- how many pairs are in this chunk</span>
  <span class="n">embedding</span> <span class="n">vector</span><span class="p">(</span><span class="mi">1536</span><span class="p">),</span>
  <span class="n">created_at</span> <span class="nb">TIMESTAMP</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>
  <span class="k">UNIQUE</span> <span class="p">(</span><span class="n">synced_panel_member_id</span><span class="p">,</span> <span class="n">chunk_text</span><span class="p">)</span>
<span class="p">);</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">idx_pmav_member</span> <span class="k">ON</span> <span class="n">panel_member_answer_vectors</span> <span class="p">(</span><span class="n">synced_panel_member_id</span><span class="p">);</span>
</code></pre></div></div>

<p>Three things in that schema are deliberate and worth calling out.</p>

<p><strong>No HNSW index, on purpose.</strong> HNSW is the canonical pgvector index and the default advice in any RAG blog post. We did not use it. The reason is the <em>shape</em> of our query, not the size of our data.
We never search across all users, we only search within one user. A B-tree on <code class="language-plaintext highlighter-rouge">synced_panel_member_id</code> narrows the result set to that one heavy user’s chunks, and then a sequential cosine comparison over that bounded set runs in single-digit milliseconds.
At our heaviest user today the set is tiny; at the ceiling we are sizing for, it tops out around 2,000 chunks per user. Either way the cosine pass stays in memory and stays fast.
HNSW would add build time, write amplification, and a real tuning surface (<a href="https://www.postgresql.org/about/news/pgvector-080-released-2952/">pgvector tuning is a topic of its own</a>), and at our query shape it would not be faster. We will add it the day telemetry says we need it. Not before.</p>

<p><strong>UNIQUE on (synced_panel_member_id, chunk_text), with a side benefit.</strong> When we ran the migration to populate the new table, the constraint deduped a sync bug we didn’t know we had: our heaviest user had 78 duplicate Q&amp;A pairs out of 127 stored rows.
The blob format had hidden it the entire time, because duplicates inside one giant joined string just look like one long string of repeated text. The constraint did the cleanup as a side effect of being correct.
The upstream sync bug that produced the duplicates is filed separately and is being fixed at the source; the constraint stays regardless, because defensive layers like this are the difference between a one-time data wash and a recurring oncall page.
A reminder that schema constraints are not just gates against bad input. They are also flashlights, pointed at dirty data that is already there.</p>

<p><strong>Nullable embedding column.</strong> Chunks are inserted synchronously as part of the user-sync transaction. Embeddings are filled in asynchronously by a background worker that batches calls to the embedding provider. This separates two concerns that have no business being in the same transaction: the durable record of what the user answered, and the derived index that lets us search it. If the embedding provider is down, sync still works. Retrieval falls through to the legacy blob path until the worker catches up. The system degrades, it does not break.</p>

<hr />

<h2 id="a-flag-not-a-query-how-the-system-knows-which-path-to-take">A flag, not a query: how the system knows which path to take</h2>

<p>Here is the question the gated design forces you to answer: at fill time, how does the code know whether <em>this</em> user is a small-blob user or a heavy retrieval user?</p>

<p>The naive answer is “query the vector table and see if rows exist.” That works, and it is wrong. It pays a DB roundtrip for every single response generation, including the 90%+ of calls that will immediately turn around and use the blob anyway. Multiply that by every page of every survey for every user in a run, and the “small users are free” claim quietly stops being true.</p>

<p>The honest answer is to store the state where it is needed. We added one column to the existing user row:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">is_chunked</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="c1">// default false</span>
</code></pre></div></div>

<p>The flag is set to <code class="language-plaintext highlighter-rouge">true</code> at sync time, the moment the user’s blob crosses the 200K-token threshold. The row was already being loaded for the response generation anyway, so the flag arrives for free. Retrieval becomes a single in-memory check:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">member</span><span class="p">.</span><span class="nx">isChunked</span><span class="p">)</span> <span class="k">return</span> <span class="nf">legacyBlob</span><span class="p">(</span><span class="nx">member</span><span class="p">.</span><span class="nx">context</span><span class="p">);</span>
<span class="c1">// only heavy members reach this line</span>
<span class="k">return</span> <span class="nf">semanticRetrieval</span><span class="p">(</span><span class="nx">member</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="nx">queryEmbedding</span><span class="p">);</span>
</code></pre></div></div>

<p>One deliberate semantic: the flag is <strong>sticky-true</strong>. Once a user becomes chunked, they stay chunked, even if a future resync drops them below 200K tokens. The reasoning is part sunk cost (the chunks are already vectorized, the embedding bill is already paid), part state consistency (no edge case where rows exist in the vector table but the flag is telling fill-time to use the blob). The trade-off is that a user cannot be un-chunked through the normal resync flow. Only deleting and re-creating the user row resets the state. We made peace with that.</p>

<p>The bigger principle: <strong>store state explicitly, don’t recompute it.</strong> A boolean that is set once at write time beats a DB query on every read.
Reading code that says <code class="language-plaintext highlighter-rouge">if (!member.isChunked)</code> tells the next engineer exactly what is going on at this branch in the design. Reading code that says <code class="language-plaintext highlighter-rouge">if ((await countVectorRows(memberId)) === 0)</code> tells them nothing about <em>why</em> the system is asking that question, and quietly costs an extra network roundtrip on every fill.
The boolean is not faster by accident; it is faster because it is the right shape for the decision being made.</p>

<hr />

<h2 id="the-grouping-decision-per-pair-per-survey-or-per-n">The grouping decision: per-pair, per-survey, or per-N</h2>

<p>This was the longest discussion in the design. Three options were on the table, and the <em>interesting</em> part of the design is which one we picked and why the obvious choices were wrong.</p>

<h3 id="per-pair-one-qa--one-row">Per-pair (one Q&amp;A = one row)</h3>

<p>The first instinct, and the one most RAG tutorials show you. Embed each Q&amp;A pair on its own, retrieve top-K pairs.</p>

<p>It dies on the row count math at scale. We are not a 1000-user app. A user group can have 90K users, and a heavy user can have 20K answers across years of activity.
Per-pair grouping puts that at <strong>1.8 billion rows</strong> in a single Postgres table, with around 11 TB of vector data. Postgres can technically store that. We do not want to operate it.
Vacuum behaviour, replication lag, backup windows, index rebuild times: every one of those gets unhealthy fast at that row count, and none of them is the failure mode you want for a feature that is supposed to be an “escape hatch.”</p>

<p>Per-pair also pays embedding cost per pair. At a million pairs across a user base, that is a million embedding API calls at sync time. The cost is real money even at OpenAI’s small-model pricing, and the throughput is bounded by the embedding API’s rate limit. None of this is unsolvable, but it is all overhead being paid in exchange for a granularity the retrieval doesn’t need.</p>

<h3 id="per-survey-one-surveys-worth-of-answers--one-row">Per-survey (one survey’s worth of answers = one row)</h3>

<p>The next-most-obvious idea, and intuitively appealing. A survey is a natural topic boundary. NPS surveys are about loyalty, demographic surveys are about who the person is, product surveys are about features. Grouping by survey preserves topic. No magic number to tune.</p>

<p>It dies on two real problems:</p>

<ol>
  <li>
    <p><strong>Similarity score dilution.</strong> A survey covers many sub-topics. A satisfaction tracker might have a Net Promoter question, a product feedback question, a competitor comparison, and a demographic question, all in one survey.
Embedding all of those into one vector averages the topic signal out. When the page asks something specific (“how satisfied with response time”), the per-survey chunk’s embedding is generic and doesn’t match strongly.
The cosine search produces lower scores across the board, which is exactly the failure mode you don’t want from a retrieval system.</p>
  </li>
  <li>
    <p><strong>Survey size is unbounded.</strong> A QuestionPro survey can have 500 questions. Some have 5000. A 500-question survey at ~150 chars per answer is 75K characters in one row, which is <em>past the 8192-token input limit</em> of most embedding models. We’d have to chunk anyway. So we’re not actually grouping by survey, we’re chunking inside surveys and pretending the boundary matters. It doesn’t.</p>
  </li>
</ol>

<h3 id="per-n-pairs-we-landed-on-n10">Per-N pairs (we landed on N=10)</h3>

<p>The middle ground. Take a user’s pairs in chronological order and group them in fixed batches of 10. One row per batch. ~1500 characters per chunk on average. Bounded chunk size, bounded row count, embedding input always inside the model’s limit.</p>

<p>The trade-off is real: a chunk of 10 chronologically-adjacent pairs may contain mixed topics. Mixing two or three topics in 1500 characters still produces a meaningful embedding (the topics are at least adjacent in the user’s timeline), but it’s not as clean as one topic per chunk would be. We chose this anyway. The reason: the <em>alternatives</em> are worse. Per-pair scales catastrophically. Per-survey loses signal for surveys that mix topics, which is most of them, and breaks on size for the large ones.</p>

<p>10 is not magic. It is the smallest number where the embedding has enough text to carry topic signal <em>and</em> where the row count stays in single-digit millions instead of single-digit billions at scale.
It is a tuning parameter, and we will revisit it. To revisit it well, we need something we do not have yet: an automated eval on generated-answer quality.
The next thing this work unlocks is exactly that. Once retrieval is doing something other than “use the blob,” we finally have two variants to compare, and a reason to build the eval. That is its own ticket, and probably its own post.</p>

<h3 id="the-other-rejections-briefer-because-theyre-the-standard-ones">The other rejections (briefer, because they’re the standard ones)</h3>

<p><strong>BM25 / Postgres full-text search.</strong> Free and tempting, but <code class="language-plaintext highlighter-rouge">plainto_tsquery</code> is AND-default and stems aggressively. A page asking <em>“how satisfied with our services”</em> tokenizes to <code class="language-plaintext highlighter-rouge">'satisfi' &amp; 'servic'</code>, returns zero matches against a user who had answered five surveys about “AI advancement satisfaction.” Survey questions paraphrase across surveys (“how likely to recommend” → “would you suggest us to a friend”). Handling paraphrase is the <em>reason</em> to do semantic retrieval, and BM25 cannot.</p>

<p><strong>JSONB array column on the existing users table.</strong> Tempting because it avoids a new migration. But pgvector’s <code class="language-plaintext highlighter-rouge">&lt;=&gt;</code> cosine operator works at the row level, not the array-element level. An array column forces <code class="language-plaintext highlighter-rouge">UNNEST</code> per query → no usable index → sequential scan of the entire users table on every retrieval.</p>

<p><strong>Reuse the existing <code class="language-plaintext highlighter-rouge">question_vectors</code> table with a discriminator column.</strong> Polymorphic schema with nullable foreign keys and branching logic in every query. “One fewer table” is a benefit that lasts a sprint. Polymorphism is a maintenance cost that compounds for years.</p>

<p><strong>HNSW from day one.</strong> Right index, wrong workload. Per-user B-tree filter + in-memory cosine over a few thousand vectors beats HNSW for our query shape until row count per user grows another order of magnitude. We’ll add it when telemetry says we need it.</p>

<blockquote>
  <p>Vector retrieval has a stack of canonical defaults: HNSW indexes, per-pair embeddings, recursive chunking, hybrid keyword-plus-semantic scoring. Each is correct for some scale and some query shape. None is correct for all of them. The canonical architecture is canonical for someone else’s problem. Measure your data, <em>and run the row count math at the scale you actually operate at</em>, before you copy it.</p>
</blockquote>

<hr />

<h2 id="k5-not-k10-or-k20">K=5, not K=10 or K=20</h2>

<p>Once retrieval is working, the next instinct is to be generous with K. More context is more information, right?</p>

<p>Wrong, and it took some reading to convince me of it. The current production-RAG research (<a href="https://lushbinary.com/blog/rag-retrieval-augmented-generation-production-guide/">Lushbinary’s 2026 guide</a> summarizes it well) keeps finding the same thing: LLM accuracy degrades as input grows.
The model spreads its attention across whatever you give it. If five of your ten retrieved passages are noise, the five good ones do worse than they would alone, because the model is now also reasoning about the noise.
Smaller K with higher-precision results beats larger K with looser thresholds.</p>

<p>That was the second time on this project we ran into the same fact in a different costume. Truncation hid the relevant data behind a length cap. Over-large K drowned it in distractors. The lesson is the same lesson: <em>less input, more accurate output</em>, as long as the input you keep is the right input.</p>

<hr />

<h2 id="what-the-new-path-costs">What the new path costs</h2>

<p>Because the retrieval pipeline only runs for heavy users, the cost numbers are nothing like “build RAG for the whole user base.” From the data we have in production today, roughly 5-10% of users cross the 200K-token threshold. That estimate will sharpen as we roll out and see real traffic distribution, but the order of magnitude is right.</p>

<p>The sizing below is for the <strong>ceiling we are designing for</strong>, not the heaviest user in production today. Today’s heaviest is small (a few dozen distinct Q&amp;A pairs, after the UNIQUE constraint deduped them). The numbers that follow describe what happens when a fully active group of long-tenured users shows up. That is the case the design has to survive.</p>

<ul>
  <li><strong>Eligible users:</strong> for a 90K-user group, somewhere between 5K and 10K cross the threshold. The rest stay on the legacy blob path with zero infrastructure changes: no row in the vector table, no embedding call, no extra latency.</li>
  <li><strong>Storage:</strong> at the ceiling we are sizing for, a heavy user with ~20K answers produces ~2K chunks at 10 pairs each. At 1536-dim float32, that is ~12 MB per heavy user, ~60-120 GB total for the heavy tail of a large user base. Postgres handles that easily on standard storage.</li>
  <li><strong>Embedding API cost:</strong> one async call per new chunk at sync time, batched. At fill time, the query embedding is cached per page, so the <em>same</em> page being filled for N heavy users costs one embedding call total, not N.</li>
  <li><strong>Retrieval latency:</strong> single-digit milliseconds per query. The B-tree filter narrows to one user’s chunks; the cosine pass runs over a few thousand vectors in memory.</li>
  <li><strong>End-to-end LLM latency on heavy users:</strong> two to three seconds. Same as the truncation hotfix was giving us, except the model is now being handed the <em>right</em> context instead of an arbitrary prefix of the wrong context.</li>
  <li><strong>End-to-end LLM latency on small users:</strong> unchanged. They never enter the retrieval path.</li>
</ul>

<p>The legacy blob is the fallback for two different reasons. For small users it is the <strong>default</strong>, because it is cheaper, simpler, and has no embedding-worker dependency.
For heavy users it is the <strong>safety net</strong>: if retrieval returns zero rows, or the embedding worker is behind, the call falls through to the old behaviour rather than failing.
Worst case is what we shipped before the hotfix, which is a worst case we have already lived through. There is no regression path we can introduce by rolling this out. Only an upside path we can lose if rollout exposes something we missed.</p>

<hr />

<h2 id="three-things-worth-carrying-out-of-this">Three things worth carrying out of this</h2>

<p>If the rest is too long to remember, these are the three.</p>

<p><strong>Log the payload size on every external call.</strong> Especially LLM calls. Especially the ones where the payload is “everything we know about this user.” The cost of always having the number is zero. The cost of not having it the first time something goes slow is hours.</p>

<p><strong>Treat <code class="language-plaintext highlighter-rouge">slice(0, N)</code> on an LLM prompt as a hotfix, not a solution.</strong> It is a perfectly reasonable thing to ship on a Monday afternoon to keep a demo alive. It is not a thing to leave in place for a quarter. If the data you are cutting has no order relative to the question being asked, the first N bytes are statistically no better than a random N bytes, and the model will use them anyway, quietly, without telling you. The fix is not a bigger N, the fix is a smaller, more relevant prompt.</p>

<p><strong>Measure your own data before copying a reference architecture.</strong> HNSW indexes, recursive chunking, hybrid keyword-plus-semantic scoring, large-K retrieval. Each of these is a correct default for somebody’s workload, but not automatically for yours. Our per-user B-tree filter followed by an in-memory cosine pass over a handful of vectors looks “wrong” against a textbook RAG diagram, and is the right answer for our query shape. Yours might be different. Read your data first, then pick the architecture.</p>

<h2 id="where-this-is-going">Where this is going</h2>

<p>The retrieval pipeline is rolling out behind a feature flag. The legacy truncation helper is still there underneath as a safety net. Once we have a few weeks of production data showing the fallback rate stays below our threshold, the blob column and the truncation helper both come out together.</p>

<p>The other consumer of the same context blob (a conversational AI surface elsewhere in our stack) currently has <em>no</em> truncation at all, and quietly exceeds the model’s token limit on long-tail users. Same retrieval call fixes it. The API is already built, it just needs to be wired in.</p>

<p>If your system pipes user history into an LLM prompt and your only defence against runaway prompt size is a length cap, you are probably shipping the same bug we were. The fix is not a bigger cap. The fix is a smaller, more relevant prompt.</p>]]></content><author><name>Amar Gupta</name></author><category term="Engineering" /><category term="AI &amp; Machine Learning" /><category term="Software Architecture" /><category term="llm" /><category term="rag" /><category term="pgvector" /><category term="retrieval" /><category term="embeddings" /><category term="postgres" /><category term="system design" /><category term="debugging" /><summary type="html"><![CDATA[We capped a 2.6M-character LLM prompt with slice(0, 10000). Latency dropped, dashboards turned green, and the model started answering with the wrong context. Here is what we replaced it with using Postgres + pgvector.]]></summary></entry><entry><title type="html">One Nav at a Time: How We Stopped Feeding the Legacy and Started Replacing It</title><link href="https://www.questionpro.com/engineering/engineering/software%20architecture/legacy%20migration/strangler%20fig/nestjs%20-typescript/app%20services/stop-feeding-the-lacy-start-replacing/" rel="alternate" type="text/html" title="One Nav at a Time: How We Stopped Feeding the Legacy and Started Replacing It" /><published>2026-04-30T00:00:00+00:00</published><updated>2026-04-30T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/engineering/software%20architecture/legacy%20migration/strangler%20fig/nestjs%20-typescript/app%20services/stop-feeding-the-lacy-start-replacing</id><content type="html" xml:base="https://www.questionpro.com/engineering/engineering/software%20architecture/legacy%20migration/strangler%20fig/nestjs%20-typescript/app%20services/stop-feeding-the-lacy-start-replacing/"><![CDATA[<p>In software engineering, the most dangerous word is <em>“rewrite.”</em> Complete rewrites are high-risk, expensive, and often fail to deliver value until the very end. In our case, we chose a little different path: the <strong>Strangler Fig Pattern</strong> — a methodical approach to building a modern system around the edges of a legacy monolith until the new architecture eventually becomes the host.</p>

<h2 id="-what-is-the-strangler-fig-pattern">🌱 What Is the Strangler Fig Pattern?</h2>

<p>The name comes from the strangler fig tree — a plant that germinates in the canopy of a host tree and slowly grows downward, wrapping around the trunk until it becomes a self-supporting structure. The host tree doesn’t get cut down. It simply becomes less and less essential as the fig takes over, until one day it’s hollow and the fig is the only thing left standing.</p>

<p>Applied to software, the pattern works like this: you don’t rewrite the legacy system — you build alongside it. New features go into the new system. Old modules get migrated one at a time, with a routing layer directing traffic to whichever implementation is live for each surface. The legacy codebase shrinks with every migration. The new system grows. At no point is the old system taken offline mid-flight, and at no point does the business have to wait for a complete rewrite to ship.
The risk is distributed across months of incremental work instead of concentrated in a single cutover event.</p>

<p><img src="/engineering/assets/images/strangler-fig-pattern.png" alt="strangler fig pattern" /></p>

<hr />

<h2 id="-the-whack-a-mole-reality">🔨 The “Whack-a-Mole” Reality</h2>

<p>Before this migration, our legacy CX system was a massive Java Struts monolith with over 1,000 files and a hand-rolled JDBC layer. It lacked an ORM — every query was hand-mapped SQL with no type safety and no enforced schema ownership.</p>

<p>We lived in a <strong>“Whack-a-Mole” culture.</strong> — fix the reminder logic in Send Flow, watch the Analytics counts drift. Tighten a validation rule in Admin Setup, and a Deploy Survey action you’d never heard of starts throwing NullPointerException in production because it had silently depended on the looser behaviour for years. We weren’t slow because we were careless. We were slow because the system punished speed.</p>

<hr />

<h2 id="-the-catalyst-centralised-root-cause-module">⚡ The Catalyst: Centralised Root Cause Module</h2>

<p>Sometimes back, we hit a turning point. We needed to build a new feature: <strong>Centralized Root Cause (CRC)</strong> — a complex churn-risk analyser. CRC wasn’t uniquely complex — we’d built harder things in the monolith before. The feature was a natural seam: a new surface, no legacy entanglement, a clean API boundary. We used it as the forcing function to stop feeding the monolith and start replacing it. Adding them to the monolith was a recipe for disaster and more cleanups later.</p>

<p>Instead, we chose to break this pattern and launched a new repository - Version2 — with a new tech stack and a strategic architectural hard stop:</p>

<blockquote>
  <p><strong>The Nav-Bar Mandate</strong>
If a new feature requires a navigation bar, it must live in the new tech stack (NestJS + React) in the Version2 repo. <strong>No new nav bars are to be added in legacy code.</strong> This single rule became our architectural enforcement mechanism.</p>
</blockquote>

<hr />

<h2 id="️-phase-1--wrapping-the-legacy">🏗️ Phase 1 — Wrapping the Legacy</h2>

<p>We didn’t pivot to a complex microservices mesh. Instead, we adopted an <strong>App-Service architecture</strong> — isolating specific business domains into focused, manageable services that communicate clearly while sharing necessary infrastructure.</p>

<p>The Strangler Fig works by wrapping the old system. We used our <strong>Navigation Bar as the routing layer</strong>. To the user, the experience is seamless. Behind the scenes, we route traffic between V1 (the legacy monolith) and V2 (Version2). When a module has migrated, the corresponding Struts action redirects the request to Version2’s API. <strong>Version2 then handles authentication and authorisation independently</strong> — validating the session, verifying permissions, and serving the response.</p>

<h3 id="the-three-phases">The Three Phases</h3>

<p><img src="/engineering/assets/images/three-phases-of-migration.png" alt="thress phases of migration" /></p>

<p><strong>Phase 1 — V1 only</strong>
The starting state. Every client request goes directly to the legacy system — no routing layer, no abstraction. Admin Setup, Send Flow, and Analytics all live inside the same deployable, sharing the same JDBC layer and the same database. There is no seam to exploit. The monolith is the entire system.</p>

<p><strong>Phase 2 — Transition</strong>
This is where the Strangler Fig actually begins. A Nav Bar built on the Struts facade is introduced as the routing layer — the single seam between legacy and new. The client never talks to V1 or V2 directly; it talks to the nav bar, which decides where to send the request.
V1 continues running unchanged — Admin Setup, Send Flow, and Analytics still live in the legacy WAR and handle their existing traffic. Meanwhile, V2 comes online alongside it, initially serving only the new modules: Root Cause, Customer 360. The two systems coexist in production. This is intentional, not temporary — Phase 2 is the stable operating state for the bulk of the migration.</p>

<p><strong>Phase 3 — V2 Target</strong>
The end state isn’t fully realised yet — this is where we’re headed. The nav bar thins from a Struts facade into a lightweight proxy, its only job being to route requests to the right Version2 module. V1 is still running, but its surface area keeps shrinking.
The work currently in flight: moving Deploy and Dashboard out of Analytics in the legacy WAR and into their own modules in Version2. Once those land, the module hierarchy in V2 reflects how the product actually works — Root Cause, Dashboard and Deploy as independent modules. The monolith doesn’t disappear in a single moment. It just runs out of things to do.</p>

<hr />

<h2 id="️-the-technical-standard">⚙️ The Technical Standard</h2>

<p>Moving to a new stack wasn’t just a language change — it was a shift to a <strong>modern service standard</strong> that made the failure modes of the old system structurally impossible.</p>

<h3 id="the-4-layer-data-flow">The 4-Layer Data Flow</h3>

<p><img src="/engineering/assets/images/modern-standard-service.png" alt="modern standard service" /></p>

<blockquote>
  <p>No module may access another module’s Repository directly. All cross-module calls go through Services (Facade Pattern).</p>
</blockquote>

<h3 id="what-this-means-in-practice">What this means in practice</h3>

<ul>
  <li><strong>4-Layer Data Flow:</strong> <code class="language-plaintext highlighter-rouge">Controller → Service → Repository → Entity</code>. Controllers handle HTTP only. All business logic lives in Services. All DB queries use parameterized TypeORM QueryBuilder — no raw SQL, no hand-mapped JDBC.</li>
  <li><strong>Facade Pattern:</strong> No module may access another module’s repository directly. Cross-module interaction goes exclusively through the exposed Service. This is the structural fix for the Whack-a-Mole problem.</li>
  <li><strong>DTO Validation:</strong> All inbound payloads validated using <code class="language-plaintext highlighter-rouge">class-validator</code> at every entry point. Magic string bugs and unvalidated inputs never reach business logic.</li>
</ul>

<hr />

<h2 id="-a-tale-of-two-eras">📊 A Tale of Two Eras</h2>

<table>
  <thead>
    <tr>
      <th>Activity</th>
      <th>V1 — The Monolithic Era</th>
      <th>V2 — The App-Service Era</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Finding a bug</strong></td>
      <td>Deep-sea diving in “Spaghetti” 🍝 — trace spans 3 modules, 1 log stream, no IDs</td>
      <td>Surgical logging within a specific service 🎯 — structured logs, trace IDs, Prometheus metrics</td>
    </tr>
    <tr>
      <td><strong>Adding a feature</strong></td>
      <td>High risk of breaking unrelated pipelines 🙏 — shared tables, no ownership</td>
      <td>A localised update to a dedicated domain ✉️ — module owns its schema and CI/CD</td>
    </tr>
    <tr>
      <td><strong>Documentation</strong></td>
      <td>Deciphering code comments from 2015 📜 — no contracts, no structure</td>
      <td>Automated OpenAPI/Swagger docs 📖 — generated from DTOs and decorators</td>
    </tr>
    <tr>
      <td><strong>Build velocity</strong></td>
      <td>Monthly “Big Bang” sprints 🗓️ — one WAR, everything or nothing</td>
      <td>Fast, continuous builds 🚀 — each service deploys independently</td>
    </tr>
    <tr>
      <td><strong>Testing</strong></td>
      <td>Deploy and pray — production was the test environment</td>
      <td>100% E2E via Testcontainers (real MySQL + Redis) — confidence before every deploy</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="-impact-speed-stability-and-sanity">🚀 Impact: Speed, Stability, and Sanity</h2>

<p>By adopting this pattern, we didn’t just clean up our code — we unlocked the business.</p>

<p><strong>⚡ Velocity</strong>
From monthly “Big Bang” releases to fast, continuous builds. Each service ships on its own schedule.</p>

<p><strong>🛡️ Reliability</strong>
With 100% E2E coverage using Testcontainers, we no longer “deploy and pray.” Real MySQL + Redis in every test run.</p>

<p><strong>📊 Customer360</strong>
Built a high-performance churn analysis tool in record time — proving the new architecture handles complex data aggregation far faster than the monolith ever could.</p>

<hr />

<h2 id="-what-weve-shipped--and-whats-next">🔭 What We’ve Shipped — and What’s Next</h2>

<h3 id="-already-shipped-in-v2">✅ Already shipped in V2</h3>

<ul>
  <li><strong>Centralized Root Cause (CRC)</strong> — greenfield module, zero legacy dependency. New tables, new NestJS module, new React frontend. V1 untouched.</li>
  <li><strong>Customer360</strong> — AI-powered churn risk analysis across all workspace surveys. Aggregated NPS scoring, top 10 root causes, per-customer severity and action items. Shares some V1 data inputs during transition, fully owned by V2.</li>
</ul>

<h3 id="-currently-in-progress">🔄 Currently in progress</h3>

<ul>
  <li><strong>Deploy (Send flow)</strong> — moving from a fragile, rigid V1 design to a reliable, independent V2 service. Deep dependency on shared campaign infrastructure being wrapped behind clean interfaces.</li>
  <li><strong>Analytics Dashboard</strong> — swapping old UX and slow queries for a modern React frontend and optimised NestJS backend to deliver insights in milliseconds.</li>
</ul>

<hr />

<h2 id="️-strategic-comparison">🗺️ Strategic Comparison</h2>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th>V1 — The Legacy Monolith</th>
      <th>V2 — The App-Service Future</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Architecture</strong></td>
      <td>Monolithic / Opaque — 1,000+ Java files, shared tables, no enforced boundaries</td>
      <td>Decoupled App-Services — domain modules, strict layering, Facade pattern enforced</td>
    </tr>
    <tr>
      <td><strong>Development</strong></td>
      <td>“Whack-a-Mole” debugging — fix one thing, break another</td>
      <td>Predictable &amp; tested — surgical changes, 100% E2E API coverage</td>
    </tr>
    <tr>
      <td><strong>UX / Performance</strong></td>
      <td>Slow queries on live tables · Old JSP/React 15 UI</td>
      <td>Modern React · Optimized NestJS queries · Real-time analytics</td>
    </tr>
    <tr>
      <td><strong>Stack</strong></td>
      <td>Struts · Ant · hand-rolled JDBC (V1)</td>
      <td>NestJS · TypeScript · TypeORM · Turborepo (V2)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="-the-lesson">💡 The Lesson</h2>

<p>The most important thing we learned is that <strong>you don’t fix a legacy system by refactoring it forever</strong>. You fix it by building a better future right next to it. This approach has moved us from a system that limits our potential to one that empowers it.</p>

<blockquote>
  <p><strong>One nav tab at a time.</strong> That’s how you strangle a monolith without breaking what’s already working.</p>
</blockquote>

<hr />]]></content><author><name>Archa Agrawal</name></author><category term="Engineering" /><category term="Software Architecture" /><category term="Legacy Migration" /><category term="Strangler Fig" /><category term="NestJs -Typescript" /><category term="App services" /><category term="system design" /><category term="performance optimization" /><category term="Legacy Modernisation" /><summary type="html"><![CDATA[The Strangler Fig — We Didn't Rewrite It. We Outgrew It.]]></summary></entry><entry><title type="html">Limits: The Engineering Decision You Keep Postponing—Until Production Makes It For You</title><link href="https://www.questionpro.com/engineering/engineering/software%20architecture/performance/limits-the-engineering-decision-you-keep-postponing/" rel="alternate" type="text/html" title="Limits: The Engineering Decision You Keep Postponing—Until Production Makes It For You" /><published>2026-04-21T00:00:00+00:00</published><updated>2026-04-21T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/engineering/software%20architecture/performance/limits-the-engineering-decision-you-keep-postponing</id><content type="html" xml:base="https://www.questionpro.com/engineering/engineering/software%20architecture/performance/limits-the-engineering-decision-you-keep-postponing/"><![CDATA[<p>What running at scale taught us about the limits we didn’t set.</p>

<hr />

<h2 id="prologue--why-well-add-it-later-is-a-trap">Prologue — Why “We’ll Add It Later” Is a Trap</h2>

<p>Every system starts with the same quiet assumption: <em>we’ll add limits later</em>.</p>

<p>And at the start, that’s a fair call. Data is small. Users are few. Edge cases are theoretical. Adding limits up front feels like premature optimization — or worse, a bad user experience you’re imposing on people who haven’t done anything wrong yet.</p>

<p>So you skip them.</p>

<p>Then, months or years in, the system starts answering back. Not with errors. Not with crashes. With something far more dangerous: it gets slow. Inconsistent. Unpredictable. Users file vague tickets like <em>“the app feels weird today”</em> and your dashboards look almost fine.</p>

<p>That’s the moment you realize limits were never optional. They were just deferred — and deferred limits always come due with interest.</p>

<p>This is the story of the incidents that reshaped how we think about limits — and why stress testing is the cheapest way to find them before your users do.</p>

<hr />

<h2 id="-incident-01--the-save-that-stopped-the-world">🚨 Incident 01 — The Save That Stopped the World</h2>

<p>The first incident wasn’t a crash. It was a plateau. CPU had climbed, stuck, and stayed there. Latency tails were drifting up across the platform — every user a little slower, with no single request to blame. The graphs were red, but they weren’t telling us anything specific.</p>

<p>After enough time in logs and traces, the pattern emerged. This wasn’t one rogue survey — it was a whole category of them. Surveys with large conjoint questions, where editing features and levels meant bulk-writing hundreds of related entities at once. Every one of those edits funneled into the same hot path.</p>

<p><strong>A quick primer</strong>: a <em>conjoint question</em> is built from <em>features</em> (like price or color) and <em>levels</em> (specific values per feature). The math is combinatorial — 10 features × 20 levels is 200 entities for a single question, and surveys often have several.</p>

<p>Every bulk edit touched all of them in one transaction. That matched the blow-up pattern exactly — and it was baked into the feature itself, not a misuse of it.</p>

<p><strong>The entire hot path came down to one call:</strong></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">levelRepository</span><span class="p">.</span><span class="nf">saveAll</span><span class="p">(</span><span class="nx">levels</span><span class="p">);</span>
</code></pre></div></div>

<p>It looks innocent. It wasn’t.</p>

<p><code class="language-plaintext highlighter-rouge">repository.save()</code> in TypeORM is an <em>upsert</em>. For every entity in the array, it checks whether the row exists, compares it field-by-field against the database version, and deep-merges nested relations before persisting. On small arrays, it’s fine. On arrays of a few thousand related entities, the comparison logic degrades into <strong>O(n²)</strong> — and all of it runs as synchronous JavaScript on the main thread.</p>

<p>While that code ran, nothing else on the Node process could make progress.</p>

<p><img src="/engineering/assets/images/deferred-limits-incident-1.png" alt="Deferred limits incident 1" /></p>

<p align="center"><sub><b>FIG. 01</b> · Event loop blocked by synchronous CPU work on a single request.</sub></p>

<p>We fixed the immediate problem quickly — an optimized bulk-write strategy that skips the per-entity comparison — and latency recovered within hours. We’re also moving CPU-heavy work to worker threads, so no future slow operation can starve the event loop. But the fix wasn’t the lesson.</p>

<p>The lesson was everything that had to be true for this to happen.</p>

<ol>
  <li>
    <p><strong>No structural limits.</strong> A conjoint question could grow to hundreds of features and levels, and a survey could contain several — with nothing to stop it. A cap on features and levels per question would have ended the story right there. One user’s edit could never have become everyone else’s problem.</p>
  </li>
  <li>
    <p><strong>No stress testing.</strong> Even with limits in place, we wouldn’t have known whether they were <em>safe</em> — because the write path had never been tested at scale. The ORM’s O(n²) cliff was invisible at small data sizes, which is exactly the kind of thing stress testing exists to find.</p>
  </li>
</ol>

<p>Limits decide what the system allows. Stress testing proves the system can handle what it allows. With neither, production ended up being the first real test — the hard way to find out.</p>

<blockquote>
  <p><strong><em>Without limits, one user can degrade the system for everyone. Without stress testing, you won’t know until they do.</em></strong></p>
</blockquote>

<hr />

<h2 id="-incident-02--the-limit-we-didnt-set-so-browser-did">🚨 Incident 02 — The Limit We Didn’t Set (So Browser Did)</h2>

<p>A customer opened a survey with 800+ questions in a single block. Many of those questions had <code class="language-plaintext highlighter-rouge">&lt;audio&gt;</code> tags embedded in their content, and when the survey loaded, the browser dutifully started creating audio players for every one of them.</p>

<p>It didn’t finish. Somewhere past the first few dozen, Chrome stopped cooperating. The page froze. The console filled with errors:</p>

<p><img src="/engineering/assets/images/deferred-limits-incident-2.png" alt="Deferred limits incident 2" /></p>

<p align="center"><sub><b>FIG. 02</b> · Chrome's WebMediaPlayer limit hit silently when rendering too many audio elements.</sub></p>

<p>Chrome has a hard cap on the number of simultaneous <code class="language-plaintext highlighter-rouge">WebMediaPlayer</code> instances per page. It’s baked into the browser — you can’t raise it, you can’t bypass it. Exceed it, and every subsequent media element silently fails to initialize.</p>

<p>The fix was virtualization — render only what’s in the viewport, defer the rest. But virtualization wasn’t the lesson. It was the workaround.</p>

<p>The lesson was that <strong>Chrome already had a limit. We just hadn’t matched it.</strong> We allowed effectively unbounded questions per block, each free to embed any number of media tags. Chrome allowed a few dozen concurrent players. One of those numbers was fixed; the other wasn’t. They were always going to meet — and the browser was always going to win.</p>

<p>A product-level cap, set well below Chrome’s, would have kept the collision from ever happening. Users would see a clear message from us, instead of a silent failure from the browser. We didn’t have that cap. So Chrome drew the line for us.</p>

<blockquote>
  <p><strong><em>Every layer of your stack has limits. The only question is whether you set yours, or discover someone else’s.</em></strong></p>
</blockquote>

<hr />

<h2 id="-incident-03--the-limit-on-whats-awake">🚨 Incident 03 — The Limit on What’s Awake</h2>

<p>The first two incidents failed loudly. One froze a backend thread, the other froze a browser tab. The third one didn’t fail at all. It just stopped being usable.</p>

<p>A customer with a 200-question survey opened the editor, expanded a few blocks to work across them, and the UI began to crawl. Clicks took a beat. Scrolling stuttered. Typing lagged behind the keystrokes.</p>

<p>We assumed it was the question count. It wasn’t — not on its own. A block with 200 questions rendered fine if nothing else was expanded. A block with 50 questions rendered fine if three others were also open. The slowness only showed up when <em>enough of the UI was active at the same time</em>.</p>

<p>We hadn’t put a limit on that. Any number of blocks could be expanded, each rendering all of its questions, inputs, and validations. A single user could, with a few clicks, ask the browser to keep hundreds of stateful React subtrees alive and reactive at once.</p>

<p><img src="/engineering/assets/images/deferred-limits-incident-3.png" alt="Deferred limits incident 3" /></p>

<p align="center"><sub><b>FIG. 03</b> · Same survey, different activation patterns. Cost scales with what's awake, not with what exists.</sub></p>

<p>The fix was to bound the active state. Above a threshold, only one block stays expanded at a time; opening another collapses the previous. The UI recovered immediately — not because rendering got faster, but because we stopped asking the browser to keep the whole survey hot in memory.</p>

<blockquote>
  <p><strong><em>Cost scales with what’s awake, not with what exists. If you don’t cap what’s active, the browser will — by slowing down until users stop.</em></strong></p>
</blockquote>

<hr />

<h2 id="-limits-you-dont-have-to-enforce">💡 Limits You Don’t Have to Enforce</h2>

<p>After three incidents, we were convinced: limits needed to be first-class.</p>

<p>The most obvious limit to add was a cap on the total number of answer entities a survey can hold. The enforcement seemed straightforward — on every update, count the answers, reject if over the cap.</p>

<p>Before we shipped it, we did the thing we hadn’t done in any of the previous incidents. <strong>We stress-tested it.</strong></p>

<p>It caught the problem immediately.</p>

<p>Counting total answers meant walking the entire survey tree on every update. The walk was cheap on small surveys. On large ones, it grew expensive — and the larger the survey, the more often it ran, because larger surveys see more edits.</p>

<p>A naive reading of <em>“enforce the limit”</em> would have placed a growing traversal directly in the hot path of every write. We were about to recreate Incident 01 on purpose.</p>

<p><img src="/engineering/assets/images/deferred-limits-conclusion.png" alt="Deferred limits conclusion" /></p>

<p align="center"><sub><b>FIG. 04</b> · Same limit, two ways to enforce it. One costly. One free.</sub></p>

<p>So we stepped back and asked a different question: <em>do we need this check at all?</em></p>

<p>We already had limits at lower layers — a cap on answers per question, and a cap on questions per survey. Multiply those together and the total answer count is <em>already</em> bounded, without any runtime check. The survey-level cap we wanted to enforce was already enforced, for free, by the composition of the limits beneath it.</p>

<p>We removed the check from the design. The final set of limits stayed at the layers where they naturally belonged — per question, per row, per column — and the aggregate took care of itself.</p>

<p>This was the turning point for how we thought about limits. The previous incidents had taught us that limits needed to exist. This one taught us something subtler: <strong>where</strong> you enforce a limit matters more than the limit itself. A cap enforced at the wrong layer is just a new bottleneck. A cap guaranteed by composition is free.</p>

<blockquote>
  <p><strong><em>The best limits are the ones the system already guarantees. If a cap can be derived from lower-level constraints, enforcing it again at runtime is overhead with no benefit.</em></strong></p>
</blockquote>

<hr />

<h2 id="-a-better-mental-model-for-limits">🧠 A Better Mental Model for Limits</h2>

<p>After three incidents and one near-miss, limits stopped being ad-hoc reactions and became a design vocabulary.</p>

<table>
  <thead>
    <tr>
      <th>What kind of limit</th>
      <th>If you don’t set it…</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Input</strong> — what the system accepts</td>
      <td>One request’s CPU work starves the event loop</td>
    </tr>
    <tr>
      <td><strong>Output</strong> — what ends up on the page</td>
      <td>The browser enforces its own limits, silently, on your users</td>
    </tr>
    <tr>
      <td><strong>Active state</strong> — how much is alive at once</td>
      <td>The UI feels broken without technically being broken</td>
    </tr>
    <tr>
      <td><strong>Composition</strong> — aggregates derived from lower-level caps</td>
      <td>You build a runtime check the system didn’t need</td>
    </tr>
    <tr>
      <td><strong>Enforcement</strong> — <em>where</em> the limit lives</td>
      <td>The right limit becomes the new bottleneck</td>
    </tr>
  </tbody>
</table>

<p>Two of these matter most. <em>Composition</em> saves you the most work — the best limits are the ones you don’t have to enforce. <em>Enforcement</em> is the easiest to get wrong — the right limit in the wrong place is still a bottleneck.</p>

<p>And underneath all of them: <em>stress testing</em> is how you find out whether your limits actually hold. Without it, production does the testing for you.</p>

<hr />

<h2 id="-limits-are-guarantees">✅ Limits Are Guarantees</h2>

<p>By the time we were done, we understood limits weren’t a constraint we had to add to the product. They were a set of guarantees we already needed:</p>

<ul>
  <li>One user’s actions can’t slow down the experience for everyone else.</li>
  <li>The browser doesn’t silently enforce its limits on your users.</li>
  <li>The interface stays responsive no matter how much data it holds.</li>
  <li>Your safety checks don’t become the bottleneck they were meant to prevent.</li>
</ul>

<p>That reframing changed how we designed features. <em>“What’s the limit?”</em> moved from one of the last questions in a design doc to one of the first.</p>

<hr />

<blockquote>
  <p><strong>If you don’t define limits deliberately, your system will discover them accidentally — in production, under load, at the worst possible time.</strong></p>
</blockquote>]]></content><author><name>Akash Karyakarte</name></author><category term="Engineering" /><category term="Software Architecture" /><category term="Performance" /><category term="system design" /><category term="performance optimization" /><category term="stress testing" /><category term="scalability" /><category term="production incidents" /><category term="event loop" /><category term="node.js" /><category term="database optimization" /><category term="react performance" /><summary type="html"><![CDATA[What running at scale taught us about the limits we didn't set.]]></summary></entry><entry><title type="html">Building Scalable AI Agents: A Journey Through Multi-Agent Architecture</title><link href="https://www.questionpro.com/engineering/ai%20&%20machine%20learning/software%20architecture/engineering/cost%20optimization/building-scalable-ai-agents/" rel="alternate" type="text/html" title="Building Scalable AI Agents: A Journey Through Multi-Agent Architecture" /><published>2026-02-21T00:00:00+00:00</published><updated>2026-02-21T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/ai%20&amp;%20machine%20learning/software%20architecture/engineering/cost%20optimization/building-scalable-ai-agents</id><content type="html" xml:base="https://www.questionpro.com/engineering/ai%20&amp;%20machine%20learning/software%20architecture/engineering/cost%20optimization/building-scalable-ai-agents/"><![CDATA[<h2 id="or-how-i-learned-to-stop-worrying-and-love-the-token-budget">Or: How I Learned to Stop Worrying and Love the Token Budget</h2>

<h2 id="introduction">Introduction</h2>

<p>Remember when you thought building an AI agent would be easy? “Just throw some prompts at GPT-4 and call it a day,”.</p>

<p>“What could go wrong?” you said.</p>

<p><em>Narrator: Everything went wrong.</em></p>

<p>Large Language Models have revolutionized intelligent applications, but here’s what nobody tells you at those fancy AI conferences: scaling an AI agent from “cool demo” to “production system that doesn’t bankrupt your company” is… <em>challenging</em>. Token costs spiral like a startup’s cloud bill, context windows overflow faster than your coffee cup on Monday morning, and response times make dial-up internet look speedy.</p>

<p>This is our journey (currently in progress, bugs included) from a basic implementation to building a production-ready, cost-efficient multi-agent system for QuestionPro’s BI platform. We’re exploring three key patterns, making mistakes in real-time, and documenting everything so you don’t have to. Think of this as “Mythbusters” but for AI architecture, with 100% more token optimization and slightly fewer explosions.</p>

<hr />

<h2 id="part-1-the-basic-agent---or-how-hard-could-it-be">Part 1: The Basic Agent - Or “How Hard Could It Be?”</h2>

<h3 id="the-innocent-beginning">The Innocent Beginning</h3>

<p>Like every developer who’s just discovered LangGraph, we started with the “obvious” approach, THE MONOLITHIC AGENT: one agent to rule them all, one agent to find them, one agent to bring them all, and in the darkness bind them (to a $10,000/month OpenAI bill… roughly).</p>

<p><img src="/engineering/assets/images/monolithic-agent-architecture.png" alt="Monolithic Agent Architecture" /></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Single agent with all capabilities</span>
<span class="kd">const</span> <span class="nx">agent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ChatOpenAI</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gpt-4</span><span class="dl">"</span> <span class="p">}).</span><span class="nf">bindTools</span><span class="p">([</span>
  <span class="nx">createDashboard</span><span class="p">,</span>
  <span class="nx">getDashboard</span><span class="p">,</span>
  <span class="nx">createWidget</span><span class="p">,</span>
  <span class="nx">listSurveys</span><span class="p">,</span>
  <span class="nx">getQuestions</span><span class="p">,</span>
  <span class="c1">// ... 40+ more tools</span>
<span class="p">]);</span>

<span class="kd">const</span> <span class="nx">systemPrompt</span> <span class="o">=</span> <span class="s2">`
You are a dashboard analytics assistant.

Available dashboards: [... 500 lines of context ...]
Survey schemas: [... 2000 lines of schemas ...]
Widget configurations: [... 1500 lines of configs ...]
`</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="the-oh-no-moment">The “Oh No” Moment</h3>

<p>You know that feeling when you check your AI bill and your heart skips a beat? That was us, thinking about going into “production”.</p>

<p><strong>Our “Everything is Fine” Metrics:</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>📊 The Uncomfortable Truth:
- Avg tokens per request: 15,000 (narrator: this is not good)
- Cost per conversation: $1.50 (multiply by users... *sweats*)
- P95 latency: 8 seconds (users hate us)
- Projected monthly costs: $4,500 (CFO hates us more)
- Success rate: 89% (11% of the time, it works every time!)

🔴 The "We Need to Talk" Problems:
- Context overflow (GPT-4 politely hanging up on us)
- Tool selection paralysis (like Netflix, which movie to watch)
- Exponential cost growth
- LLM having an existential crisis (overwhelmed with 40+ tools)
</code></pre></div></div>

<h3 id="the-exponential-growth-problem-or-math-is-unforgiving">The Exponential Growth Problem (Or: Math is Unforgiving)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Turn 1:  12,500 tokens  ✅ "This is fine"
Turn 5:  22,000 tokens  ⚠️ "This is less fine"
Turn 10: 37,000 tokens  ❌ "This is fire, everything is fire"
</code></pre></div></div>

<p><strong>We were literally pricing ourselves out of business, one helpful conversation at a time.</strong></p>

<p>The real kicker? We kept asking “Can we just add more features?” while totally unaware of the bill that might get generated.</p>

<p>Note: These numbers are rough estimates from our experiments and paper napkin calculations. We’re still actively developing and iterating, but gives an idea to think in the right direction.</p>

<hr />

<h2 id="part-2-the-skills-pattern---progressive-disclosure">Part 2: The Skills Pattern - Progressive Disclosure</h2>

<h3 id="the-aha-moment">The “Aha!” Moment</h3>

<h4 id="or-how-we-discovered-that-laziness-is-actually-a-virtue">Or: How We Discovered That Laziness is Actually a Virtue</h4>

<p>After three existential crises where we admitted our agent was hemorrhaging money, we stumbled upon a life-changing concept:</p>

<blockquote>
  <p>“What if… and hear me out… we DON’T load everything into the prompt like we’re packing for a 2-week vacation?”</p>
</blockquote>

<p>Revolutionary, I know. We felt like we’d discovered fire, except fire was actually “reading the documentation properly.”</p>

<p>Enter <strong>progressive disclosure</strong> - a fancy term for “only fetch stuff when you actually need it,” which is basically how every efficient human operates but somehow felt groundbreaking when applied to AI. (Yes, we’re aware of the irony.)</p>

<blockquote>
  <p>“Don’t load everything upfront. Load only what you need, when you need it.”</p>
</blockquote>

<p>Instead of including all context in every request, we implemented <strong>progressive disclosure</strong> - the agent loads specialized knowledge through the skills pattern.</p>

<p><img src="/engineering/assets/images/skills-pattern-visualization.png" alt="Skills Pattern Visualization" /></p>

<h3 id="implementation">Implementation</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Lightweight system prompt with skill metadata only</span>
<span class="kd">const</span> <span class="nx">systemPrompt</span> <span class="o">=</span> <span class="s2">`
You are a dashboard analytics assistant.

Available Skills (load when needed):
- survey_schema: Get survey questions and field types
- widget_config: Get chart-specific settings  
- dashboard_filters: Get filter options
- styling_themes: Get appearance options

Current context:
- Workspace: 1
- Dashboard: </span><span class="p">${</span><span class="nx">state</span><span class="p">.</span><span class="nx">dashboardId</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">}</span><span class="s2">
`</span><span class="p">;</span>

<span class="c1">// Skills load on-demand</span>
<span class="kd">const</span> <span class="nx">loadSkillTool</span> <span class="o">=</span> <span class="nf">tool</span><span class="p">(</span><span class="k">async </span><span class="p">({</span> <span class="nx">skillName</span><span class="p">,</span> <span class="nx">resourceId</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">skillName</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">survey_schema</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="k">await</span> <span class="nf">fetchSurveySchema</span><span class="p">(</span><span class="nx">resourceId</span><span class="p">);</span>
    <span class="c1">// Returns ~2,000 tokens only when needed</span>

    <span class="k">case</span> <span class="dl">"</span><span class="s2">widget_config</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="k">await</span> <span class="nf">fetchWidgetConfig</span><span class="p">(</span><span class="nx">resourceId</span><span class="p">);</span>
    <span class="c1">// Returns ~1,500 tokens only when needed</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="skills-in-action">Skills in Action</h3>

<p><strong>User</strong>: “Create a gender chart from Customer Survey”</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request 1: Agent sees lightweight prompt
├─ System: 1,000 tokens (was 8,000)
├─ Tools: 1,500 tokens
└─ Messages: 2,000 tokens
   Total: 4,500 tokens ✅

Agent decides: "I need the survey schema"
└─ Calls: load_skill("survey_schema", 12345)

Request 2: Agent receives survey details
├─ Previous context: 4,500 tokens
├─ Survey schema: 2,000 tokens
└─ Total: 6,500 tokens ✅

Agent creates chart with correct data mapping
</code></pre></div></div>

<h3 id="the-results-or-when-theory-meets-reality-and-actually-works">The Results (Or: When Theory Meets Reality and Actually Works)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>📊 Skills Pattern Metrics (We Were Shocked Too):

- Avg tokens: 6,500 (was 15,000) 🎉
- Cost per conversation: $0.65 (was $1.50) 💰
- P95 latency: 4s (was 8s) ⚡
- Monthly costs: $1,950 (was $4,500) 🎊

✅ 57% cost reduction (CFO can sleep better)
✅ 50% faster responses (users will hate us a little less now)
✅ Stable token usage (no more exponential nightmares)
✅ Team morale: Significantly improved

</code></pre></div></div>

<p><em>As this was my late night hustle when no one was around, I just high fived my wall.</em></p>

<pre><code class="language-texttext">
### But Wait... (The Plot Thickens)

Just when we thought we'd solved AI, reality decided to humble us. Again.


❌ The "Not So Fast" Problems:

1. Agent handling 40+ tools → Like asking someone to pick
   their favorite child, but with 40 children
2. Mixed concerns → Debugging became "which of these
   40 things broke?"
3. No parallelization → Everything sequential because
   apparently we hate speed
4. Some skills returned 3,000+ tokens → The token diet
   didn't last long

</code></pre>

<p><strong>The Comedy of Errors:</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
User: "Style my dashboard in dark mode."
(A simple, reasonable request.)

Agent's internal monologue:
"Hmm, I have 40 tools. Let me read each description...
create_dashboard? No...
update_dashboard? Maybe?
create_widget? Probably not...
update_theme? OH WAIT THAT'S THE ONE! ✅
...only took me 3 seconds to figure that out"

User: _has already rage-quit_

</code></pre></div></div>

<hr />

<h2 id="part-3-multi-agent-architecture---specialized-experts">Part 3: Multi-Agent Architecture - Specialized Experts</h2>

<h3 id="the-why-didnt-we-think-of-this-sooner-moment">The “Why Didn’t We Think of This Sooner?” Moment</h3>

<h4 id="the-breakthrough-cue-dramatic-music">The Breakthrough (Cue Dramatic Music)</h4>

<p>Picture this: It’s 2 AM, you’re on your third coffee, scrolling through LangGraph documentation for the millionth time, when suddenly…</p>

<blockquote>
  <p>“What if… <em>what if</em>… we don’t make one agent do EVERYTHING? What if we have specialized agents? Like… real companies?”</p>
</blockquote>

<p>🤯 <em>Mind. Blown.</em></p>

<p>We felt like we’d invented the wheel, despite the fact that humans have been organizing into specialized roles since, like, the dawn of civilization. But hey, better late than never!</p>

<p><strong>Our New Squad:</strong></p>

<ul>
  <li><strong>Dashboard Agent</strong>: The organized one who actually reads the manual</li>
  <li><strong>Widget Agent</strong>: The creative type, probably went to art school</li>
  <li><strong>Datasource Agent</strong>: The data nerd (affectionately), speaks fluent SQL</li>
  <li><strong>Styling Agent</strong>: Fashion police of the digital world</li>
</ul>

<p>Basically, we went from having one overworked, stressed-out agent having a breakdown, to a healthy work environment with proper delegation. <em>Revolutionary.</em></p>

<hr />

<h3 id="the-breakthrough">The Breakthrough</h3>

<blockquote>
  <p>“What if instead of one agent doing everything, we had specialized experts?”</p>
</blockquote>

<p>Like a company with departments (Sales, Engineering, Design), we created specialized agents:</p>

<ul>
  <li><strong>Dashboard Agent</strong>: Dashboards and tabs expert</li>
  <li><strong>Widget Agent</strong>: Visualizations expert</li>
  <li><strong>Datasource Agent</strong>: Data and surveys expert</li>
  <li><strong>Styling Agent</strong>: Themes and appearances expert</li>
</ul>

<p><img src="/engineering/assets/images/multi-agent-system-architecture.png" alt="Multi-Agent System Architecture" /></p>

<h3 id="federal-router-implementation">Federal Router Implementation</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Lightweight orchestrator</span>
<span class="kd">const</span> <span class="nx">federalAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ChatOpenAI</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gpt-4</span><span class="dl">"</span> <span class="p">}).</span><span class="nf">bindTools</span><span class="p">([</span>
  <span class="nx">routeToDashboardAgent</span><span class="p">,</span>
  <span class="nx">routeToWidgetAgent</span><span class="p">,</span>
  <span class="nx">routeToDatasourceAgent</span><span class="p">,</span>
  <span class="nx">routeToStylingAgent</span><span class="p">,</span>
<span class="p">]);</span>

<span class="kd">const</span> <span class="nx">federalPrompt</span> <span class="o">=</span> <span class="s2">`
You are a routing assistant. Delegate to specialists:

- Dashboard operations → Dashboard Agent
- Widget creation/editing → Widget Agent
- Data selection → Datasource Agent
- Styling/themes → Styling Agent

You route and synthesize - you don't execute tasks.
`</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="specialized-agent-example">Specialized Agent Example</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Dashboard Agent - Only 6 focused tools</span>
<span class="kd">const</span> <span class="nx">dashboardAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ChatOpenAI</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gpt-4</span><span class="dl">"</span> <span class="p">}).</span><span class="nf">bindTools</span><span class="p">([</span>
  <span class="nx">createDashboard</span><span class="p">,</span>
  <span class="nx">getDashboard</span><span class="p">,</span>
  <span class="nx">updateDashboard</span><span class="p">,</span>
  <span class="nx">createTab</span><span class="p">,</span>
  <span class="nx">updateTab</span><span class="p">,</span>
  <span class="nx">deleteDashboard</span><span class="p">,</span>
<span class="p">]);</span>
</code></pre></div></div>

<h3 id="conversation-flow">Conversation Flow</h3>

<p><strong>User</strong>: “Create sales dashboard with gender chart from Customer Survey”</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌────────────────────────────────────────┐
│ 1. Federal Router                      │
│ Tools: 4 routing (400 tokens)          │
│ Decision: Dashboard → Data → Widget    │
│ Cost: 1,400 tokens                     │
└────────────────────────────────────────┘
                ↓
┌────────────────────────────────────────┐
│ 2. Dashboard Agent                     │
│ Tools: 6 dashboard (600 tokens)        │
│ Creates: "Sales" dashboard (ID 100)    │
│ Cost: 1,400 tokens                     │
└────────────────────────────────────────┘
                ↓
┌────────────────────────────────────────┐
│ 3. Datasource Agent                    │
│ Tools: 10 data (1,000 tokens)          │
│ Finds: Survey 12345, Question 67890    │
│ Cost: 2,200 tokens                     │
└────────────────────────────────────────┘
                ↓
┌────────────────────────────────────────┐
│ 4. Widget Agent                        │
│ Tools: 8 widget (800 tokens)           │
│ Creates: Pie chart widget              │
│ Cost: 2,800 tokens                     │
└────────────────────────────────────────┘

Total workflow: 8,200 tokens (vs 60,000)
</code></pre></div></div>

<h3 id="the-results-holy-sh-i-mean-wow">The Results (Holy Sh— I Mean, Wow!)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
📊 The Numbers Don't Lie (But We Triple-Checked Anyway):

- Avg tokens per workflow: 8,200 (was 60,000) 📉
- Cost per workflow: $0.082 (was $0.60) 💸
- P95 latency: 3.5s (was 8s) 🚀
- Projected monthly costs: $780 (was $4,500) 🎯
- Success rate: 97% (was 89%) ✨

✅ 86% token reduction (not a typo!)
✅ 86% cost savings (CFO wants to buy us lunch)
✅ 56% faster responses (users think we upgraded servers)
✅ Clear debugging (we now know WHICH agent to blame)
✅ Team happiness: Through the roof
✅ Sleep quality: Significantly improved

</code></pre></div></div>

<p>At this point, we were pretty sure we’d reached peak performance. We were wrong.</p>

<hr />

<h2 id="part-4-the-hybrid-approach---maximum-efficiency">Part 4: The Hybrid Approach - Maximum Efficiency</h2>

<h3 id="peak-engineering">Peak Engineering</h3>

<h4 id="or-what-happens-when-you-combine-your-two-good-ideas">Or: What Happens When You Combine Your Two Good Ideas</h4>

<p>After achieving what we thought was AI nirvana with multi-agents, I had a dangerous thought:</p>

<blockquote>
  <p>“Hey… what if we combine multi-agents WITH the skills pattern?”</p>
</blockquote>

<p>It was an idea that precedes either genius or disaster. (In software engineering, these are often the same thing.)</p>

<p>But then we realized: <strong>Why choose between our two good ideas when we can have BOTH?</strong></p>

<p>Multi-agents solved the “too many tools” problem, but we could still use skills to optimize even further. It’s like discovering that peanut butter AND jelly make a better sandwich together. Revolutionary? No. Delicious? Absolutely.</p>

<h3 id="combining-the-best-of-both">Combining the Best of Both</h3>

<p>Multi-agents solved tool confusion, but skills can optimize even further. <strong>Each specialized agent uses skills for deep context.</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Widget Agent with Skills</span>
<span class="kd">const</span> <span class="nx">widgetAgent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ChatOpenAI</span><span class="p">({</span> <span class="na">model</span><span class="p">:</span> <span class="dl">"</span><span class="s2">gpt-4</span><span class="dl">"</span> <span class="p">}).</span><span class="nf">bindTools</span><span class="p">([</span>
  <span class="c1">// Core tools (lightweight, always bound)</span>
  <span class="nx">createWidget</span><span class="p">,</span>
  <span class="nx">updateWidget</span><span class="p">,</span>
  <span class="nx">deleteWidget</span><span class="p">,</span>

  <span class="c1">// Skill loader (heavy context on-demand)</span>
  <span class="nx">loadSkill</span><span class="p">,</span>
<span class="p">]);</span>

<span class="kd">const</span> <span class="nx">widgetPrompt</span> <span class="o">=</span> <span class="s2">`
You are the Widget Agent - visualization expert.

When you need details:
- Chart config → load_skill("bar_chart_config")
- Widget styling → load_skill("widget_styling", widgetId)
- Data mapping → load_skill("data_mapping_rules")

Current: Dashboard </span><span class="p">${</span><span class="nx">state</span><span class="p">.</span><span class="nx">dashboardId</span><span class="p">}</span><span class="s2">, Tab </span><span class="p">${</span><span class="nx">state</span><span class="p">.</span><span class="nx">tabId</span><span class="p">}</span><span class="s2">
`</span><span class="p">;</span>
</code></pre></div></div>

<p><img src="/engineering/assets/images/hybrid-architecture-diagram.png" alt="Hybrid Architecture Diagram" /></p>

<h3 id="hybrid-in-action">Hybrid in Action</h3>

<p><strong>User</strong>: “Create a pie chart showing age distribution”</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Federal Router → Datasource Agent → Widget Agent

Datasource Agent:
├─ Loads: survey_schema skill (2,000 tokens)
├─ Finds age question
└─ Returns structured data

Widget Agent:
├─ Loads: pie_chart_config skill (1,500 tokens)
├─ Creates widget with proper mapping
└─ Total: 6,200 tokens ✅

Vs. loading everything upfront: 18,000 tokens ❌
</code></pre></div></div>

<h3 id="the-metrics">The Metrics</h3>

<p>After implementing the hybrid approach and running it through our test suite (read: throwing everything at it to see what breaks), we got these numbers. We didn’t believe them at first either.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
📊 The "Are We Sure This Is Right?" Results:

Token Usage (The Good Kind of Reduction):
├─ Simple queries: 2,500 (was 12,000) → 79% reduction 🎯
├─ Medium queries: 6,000 (was 35,000) → 83% reduction 💪
└─ Complex workflows: 12,000 (was 80,000) → 85% reduction 🚀

Cost Per User/Month:
├─ Light users: $0.25 (was $1.20) → 79% savings
├─ Regular users: $1.50 (was $8.75) → 83% savings
└─ Power users: $6.00 (was $35.00) → 83% savings
(Power users can stay, we can afford them now!)

Performance (Users Think We're Wizards):
├─ P50 latency: 1.8s (was 4.2s) → 57% faster ⚡
├─ P95 latency: 3.5s (was 8.7s) → 60% faster ⚡⚡
└─ P99 latency: 5.2s (was 15s) → 65% faster ⚡⚡⚡

Reliability (Actually Impressed Ourselves):
├─ Success rate: 97% (was 89%) → +8% improvement
├─ Tool selection: 99% (was 85%) → +14% improvement
└─ Context overflow: 0.1% (was 8%) → Basically extinct

💰 Projected Monthly Costs: $780 (was $4,500)
💰 Annual Savings: $44,640
💰 Engineer Stress Levels: Down 90%
☕ Coffee Consumption: Actually decreased
😴 Sleep Quality: Markedly improved

</code></pre></div></div>

<hr />

<h2 id="choosing-your-pattern-the-decision-tree">Choosing Your Pattern: The Decision Tree</h2>

<h3 id="the-which-one-do-i-actually-need-decision-tree">The “Which One Do I Actually Need?” Decision Tree</h3>

<h4 id="or-saving-you-from-our-mistakes">Or: Saving You From Our Mistakes</h4>

<p>Look, we tried ALL the things so you don’t have to. Here’s our hard-won wisdom, gained through tears, tokens, and too much coffee:</p>

<h3 id="final-architecture-overview">Final Architecture Overview</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    ┌─────────────────────────────────────────────┐
    │ Federal Router Agent (GPT-3.5)              │
    │ 4 routing tools (400 tokens)                │
    │ Fast, cheap classification                  │
    └─────────────────────────────────────────────┘
                      │
          ┌───────────┼───────────┬──────────┐
          ↓           ↓           ↓          ↓
    ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
    │Dashboard │ │ Widget   │ │Datasource│ │ Styling  │
    │ (GPT-4)  │ │ (GPT-4)  │ │ (GPT-4)  │ │(GPT-3.5) │
    ├──────────┤ ├──────────┤ ├──────────┤ ├──────────┤
    │6 tools   │ │8 tools   │ │10 tools  │ │6 tools   │
    │+ Skills: │ │+ Skills: │ │+ Skills: │ │+ Skills: │
    │ filters  │ │ configs  │ │ schemas  │ │ themes   │
    │ layouts  │ │ mapping  │ │ datasets │ │ fonts    │
    │ rules    │ │ styling  │ │ stacks   │ │ a11y     │
    └──────────┘ └──────────┘ └──────────┘ └──────────┘

</code></pre></div></div>

<hr />

<p>Lessons From the Trenches</p>

<h3 id="what-actually-worked-surprisingly">What Actually Worked (Surprisingly)</h3>

<h4 id="1-baby-steps-dont-just-work-theyre-essential">1. Baby Steps Don’t Just Work, They’re Essential</h4>

<p>We wanted to rebuild everything overnight. Our tech lead said “no.” We’re glad he did.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
┌─────────────────────┬──────────────┬────────────────┐
│ Your Situation      │ Pattern      │ Expected Gain  │
├─────────────────────┼──────────────┼────────────────┤
│ &lt; 10 tools          │ Basic Agent  │ Keep simple    │
│ Simple context      │              │                │
├─────────────────────┼──────────────┼────────────────┤
│ 10-25 tools         │ Skills       │ 50-60% savings │
│ Large context       │              │                │
│ Single domain       │              │                │
├─────────────────────┼──────────────┼────────────────┤
│ 25-50 tools         │ Multi-Agent  │ 70-80% savings │
│ Multiple domains    │              │                │
│ Clear separation    │              │                │
├─────────────────────┼──────────────┼────────────────┤
│ 50+ tools           │ Hybrid       │ 80-85% savings │
│ Complex domains     │              │                │
│ Deep context needs  │              │                │
└─────────────────────┴──────────────┴────────────────┘

</code></pre></div></div>

<p><img src="/engineering/assets/images/decision-tree-flowchart.png" alt="Decision Tree Flowchart" /></p>

<hr />

<h2 id="hard-truths-we-learned-the-expensive-way">Hard Truths We Learned (The Expensive Way)</h2>

<h3 id="1-tool-descriptions-write-them-like-your-job-depends-on-it">1. Tool Descriptions: Write Them Like Your Job Depends On It</h3>

<p>Because your token budget literally does.</p>

<p>❌ Bad (Our First Attempt):</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">createWidget</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Creates a widget</span><span class="dl">"</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// LLM: "Cool story bro, but WHEN do I use this?"</span>
</code></pre></div></div>

<p>✅ Good (After Many Painful Iterations):</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">createWidget</span><span class="p">:</span> <span class="p">{</span>
  <span class="nl">description</span><span class="p">:</span> <span class="s2">`Creates a visualization widget.
  
  Use when: User asks to "add chart/graph/visualization"
  Don't use for: Updating widgets (use update_widget)
  
  // Yes, we're treating the LLM like a junior dev.
  // Yes, it works.`</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pro tip: Think of tool descriptions as writing documentation for the world’s most literal intern. Because that’s essentially what it is.</p>

<h3 id="2-monitor-what-matters">2. Monitor What Matters</h3>

<p>Track more than just cost:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">({</span>
  <span class="na">event</span><span class="p">:</span> <span class="dl">"</span><span class="s2">agent_execution</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">workflowName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">dashboard_creation</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">metadata</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">agentType</span><span class="p">:</span> <span class="dl">"</span><span class="s2">widget_agent</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">skillsLoaded</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">bar_chart_config</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">tokensUsed</span><span class="p">:</span> <span class="mi">3800</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="3-progressive-skill-rollout">3. Progressive Skill Rollout</h3>

<p>Don’t implement all skills at once. Start with the most-used:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Priority 1: survey_schema (80% of requests)
Priority 2: bar_chart_config (60% of requests)
Priority 3: widget_styling (40% of requests)
Priority 4: dashboard_filters (30% of requests)
</code></pre></div></div>

<h3 id="4-dont-over-specialize-learn-from-our-hubris">4. Don’t Over-Specialize (Learn From Our Hubris)</h3>

<p>In our enthusiasm, we initially created 8 agents. EIGHT. We were basically creating an AI bureaucracy.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Original (Too Many):
├─ Dashboard Agent
├─ Tab Agent ❌ (seriously, tabs needed their own agent?)
├─ Widget Agent
├─ Widget Styling Agent ❌ (why did we do this to ourselves)
├─ Survey Agent ❌
├─ Dataset Agent ❌
├─ Analytics Agent ❌
└─ Export Agent ❌

After Therapy (Better):
├─ ✅ Dashboard Agent (includes tabs, we're not savages)
├─ ✅ Widget Agent (includes styling, it's fine)
├─ ✅ Datasource Agent (all data sources, one happy family)
└─ ✅ Styling Agent (themes + accessibility)

</code></pre></div></div>

<p>Sweet spot: <strong>4-6 agents</strong></p>

<p>More than that and you’re just creating coordination overhead. It’s like having too many group chats - eventually nobody knows what’s happening where.</p>

<hr />

<h2 id="quick-wins-thatll-make-you-look-like-a-hero">Quick Wins That’ll Make You Look Like a Hero</h2>

<h3 id="seriously-do-these-today">(Seriously, Do These Today)</h3>

<h4 id="1-stop-writing-novellas-in-your-system-prompts-30-minutes">1. Stop Writing Novellas in Your System Prompts (30 minutes)</h4>

<p>Your system prompt is not the place to explain your life story, company history, and philosophical stance on data visualization.</p>

<h4 id="2-return-structured-data-from-skills">2. Return Structured Data from Skills</h4>

<p>❌ Bad (text):</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">The survey has gender as multiple choice and age as numeric...</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>✅ Good (JSON):</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">questions</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">67890</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Gender?</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">multiple_choice</span><span class="dl">"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">67891</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Age?</span><span class="dl">"</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">numeric</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="3-dont-over-specialize">3. Don’t Over-Specialize</h4>

<p>We initially had 8 agents, consolidated to 4:</p>

<ul>
  <li>✅ Dashboard Agent (includes tabs)</li>
  <li>✅ Widget Agent (includes styling)</li>
  <li>✅ Datasource Agent (all data sources)</li>
  <li>✅ Styling Agent (themes + accessibility)</li>
</ul>

<hr />

<h2 id="lessons-learned-what-weve-learned-so-far">Lessons Learned: What We’ve Learned (So Far)</h2>

<h3 id="the-real-truth-about-building-ai-agents">The Real Truth About Building AI Agents</h3>

<p>Here’s what they don’t tell you in the glossy blog posts and conference talks:</p>

<p>Building scalable AI agents isn’t about having the most sophisticated architecture from day one. It’s about:</p>

<ol>
  <li><strong>Start simple</strong> - Basic agent for MVP (it’s okay, we all started here)</li>
  <li><strong>Measure everything</strong> - If you’re not tracking tokens, you’re flying blind</li>
  <li><strong>Fail fast, learn faster</strong> - We made EVERY mistake so you don’t have to</li>
  <li><strong>Evolve incrementally</strong> - Skills → Multi-Agent → Hybrid (this is the way)</li>
  <li><strong>Focus on user value</strong> - Fast, accurate, reliable (novel concept, we know)</li>
</ol>

<h3 id="our-journey-in-numbers-the-beforeafter-youve-been-waiting-for">Our Journey in Numbers (The Before/After You’ve Been Waiting For)</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Where We Started (The Dark Times):
├─ Cost: $4,500/month (gulp)
├─ Tokens: 15k per request (yikes)
├─ Success: 89% (not great, Bob)
└─ Team morale: Low

Where We're At Now (The Good Times):
├─ Cost: $780/month (manageable!)
├─ Tokens: 6.6k per request (sustainable)
├─ Success: 97% (actually good!)
└─ Team morale: High
</code></pre></div></div>

<h3 id="whats-next-or-what-were-learning-next">What’s Next (Or: What We’re Learning Next)</h3>

<p>As we continue building and scaling QuestionPro’s BI agent, here’s what we’re watching:</p>

<ul>
  <li><strong>Context management is king</strong> - Progressive disclosure isn’t optional, it’s survival</li>
  <li><strong>Specialization is inevitable</strong> - One agent doing everything is like one person doing all jobs at a company (recipe for disaster)</li>
  <li><strong>Hybrid is the sweet spot</strong> - Why choose between good ideas?</li>
  <li><strong>Cost optimization is non-negotiable</strong> - It’s literally the difference between “cool AI feature” and “bankrupt company”</li>
  <li><strong>Testing is hard</strong> - LLMs are non-deterministic. Our test suite has trust issues.</li>
  <li><strong>Documentation matters</strong> - Future you will thank present you (we learned this the hard way)</li>
</ul>

<h3 id="resources-that-actually-helped-us">Resources (That Actually Helped Us)</h3>

<p><strong>LangGraph Documentation:</strong></p>

<ul>
  <li>Multi-Agent Patterns (bookmark this, seriously)</li>
  <li>Progressive Disclosure / Skills Pattern (game changer)</li>
  <li>Examples that actually work (rare, treasure them)</li>
</ul>

<p><strong>Monitoring Tools We Actually Use:</strong></p>

<ul>
  <li>Langfuse (our current favorite)</li>
  <li>LangSmith (also good)</li>
  <li>Custom OpenTelemetry (for the brave)</li>
  <li>Coffee (monitors our alertness)</li>
</ul>

<h3 id="calculate-your-potential-savings-do-this-right-now">Calculate Your Potential Savings (Do This Right Now)</h3>

<p>Seriously, take 2 minutes and see what you could be saving:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Your current situation</span>
<span class="kd">const</span> <span class="nx">currentCost</span> <span class="o">=</span> <span class="p">((</span><span class="nx">monthlyRequests</span> <span class="o">*</span> <span class="nx">avgTokens</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1</span><span class="nx">_000_000</span><span class="p">)</span> <span class="o">*</span> <span class="nx">$10</span><span class="p">;</span>

<span class="c1">// What you COULD be saving</span>
<span class="kd">const</span> <span class="nx">potentialSavings</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">withSkills</span><span class="p">:</span> <span class="nx">currentCost</span> <span class="o">*</span> <span class="mf">0.5</span><span class="p">,</span> <span class="c1">// 50% reduction</span>
  <span class="na">withMultiAgent</span><span class="p">:</span> <span class="nx">currentCost</span> <span class="o">*</span> <span class="mf">0.7</span><span class="p">,</span> <span class="c1">// 70% reduction</span>
  <span class="na">withHybrid</span><span class="p">:</span> <span class="nx">currentCost</span> <span class="o">*</span> <span class="mf">0.85</span><span class="p">,</span> <span class="c1">// 85% reduction</span>
<span class="p">};</span>

<span class="c1">// Now imagine what you could do with that money</span>
<span class="c1">// (We're thinking coffee budget + GPU upgrades)</span>
</code></pre></div></div>

<hr />

<h2 id="final-thoughts-the-honest-ones">Final Thoughts (The Honest Ones)</h2>

<p>Look, we’re still figuring this out. We’re still learning. We’re still making mistakes (just more expensive ones now that we know better). But that’s the point… nobody has this completely figured out. AI is moving faster than documentation can keep up.</p>

<p>What we DO know:</p>

<ul>
  <li>Start simple, evolve based on actual problems</li>
  <li>Measure EVERYTHING (seriously, token tracking is non-negotiable)</li>
  <li>Don’t over-engineer (we did this so you don’t have to)</li>
  <li>Community knowledge is invaluable (thank you, internet strangers)</li>
  <li>It’s okay to not know everything (we certainly don’t)</li>
</ul>

<p><img src="/engineering/assets/images/journey-visualization.png" alt="Journey Visualization" /></p>

<h3 id="the-future">The Future</h3>

<p>As agents scale:</p>

<ul>
  <li><strong>Context management is king</strong> - Progressive disclosure wins</li>
  <li><strong>Specialization is inevitable</strong> - One agent can’t do it all</li>
  <li><strong>Hybrid is the sweet spot</strong> - Combines best of all patterns</li>
  <li><strong>Cost optimization is non-negotiable</strong> - Make or break for production</li>
</ul>]]></content><author><name>Akhil Kumar</name></author><category term="AI &amp; Machine Learning" /><category term="Software Architecture" /><category term="Engineering" /><category term="Cost Optimization" /><category term="ai agents" /><category term="multi-agent systems" /><category term="langgraph" /><category term="langchain" /><category term="token optimization" /><category term="cost optimization" /><category term="llm" /><category term="gpt-4" /><category term="progressive disclosure" /><category term="software architecture" /><category term="engineering patterns" /><category term="QuestionPro" /><summary type="html"><![CDATA[Or: How I Learned to Stop Worrying and Love the Token Budget]]></summary></entry><entry><title type="html">Fresher to Engineering Manager: Journey at QuestionPro</title><link href="https://www.questionpro.com/engineering/personal%20growth/continuous%20learning/professional%20growth/leadership/questionpro%20culture/fresher-to-engineering-manager-journey-at-questionpro/" rel="alternate" type="text/html" title="Fresher to Engineering Manager: Journey at QuestionPro" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/personal%20growth/continuous%20learning/professional%20growth/leadership/questionpro%20culture/fresher-to-engineering-manager-journey-at-questionpro</id><content type="html" xml:base="https://www.questionpro.com/engineering/personal%20growth/continuous%20learning/professional%20growth/leadership/questionpro%20culture/fresher-to-engineering-manager-journey-at-questionpro/"><![CDATA[<p>When I look back on my journey with <strong>QuestionPro</strong>, which began in <strong>March 2012</strong>, I’m filled with gratitude, pride, and a deep sense of belonging. What started as my first professional step after earning a <strong>degree in Computer Engineering from Pune University</strong> has evolved into a career-defining experience spanning over a decade.</p>

<h2 id="the-early-days">The Early Days</h2>

<p>When I joined QuestionPro, the company had just moved its office from <strong>Nashik to Pune</strong>. Our first workspace was small but cozy—more like a collaborative lab than a traditional office. There were only <strong>eight of us</strong> then, wearing multiple hats and learning as we went.</p>

<p>I started as a <strong>Java Developer</strong>, primarily focused on <strong>bug fixes, feature testing,</strong> and writing or updating <strong>help documentation</strong>. Those early tasks might seem small, but they taught me the importance of precision, patience, and understanding the product from the user’s perspective.</p>

<p><img src="/engineering/assets/images/questionpro-pune-team.jpg" alt="QuestionPro Pune Lonavala Trip" /></p>

<h2 id="learning-mentorship-and-confidence">Learning, Mentorship, and Confidence</h2>

<p>The next two years were a turning point. Under the mentorship of <strong>Shri and Anish</strong>, I began to take on larger challenges, own modules, and develop new features. Their guidance shaped not only my technical skills but also my professional mindset.</p>

<p>They taught me that leadership isn’t just about giving directions—it’s about <strong>owning outcomes</strong> and continuously learning. Even today, they remain my <strong>role models</strong>, and I still find myself learning from their approach to problem-solving and innovation.</p>

<h2 id="stepping-into-leadership">Stepping into Leadership</h2>

<p>As QuestionPro grew, so did my responsibilities. I transitioned into a <strong>Team Lead (TL)</strong> role, managing a small team while continuing to code and contribute hands-on. This phase was both exciting and demanding—it pushed me to develop skills in <strong>people management, communication</strong>, and <strong>strategic thinking</strong>.</p>

<p>However, what truly made it memorable were the moments outside of <strong>work</strong>—our <strong>Friday night gatherings, table tennis matches</strong>, and <strong>cricket games</strong>. These experiences built lasting friendships and strengthened our culture of collaboration and trust.</p>

<h2 id="working-with-visionary-leaders">Working with Visionary Leaders</h2>

<p>One of the highlights of my career has been the opportunity to work directly with <strong>Vivek Bhaskaran</strong>, our <strong>Founder &amp; CEO</strong>, and <strong>Erik Koto</strong>, our <strong>COO/Ex-CEO</strong>. Having direct access to the leadership team gave me a unique perspective on how decisions are made and how innovation is driven at scale.</p>

<p>The management team at QuestionPro has always been <strong>accessible, supportive</strong>, and <strong>empathetic</strong>. They’ve built a culture where everyone’s ideas are valued, and every challenge is seen as an opportunity to grow.</p>

<h2 id="growth-ownership-and-empowerment">Growth, Ownership, and Empowerment</h2>

<p>Over the years, my focus has shifted from purely technical work to <strong>engineering leadership</strong>. I’ve learned the art of <strong>hiring, mentoring</strong>, and <strong>helping team members</strong> chart their own career paths. Watching people I’ve mentored grow into confident engineers and leaders has been one of the most rewarding aspects of my journey.</p>

<p>Today, as an <strong>Engineering Manager</strong>, I handle <strong>three different products</strong>, each with its own challenges and learning curves. The responsibility is immense, but so is the satisfaction that comes with building scalable, user-centered products that impact thousands of users worldwide.</p>

<h2 id="what-makes-questionpro-special">What Makes QuestionPro Special</h2>

<p>If I were to summarize what makes QuestionPro unique, it would be its <strong>open culture</strong> and <strong>growth mindset</strong>. Here, you’re encouraged to take initiative, challenge assumptions, and experiment with new ideas.</p>

<p>Failures aren’t punished—they’re treated as learning opportunities. That philosophy has allowed me to evolve continuously, both as an engineer and as a leader.</p>

<p><img src="/engineering/assets/images/questionpro-pune-lonavala-trip.jpg" alt="QuestionPro Pune Lonavala Trip" /></p>

<h2 id="looking-backand-ahead">Looking Back—and Ahead</h2>

<p>From debugging my first piece of code to managing multiple teams and products, every step of this journey has taught me something valuable.</p>

<ul>
  <li>
    <p><strong>QuestionPro</strong> has given me more than a career—it has given me a community.</p>
  </li>
  <li>
    <p>It has shown me that growth doesn’t happen by chance; it happens by <strong>staying curious, embracing challenges</strong>, and <strong>never stopping learning</strong>.</p>
  </li>
  <li>
    <p>And most importantly, it has reinforced my belief that <strong>great products are built by great people</strong> who trust and learn from each other.</p>
  </li>
</ul>

<p>As I look to the future, I’m excited to continue this journey of building, leading, and learning—together with a team that feels more like family.</p>

<hr />]]></content><author><name>Kiran Dongare</name></author><category term="Personal Growth" /><category term="Continuous Learning" /><category term="Professional Growth" /><category term="Leadership" /><category term="QuestionPro Culture" /><category term="personal growth" /><category term="professional growth" /><category term="leadership" /><category term="communication" /><category term="team culture" /><category term="people management" /><category term="continuous learning" /><category term="questionpro journey" /><summary type="html"><![CDATA[When I look back on my journey with QuestionPro, which began in March 2012, I’m filled with gratitude, pride, and a deep sense of belonging. What started as my first professional step after earning a degree in Computer Engineering from Pune University has evolved into a career-defining experience spanning over a decade.]]></summary></entry><entry><title type="html">The Art of Not Breaking Things: A CbC Manifesto… a mindset to build polished code</title><link href="https://www.questionpro.com/engineering/software%20engineering%20philosophy/code%20quality%20&%20maintainability/programming%20best%20practices/type%20systems%20&%20safety/backend%20&%20application%20architecture/developer%20mindset%20&%20productivity/correctness-by-construction/" rel="alternate" type="text/html" title="The Art of Not Breaking Things: A CbC Manifesto… a mindset to build polished code" /><published>2026-02-05T00:00:00+00:00</published><updated>2026-02-05T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/software%20engineering%20philosophy/code%20quality%20&amp;%20maintainability/programming%20best%20practices/type%20systems%20&amp;%20safety/backend%20&amp;%20application%20architecture/developer%20mindset%20&amp;%20productivity/correctness-by-construction</id><content type="html" xml:base="https://www.questionpro.com/engineering/software%20engineering%20philosophy/code%20quality%20&amp;%20maintainability/programming%20best%20practices/type%20systems%20&amp;%20safety/backend%20&amp;%20application%20architecture/developer%20mindset%20&amp;%20productivity/correctness-by-construction/"><![CDATA[<p>Writing code is a lot similar to carpentry..If you eyeball the cut, saw like a maniac with confusion in mind, and then later try to fix the gap with glue, you aren’t building a table, you are building a hazard.</p>

<p><strong>Correctness by Construction (CbC)</strong> is the shift from the chaotic energy of “Let’s see if this runs” to the calm confidence of “I know this runs because I made it impossible for it not to.”</p>

<p>p.s. “No, not Complete blood Count (CbC) :3”</p>

<p>Let me talk about potential tradeoffs before jumping into how we could achieve the CbC mindset,</p>

<h3 id="pros">Pros</h3>

<ul>
  <li><strong>Peace</strong>: You spend 90% less time in the debugger (which is just a crime scene where you are both the detective and the murderer).</li>
  <li><strong>Sleep</strong>: Yeah.</li>
</ul>

<h3 id="cons">Cons</h3>

<ul>
  <li><strong>Slower Start</strong>: You have to think before you type. It feels less like “hacking” and more like “engineering.”</li>
  <li><strong>Boredom</strong>: You won’t look as busy to your peers because you aren’t “putting out fires” constantly.</li>
</ul>

<p>Here is the toolkit for your CbC journey, a guide to replacing ‘hope’ with the certainty of flawless, intentional design.</p>

<h2 id="the-prologue-strong-specifications">The Prologue: Strong Specifications</h2>

<p><img src="/engineering/assets/images/cbc-strong-specifications.jpg" alt="Strong Specifications" /></p>

<p>Before you write a single line of code, write the story. Define exactly what the function does,</p>

<ul>
  <li><strong>The Vibe</strong>: Don’t build a house based on a napkin sketch.</li>
  <li><strong>The Habit</strong>: If you can’t explain the constraints in English, you certainly can’t explain them in JavaScript ;)</li>
</ul>

<hr />

<h2 id="the-bouncer-safe-languages--types">The Bouncer: Safe Languages &amp; Types</h2>

<p><img src="/engineering/assets/images/cbc-bouncer.jpg" alt="The Bouncer" /></p>

<p>Use your type system to forbid “impossible” situations. A bug cannot happen if the code literally refuses to compile it.</p>

<p><strong>Don’t:</strong> Build a “Frankenstein” object full of optional flags</p>

<ul>
  <li><strong>Bad</strong>: A request state like</li>
</ul>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nl">isLoading</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="nx">error</span><span class="p">?:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">data</span><span class="p">?:</span> <span class="kr">string</span> <span class="p">}.</span>
</code></pre></div></div>

<ul>
  <li><strong>The Trap</strong>: You can accidentally create a state where isLoading: true and error: “Failed”. Is it loading? Is it broken? The code is confused, and so are you.</li>
</ul>

<p><strong>Do</strong>: Use Discriminated Unions. Force the data to exist only when the state allows it.</p>

<ul>
  <li><strong>Good</strong>: Define distinct states</li>
</ul>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">State</span> <span class="o">=</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="dl">"</span><span class="s2">loading</span><span class="dl">"</span> <span class="p">}</span> <span class="o">|</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="dl">"</span><span class="s2">success</span><span class="dl">"</span><span class="p">;</span> <span class="nl">data</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span>
</code></pre></div></div>

<p>If you check status === ‘loading’, TypeScript physically prevents you from accessing .data. It doesn’t exist yet. The compiler catches the bug before you even run the code.</p>

<h2 id="the-handshake-design-by-contract">The Handshake: Design-by-Contract</h2>

<p><img src="/engineering/assets/images/cbc-design-by-contract.jpg" alt="The Handshake" /></p>

<p>Every function is a business deal.</p>

<ul>
  <li><strong>Preconditions</strong>: “You promise to give me a positive number.”</li>
  <li><strong>Postconditions</strong>: “I promise to return its square root.”</li>
  <li><strong>Invariants</strong>: “I promise the database never catches fire.” If the contract is broken, the program shouldn’t try to limp along, it should shout immediately!</li>
</ul>

<h2 id="the-haiku-modularity--simplicity">The Haiku: Modularity &amp; Simplicity</h2>

<p><img src="/engineering/assets/images/cbc-modularity.jpg" alt="The Haiku" /></p>

<p>Complexity is where bugs hide to reproduce. Keep your components small, pure, and simple.</p>

<ul>
  <li><strong>The Philosophy</strong>: Write code like a Haiku, not a dissertation. If a function does three things, it’s doing two things too many.</li>
</ul>

<h2 id="code-hygiene-avoid-error-prone-patterns">Code Hygiene: Avoid Error-Prone Patterns</h2>

<p><img src="/engineering/assets/images/cbc-lawsuit.jpg" alt="Code Hygiene" /></p>

<p>Some patterns are just slippery floors waiting for a lawsuit.</p>

<ul>
  <li><strong>Don’t</strong>: Use shared mutable state (global variables that everyone touches). That’s like sharing a toothbrush.</li>
  <li><strong>Do</strong>: Use Pure Functions (same input always equals same output) and Result types instead of throwing random Exceptions.</li>
</ul>

<h2 id="the-epilogue-tests-confirm-they-dont-fix">The Epilogue: Tests Confirm, They Don’t Fix</h2>

<p><img src="/engineering/assets/images/cbc-confirm.jpg" alt="Tests Confirm" /></p>

<p>Testing is your safety net, not your construction plan.</p>

<ul>
  <li><strong>The Shift</strong>: If you rely on tests to find all your bugs, you’ve already lost.</li>
  <li><strong>The Goal</strong>: Build it so solid that the tests are just a formality, a victory lap to prove you were right all along.</li>
</ul>

<h2 id="the-grand-finale">The Grand Finale</h2>

<p>Adopt the mindset of a sculptor. You don’t chip away stone and hope it looks like a horse later. You verify every angle as you go.</p>]]></content><author><name>Syed Saad</name></author><category term="Software Engineering Philosophy" /><category term="Code Quality &amp; Maintainability" /><category term="Programming Best Practices" /><category term="Type Systems &amp; Safety" /><category term="Backend &amp; Application Architecture" /><category term="Developer Mindset &amp; Productivity" /><category term="Correctness by Construction" /><category term="typescript" /><category term="Design by Contract" /><category term="Discriminated Unions" /><category term="Functional Programming Concepts" /><category term="Bug Prevention" /><category term="clean code" /><category term="kiss" /><category term="dry" /><category term="srp" /><category term="software development" /><category term="best practices" /><summary type="html"><![CDATA[Writing code is a lot similar to carpentry..If you eyeball the cut, saw like a maniac with confusion in mind, and then later try to fix the gap with glue, you aren’t building a table, you are building a hazard.]]></summary></entry><entry><title type="html">QuestionPro TechCommit Pune: Scaling Architectures, AI, and Networking</title><link href="https://www.questionpro.com/engineering/technology%20meetup/software%20engineering/data%20science/questionpro-techcommit-pune-scaling-ai-dec-13/" rel="alternate" type="text/html" title="QuestionPro TechCommit Pune: Scaling Architectures, AI, and Networking" /><published>2025-12-05T00:00:00+00:00</published><updated>2025-12-05T00:00:00+00:00</updated><id>https://www.questionpro.com/engineering/technology%20meetup/software%20engineering/data%20science/questionpro-techcommit-pune-scaling-ai-dec-13</id><content type="html" xml:base="https://www.questionpro.com/engineering/technology%20meetup/software%20engineering/data%20science/questionpro-techcommit-pune-scaling-ai-dec-13/"><![CDATA[<p>Are you a developer, software engineer, or data scientist in the <strong>Pune</strong> area looking to connect, collaborate, and dive deep into cutting-edge technology?</p>

<p>QuestionPro is excited to announce <strong>TechCommit</strong>, a focused technical meetup designed to accelerate your growth and provide actionable insights into building high-performance, intelligent systems. Join us in our <strong>Pune office</strong> on <strong>Saturday, December 13th</strong>, for two power-packed sessions!</p>

<h2 id="️-essential-event-details">🗓️ Essential Event Details</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Detail</th>
      <th style="text-align: left">Information</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>What</strong></td>
      <td style="text-align: left">QuestionPro TechCommit Developer Meetup</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>When</strong></td>
      <td style="text-align: left">Friday, December 13th (10:00 AM – 1:30 PM IST)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Where</strong></td>
      <td style="text-align: left"><a href="https://maps.app.goo.gl/b2j8oBmmtWum9GAp7">QuestionPro, 1B, Nano Space IT Park, Baner, Pune, Maharashtra</a></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Registration Deadline</strong></td>
      <td style="text-align: left">Thursday, December 11th</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="️-agenda-deep-dives-by-questionpro-experts">🎙️ Agenda: Deep Dives by QuestionPro Experts</h2>

<p>We have lined up two high-impact talks from our senior experts, focusing on the real-world challenges of building highly available systems and leveraging Artificial Intelligence for data analysis.</p>

<h3 id="talk-1-from-zero-to-10k-live-users-scaling-real-time-engagement-platforms">Talk 1: From Zero to 10k Live Users: Scaling Real-time Engagement Platforms</h3>

<p><strong>Speaker: <a href="https://www.linkedin.com/in/rohan-rao-kn8629/">Rohan Rao</a>, Software Engineer</strong></p>

<p>Building a real-time platform that handles thousands of concurrent users is a critical challenge. Rohan Rao, a Software Engineer with over 7 years of experience, will share lessons learned and practical strategies for:</p>

<ul>
  <li>Designing resilient and <strong>scalable architectures</strong>.</li>
  <li>Implementing <strong>high-performance solutions</strong> that maintain speed under heavy load.</li>
</ul>

<h3 id="talk-2-ai-in-action-building-a-smarter-topic-analysis-system">Talk 2: AI in Action: Building a Smarter Topic Analysis System</h3>

<p><strong>Speaker: <a href="https://www.linkedin.com/in/pratikdhulubulu/">Pratik Dhulubulu</a>, Dedicated Data Scientist</strong></p>

<p>Artificial intelligence is rapidly transforming how companies understand user data. Pratik Dhulubulu, a Data Scientist with 4+ years of experience, will move past the hype and demonstrate how to:</p>

<ul>
  <li>Leverage AI to <strong>solve complex data problems</strong>.</li>
  <li>Build a sophisticated, data-backed <strong>topic analysis system</strong> using modern <strong>Python</strong> techniques.</li>
</ul>

<hr />

<h2 id="why-this-pune-tech-meetup-is-a-must-attend">Why This Pune Tech Meetup Is a Must-Attend</h2>

<ol>
  <li><strong>Focused Learning:</strong> Gain high-value, technical knowledge from experienced practitioners in a short, efficient format.</li>
  <li><strong>Networking:</strong> Connect face-to-face with fellow software engineers, data scientists, and developers in the vibrant <strong>Pune tech scene</strong>.</li>
  <li><strong>Community:</strong> Discuss the real-world implementation challenges of <strong>scaling</strong> and <strong>AI</strong> in a professional environment.</li>
</ol>

<h2 id="secure-your-spot">Secure Your Spot</h2>

<p>Spaces for this <strong>developer meetup in Pune</strong> are limited. Don’t miss out!</p>

<p>The <strong>last day to register is Thursday, December 11th</strong>.</p>

<p><strong><a href="https://techcommit.questionpro.com/13-dec-2025">Register Now</a></strong></p>

<p>We look forward to connecting with you at the QuestionPro office in Baner!</p>]]></content><author><name>QuestionPro Events Team</name></author><category term="Technology Meetup" /><category term="Software Engineering" /><category term="Data Science" /><category term="Pune Tech Meetup" /><category term="Scaling Architecture" /><category term="Real-time Systems" /><category term="AI" /><category term="Data Science" /><category term="QuestionPro" /><category term="developer meetup" /><summary type="html"><![CDATA[Are you a developer, software engineer, or data scientist in the Pune area looking to connect, collaborate, and dive deep into cutting-edge technology?]]></summary></entry></feed>