<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hassan</title>
    <description>The latest articles on DEV Community by Hassan (@hassan_4e2f0901edda).</description>
    <link>https://dev.to/hassan_4e2f0901edda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3791691%2Fd6db6c13-f778-4c01-a8ef-1ae130062719.png</url>
      <title>DEV Community: Hassan</title>
      <link>https://dev.to/hassan_4e2f0901edda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hassan_4e2f0901edda"/>
    <language>en</language>
    <item>
      <title>Your Extraction Agent Is 94% Accurate and Still Ships Wrong Quantities</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 05 Nov 2026 06:09:27 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/your-extraction-agent-is-94-accurate-and-still-ships-wrong-quantities-223g</link>
      <guid>https://dev.to/hassan_4e2f0901edda/your-extraction-agent-is-94-accurate-and-still-ships-wrong-quantities-223g</guid>
      <description>&lt;p&gt;&lt;em&gt;Document-level accuracy hides the errors that cost money. Score fields, weight them by consequence, and gate every prompt change in CI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A team demos an agent that reads supplier quotes and customer RFQs and turns them into structured line items. The PDF-to-JSON accuracy is reported as 94%. Two weeks into the pilot, a customer receives a quote for 120 units that should have been 12. The description field was perfect. The quantity was off by a digit, and no dashboard showed it, because the metric averaged everything together.&lt;/p&gt;

&lt;p&gt;This is the most common gap we see in LLM extraction projects: the model is good enough, and the measurement is not. If you cannot say how often the quantity field is wrong on scanned PDFs from your top five suppliers, you do not know whether the system works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Averaged accuracy rewards the wrong behavior
&lt;/h2&gt;

&lt;p&gt;Extraction outputs have fields of very different weight. A mangled product description gets noticed by a human in seconds. A wrong unit (m2 versus m3, piece versus pack of ten) or a shifted decimal in a quantity flows straight into a quote, an order, or an ERP posting.&lt;/p&gt;

&lt;p&gt;A single accuracy number blends these together. Say a document has 8 fields per line, and the model nails 7 of them on every line. You report 87.5% field accuracy, and the one wrong field per line is the quantity. The number looks fine. The system is useless.&lt;/p&gt;

&lt;p&gt;Document format makes it worse. Native PDFs, scanned faxes, Excel sheets with merged cells and hand-edited emails behave like different models. An aggregate over all of them tells you nothing about the scans, which is where the failures cluster. In the past month, at least four DACH-funded startups announced agents for legacy-heavy workflows: procurement, B2B trade, travel and building materials (per our market tracking, sourced from Tech.eu weekly recaps). Their domains differ and the documents are messy in the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we run on our own pipeline
&lt;/h2&gt;

&lt;p&gt;Our internal sales pipeline uses LLM agents to research companies, score leads and draft outreach. None of that output is trusted on its own. Each agent run is checked against acceptance criteria: required fields present, a justification attached to every score dimension, a source URL on every claim. Before any first email leaves, a person approves it. The model drafts; deterministic code and a human decide what ships.&lt;/p&gt;

&lt;p&gt;That setup is small, and it taught us the lesson that applies to client work: validation and review catch more than prompt tweaking does. When a field fails a check, the cheapest fix is almost never a longer prompt. It is a rule, a lookup table or a review step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the eval harness before the second prompt revision
&lt;/h2&gt;

&lt;p&gt;The harness is not complicated, and it should exist before anyone rewrites the prompt for the first time. We build these with the client's domain expert, because only they can label the gold set correctly.&lt;/p&gt;

&lt;p&gt;Start with a gold set of 50 to 100 real documents, stratified by source: native PDF, scan, spreadsheet, email body. A domain expert labels every line item. Fifty documents with 20 lines each is already 1,000 labeled lines, enough to see patterns. Synthetic documents are not a substitute, because the failures come from real-world mess.&lt;/p&gt;

&lt;p&gt;Score each field with its own match rule. Use exact match on article numbers and quantities, canonicalized match on units, fuzzy match on descriptions. Lines must be aligned first, by article number where it exists and by position otherwise. Alignment bugs in the harness produce phantom regressions, so test the aligner on its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article_no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article_no&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;article_no&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article_no&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article_no&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Decimal, never float&lt;/span&gt;
    &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nf"&gt;canonicalUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;canonicalUnit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// CI gate: fail the build if any critical field regresses on any stratum&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stratum&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;strata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stratum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stratum&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`regression: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stratum&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the gate on each document type separately. A prompt change that lifts native PDFs by two points and drops scans by six should fail, and an average would let it through.&lt;/p&gt;

&lt;p&gt;Pin model versions and record every result. Store each eval run with the prompt hash, model identifier and per-field numbers. When a provider updates a model behind an alias, you will see the shift in the next scheduled run instead of in a customer complaint. Treat prompts as versioned artifacts with the same discipline as code; we wrote about that separately in our post on prompt versioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route to humans with deterministic checks, not model confidence
&lt;/h2&gt;

&lt;p&gt;Self-reported confidence from an LLM is poorly calibrated. A model can return 0.97 on a field it got wrong. Use signals you can verify instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema validation with a strict parser (Zod in TypeScript, Pydantic in Python). Reject, do not coerce, anything outside the enum of known units.&lt;/li&gt;
&lt;li&gt;Cross-field arithmetic. If quantity times unit price does not equal the line total within rounding, flag the line.&lt;/li&gt;
&lt;li&gt;Two-pass disagreement. Extract twice with different prompts or models and compare critical fields. Disagreement goes to review. This roughly doubles token cost on the extraction step, so apply it to critical fields only if cost matters.&lt;/li&gt;
&lt;li&gt;Plausibility against history. A quantity of 120 where the same customer has ordered 10 to 15 for two years deserves a second look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flagged lines go into a review queue in Postgres with the document image next to the extracted values. Every correction a reviewer makes is stored and, after a spot check, promoted into the gold set. After a few months the gold set reflects your real traffic, and the harness gets sharper without extra labeling projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staffing this
&lt;/h2&gt;

&lt;p&gt;The work splits into three parts: the extraction service (Python or Node.js), the eval and review tooling (TypeScript for the reviewer UI, Postgres underneath), and the labeling process with a domain expert. The first two are ordinary backend engineering. Teams with strong domain knowledge and few backend engineers often skip the harness because it looks like overhead, then spend the pilot debugging by anecdote. A backend engineer who has built one before can stand up a first version quickly, and that costs far less than one wrong quote reaching a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Report accuracy per field and per document type. A blended number hides the failures that matter.&lt;/li&gt;
&lt;li&gt;Weight fields by consequence. Quantities, units and identifiers get exact-match rules and a CI gate; descriptions get fuzzy matching.&lt;/li&gt;
&lt;li&gt;Gate every prompt, model and parser change against a gold set of 50 to 100 real documents, and pin model versions.&lt;/li&gt;
&lt;li&gt;Route to human review using schema checks, arithmetic and cross-pass disagreement. Do not rely on the model's own confidence.&lt;/li&gt;
&lt;li&gt;Feed reviewer corrections back into the gold set so the harness reflects real traffic.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>typescript</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Replayable Credit Decisions: The Backend Pattern Auditors and Engineers Both Want</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 29 Oct 2026 06:08:49 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/replayable-credit-decisions-the-backend-pattern-auditors-and-engineers-both-want-400c</link>
      <guid>https://dev.to/hassan_4e2f0901edda/replayable-credit-decisions-the-backend-pattern-auditors-and-engineers-both-want-400c</guid>
      <description>&lt;p&gt;&lt;em&gt;If you cannot re-run a lending decision from last March and get the same answer, you do not have an audit trail. You have a log file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A merchant asks why their credit line was declined in March. Compliance wants the answer by Friday. The engineer who wrote the underwriting rules left in June, the rules have been edited eleven times since, and the bureau response was never stored. All that remains is a row with &lt;code&gt;status = 'declined'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the normal state of lending backends that grew from a prototype. Underwriting starts as an &lt;code&gt;if&lt;/code&gt; block in a service. It becomes a config file, then a feature-flagged config file, then a set of rules that product managers adjust directly. Each step is reasonable. Together they destroy your ability to explain a past decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision is a function of inputs and rules, and you are storing neither
&lt;/h2&gt;

&lt;p&gt;Most schemas store the outcome. Some store the score. Almost none store the exact inputs the rules saw, or the exact version of the rules that ran. Both are required to reproduce a decision.&lt;/p&gt;

&lt;p&gt;The gap matters more now than it did three years ago. The CJEU's SCHUFA ruling (C-634/21, December 2023) held that producing a credit score can itself count as an automated decision under GDPR Article 22 when a lender relies on it heavily. For natural persons, including sole traders, that brings explanation and human-review obligations. The EU AI Act lists creditworthiness assessment of natural persons as a high-risk use case in Annex III. And DORA (Regulation 2022/2554, applicable since January 2025) pushes financial entities and their ICT providers toward demonstrable control over change management and incident reconstruction.&lt;/p&gt;

&lt;p&gt;None of these regulations prescribe a schema. All of them are easier to satisfy if any past decision can be reconstructed on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we see when lending teams hit this wall
&lt;/h2&gt;

&lt;p&gt;In the fintech backends we have worked on or reviewed, the failure mode is consistent. Rules live in three places: code, a database table that ops edits, and a third-party decisioning tool. Nobody can say which version ran for a given application. Bureau and bank-data responses are fetched at decision time and discarded after the score is computed. When a regulator, an auditor or a funding partner asks for reconstruction, an engineer spends days piecing it together from application logs, and the answer is "probably this."&lt;/p&gt;

&lt;p&gt;The second symptom shows up in engineering velocity. Teams become afraid to change rules because they cannot tell what a change would have done to historical applications. A rules change is shipped on a Friday with a prayer instead of a diff.&lt;/p&gt;

&lt;p&gt;Both symptoms have the same cause, and the fix is not exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the evaluator pure, snapshot the inputs, version the rules
&lt;/h2&gt;

&lt;p&gt;Three decisions give you replayability. None of them need a new platform. PostgreSQL and a disciplined module boundary are enough.&lt;/p&gt;

&lt;p&gt;First, the evaluator is a pure function. It takes a rule spec and an inputs object and returns an outcome with reason codes. No clock reads, no HTTP calls, no database access, no randomness. Everything the decision needs arrives as an argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DecisionInputs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;applicationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;evaluatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// passed in, never Date.now()&lt;/span&gt;
  &lt;span class="nl"&gt;applicant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicantFacts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;bureau&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BureauSnapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// stored response, not a live call&lt;/span&gt;
  &lt;span class="nl"&gt;bankData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BankDataSnapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RuleSpec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DecisionInputs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReasonCode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;declined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The side effects happen before the call. An orchestrating NestJS service fetches the bureau data and bank data, writes them to storage, then hands the snapshot to &lt;code&gt;evaluate&lt;/code&gt;. The orchestration is messy and allowed to be. The evaluator is not.&lt;/p&gt;

&lt;p&gt;Second, rules are immutable, content-addressed versions. A rule change creates a new row. Nothing is edited in place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;rule_versions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;version&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content_hash&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- sha256 of the canonical spec&lt;/span&gt;
  &lt;span class="n"&gt;spec&lt;/span&gt;         &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_by&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;application_id&lt;/span&gt;  &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rule_version_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;rule_versions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;inputs&lt;/span&gt;          &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- full snapshot, incl. bureau response&lt;/span&gt;
  &lt;span class="n"&gt;outcome&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reasons&lt;/span&gt;         &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;decided_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule_versions&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;REVOKE&lt;/code&gt; is the point. An audit trail the application can rewrite is a suggestion. Add a trigger that rejects updates for the roles that still hold the privilege, and keep the migration role separate from the runtime role.&lt;/p&gt;

&lt;p&gt;If rules must be code rather than data, the equivalent is a git SHA stored on every decision row, plus a build that refuses to deploy rule changes without it.&lt;/p&gt;

&lt;p&gt;Third, retention is a decision made explicitly. Storing full bureau responses indefinitely conflicts with GDPR storage limitation. Pick a retention window per data class with your DPO, encrypt the snapshot column, and delete on schedule. The decision row can keep its hash of the inputs after the payload expires, which proves integrity even when replay is no longer possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay turns rule changes from a gamble into a diff
&lt;/h2&gt;

&lt;p&gt;Once decisions are replayable, you get something more useful than compliance: a test suite built from production history.&lt;/p&gt;

&lt;p&gt;Before a new rule version ships, run it against the last few thousand stored inputs and compare outcomes to what actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pnpm replay --candidate underwriting@v14 --baseline underwriting@v13 --last 5000

outcomes changed: 212 / 5000 (4.2%)
  approved -&amp;gt; declined: 31
  declined -&amp;gt; approved: 181
top reason codes driving change: CASHFLOW_VOLATILITY (140), SECTOR_CAP (52)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A product manager can see that a "small tweak" would have flipped 181 declines, and decide whether that is the intent. Risk gets a number before release instead of a surprise after it. The same harness works as a regression test in CI: pin a golden set of inputs and fail the build if the outcomes change without an explicit approval.&lt;/p&gt;

&lt;p&gt;One caveat to state plainly. Replay reproduces the rules engine, not the world. If a decision depends on a model, pin the model artifact and its version as part of the rule spec. If it depends on an external call, the snapshot is the record and the live source is irrelevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a small team goes wrong
&lt;/h2&gt;

&lt;p&gt;Retrofitting is cheap when the service already has a clean seam between data fetching and rules. It gets expensive when rules are tangled with I/O, because the real work is untangling them. Retrofits tend to stall at this point, since the feature roadmap does not wait.&lt;/p&gt;

&lt;p&gt;This is the kind of work where an embedded engineer with backend depth in PostgreSQL and TypeScript pays off. It is unglamorous, it touches the core of the product, and it needs someone who will stay long enough to see the replay harness adopted by the team that owns the rules. We hire engineers for that specific profile and embed them in the client's team, so the refactor is owned by someone who knows the codebase, not delivered as a document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Store the inputs and the rule version with every decision. The outcome alone cannot be explained later.&lt;/li&gt;
&lt;li&gt;Keep the evaluator pure. Fetch external data first, snapshot it, then evaluate.&lt;/li&gt;
&lt;li&gt;Make rule versions immutable and revoke UPDATE and DELETE from the runtime database role.&lt;/li&gt;
&lt;li&gt;Decide retention per data class up front, so replayability does not collide with GDPR storage limitation.&lt;/li&gt;
&lt;li&gt;Use replay against historical inputs as the release gate for every rules change.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;SifrVentures builds dedicated engineering teams for tech companies. Based in Berlin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Draft created 2026-10-22 by the CTO agent. Pending partner review before publishing as a LinkedIn article.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>hiring</category>
      <category>startup</category>
    </item>
    <item>
      <title>Your AI Agent Will Retry. Your Booking System Will Not Notice.</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 29 Oct 2026 06:08:38 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/your-ai-agent-will-retry-your-booking-system-will-not-notice-3e0</link>
      <guid>https://dev.to/hassan_4e2f0901edda/your-ai-agent-will-retry-your-booking-system-will-not-notice-3e0</guid>
      <description>&lt;p&gt;&lt;em&gt;Letting an agent write to a legacy system of record takes an intent log, not a better prompt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent calls &lt;code&gt;create_booking&lt;/code&gt;. The legacy API takes 31 seconds and the HTTP client times out. The orchestrator retries the step. The second call succeeds, and the customer now holds two reservations and a confused confirmation email. The prompt was fine. The model was fine. Nothing in the stack knew that the first call had actually gone through.&lt;/p&gt;

&lt;p&gt;Every agent pilot we look at passes its demo on read-only tools: search, lookup, summarize. The trouble starts with the first tool that changes state. Booking, order entry, ticket creation, ERP postings. That is the write path, and it needs the same engineering you would give a payment flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  An agent has three retry layers, and the legacy system sees all of them
&lt;/h2&gt;

&lt;p&gt;The first layer is the HTTP client. Most SDKs retry on connection errors and 5xx responses by default. The second is the orchestrator. Whether you use LangGraph, Temporal, a queue worker, or a loop you wrote yourself, a failed step gets re-run, and a redelivered message gets processed twice. At-least-once delivery is the default contract of nearly every queue you will use.&lt;/p&gt;

&lt;p&gt;The third layer is unique to agents. The model reads a timeout or an ambiguous response, decides the action probably failed, and calls the tool again. This retry is not byte-identical to the first. The arguments may differ in small ways: &lt;code&gt;"adults": 2&lt;/code&gt; versus &lt;code&gt;"adults": "two"&lt;/code&gt;, a date in a different format, an extra optional field. An idempotency key derived from the raw request body will not catch it.&lt;/p&gt;

&lt;p&gt;Stripe popularized client-supplied idempotency keys for exactly this problem, and they work well when the downstream system honors them. Many systems of record do not. SOAP interfaces, batch-file ERPs, and older reservation backends often have no idempotency concept at all. Some will happily create a duplicate and return a fresh ID each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is not a failure, and your agent needs to be told that
&lt;/h2&gt;

&lt;p&gt;The most damaging bug in these systems is a tool wrapper that turns a timeout into the string &lt;code&gt;"Error: request failed"&lt;/code&gt;. The model reads that as a failure and does what a helpful assistant does: it tries again.&lt;/p&gt;

&lt;p&gt;The honest state after a timeout is unknown. The request may have been rejected, may still be queued, or may have committed. Treat it as a fourth outcome next to success, rejection, and error. The agent should receive "outcome pending, do not retry" and a reconciler should find out what happened.&lt;/p&gt;

&lt;p&gt;We apply the same discipline to our own internal tooling. Our outreach pipeline runs agents that draft and schedule emails, and the sender records a marker per lead and per email step, checked before every send. The agent never decides whether an email already went out. A file on disk does. The mechanism is almost embarrassingly low-tech, and it has survived crashed runs and re-triggered cron jobs because the check lives outside the model. Production systems need the same property with a stronger store behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put an intent log between the model and the legacy API
&lt;/h2&gt;

&lt;p&gt;The pattern is small. The agent never calls the legacy system directly. It calls a tool wrapper that writes an intent row to Postgres and returns. A separate worker executes intents against the downstream system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tool_intents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;intent_key&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_id&lt;/span&gt;       &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tool&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt;         &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
               &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'sent'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;external_ref&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is computed from business intent, not from the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BookingArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// zod: "two" becomes 2, dates become ISO&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_booking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;stableStringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`INSERT INTO tool_intents (intent_key, run_id, tool, args, state)
   VALUES ($1, $2, 'create_booking', $3, 'pending')
   ON CONFLICT (intent_key) DO NOTHING
   RETURNING intent_key`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;intentKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;loadPriorResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intentKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same intent, same answer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema validation does the normalization work. Once the model's arguments have been coerced into a strict type, two retries that mean the same thing hash to the same key, and the second insert hits the primary key and returns the earlier result. If a run can legitimately perform the same action twice, add the plan step or line item ID to the hash so the two intents differ on purpose.&lt;/p&gt;

&lt;p&gt;The worker claims rows with &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt;, which lets several workers drain the table without double-claiming. Writing the intent and enqueuing the work is one transaction, which is the transactional outbox pattern. Before it calls the legacy API, it writes the intent key into whatever free-text reference field the system offers. Almost every booking or ERP interface has one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile unknowns by reading, never by writing again
&lt;/h2&gt;

&lt;p&gt;When the worker's call times out, the intent moves to &lt;code&gt;unknown&lt;/code&gt;. A reconciler job then queries the legacy system for a record carrying that reference. If it finds one, the intent becomes &lt;code&gt;confirmed&lt;/code&gt; with the external ID. If nothing appears after a defined window, it becomes &lt;code&gt;rejected&lt;/code&gt; and can be retried safely under the same key, because the reconciler has verified that nothing exists.&lt;/p&gt;

&lt;p&gt;Meanwhile the agent sees a structured result such as &lt;code&gt;{"status":"pending","intent":"..."}&lt;/code&gt;. A model told that a booking is pending will tell the customer it is being processed. A model told that a call failed will try something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split the dangerous tools in two
&lt;/h2&gt;

&lt;p&gt;Give the model &lt;code&gt;hold_booking&lt;/code&gt; and &lt;code&gt;confirm_booking&lt;/code&gt; instead of one &lt;code&gt;create_booking&lt;/code&gt;. A hold carries a TTL and expires on its own, so a wrong hold costs nothing. Confirmation is the irreversible step, and it can sit behind a policy check, an amount threshold, or a human approval click. Our own outreach pipeline works this way: agents draft freely, and a person approves before the first email to a new contact goes out.&lt;/p&gt;

&lt;p&gt;This split also gives you a clean place to enforce limits. The model can propose a thousand holds. Only a bounded number can ever become confirmations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it takes to build
&lt;/h2&gt;

&lt;p&gt;None of this needs an ML team. It is Postgres, a worker process, schema validation, and a reconciler, which makes it a solid fit for one senior backend engineer working in NestJS or Python and owning the integration boundary. The unglamorous parts, such as the reconciler and the monitoring on &lt;code&gt;unknown&lt;/code&gt; intents, decide whether the agent can be trusted with real customers. Teams under funding pressure tend to postpone them until the first duplicate order arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Agents retry at three layers: HTTP client, orchestrator, and the model itself. Only the first two are visible in your logs.&lt;/li&gt;
&lt;li&gt;Treat a timeout as &lt;code&gt;unknown&lt;/code&gt;, tell the model "pending, do not retry", and resolve the state by reading the downstream system.&lt;/li&gt;
&lt;li&gt;Derive idempotency keys from validated, normalized business arguments, not from raw request bodies or LLM-generated call IDs.&lt;/li&gt;
&lt;li&gt;Route every state-changing tool through an intent table in Postgres, with a worker and a reconciler between the model and the system of record.&lt;/li&gt;
&lt;li&gt;Split irreversible actions into a cheap, expiring hold and a gated confirmation.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>startup</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Platform Engineering After Series A: The Kubernetes Debt That Slows Every Engineer on Your Team</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 05:58:43 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/platform-engineering-after-series-a-the-kubernetes-debt-that-slows-every-engineer-on-your-team-1b13</link>
      <guid>https://dev.to/hassan_4e2f0901edda/platform-engineering-after-series-a-the-kubernetes-debt-that-slows-every-engineer-on-your-team-1b13</guid>
      <description>&lt;p&gt;&lt;em&gt;Most SaaS teams hit a DevOps inflection point 12-18 months post-funding. By the time it's visible, one person holds all the infrastructure knowledge and every deploy carries risk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the moment a Series A closes, a startup's infrastructure is running exactly as well as it needs to. One engineer set up the Kubernetes cluster eight months ago. It hasn't broken. Deploys take 20 minutes but they work. The team is focused on product. Nobody is thinking about platform engineering.&lt;/p&gt;

&lt;p&gt;Twelve months later, that cluster is the single biggest constraint on engineering velocity, and the problem doesn't appear in any sprint board.&lt;/p&gt;

&lt;p&gt;This is not an edge case. It is the default trajectory for engineering teams between €5M and €30M in funding, across fintech, healthtech, and B2B SaaS in DACH. The pattern repeats because the incentives at seed stage reward individuals who ship, not teams who document. That's a rational tradeoff early on. It stops being rational once the team doubles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Founding DevOps Pattern Breaks Down
&lt;/h2&gt;

&lt;p&gt;At seed, a single engineer owns all infrastructure. This is correct. There is nothing to run at scale, and the fastest path is one person who knows where everything lives. They install Kubernetes on a managed cloud provider (EKS or GKE, usually), write Helm charts or skip Helm and commit manifests directly, wire up a CI/CD pipeline to the main branch, and move on.&lt;/p&gt;

&lt;p&gt;Three things go wrong quietly over the next year.&lt;/p&gt;

&lt;p&gt;Staging drifts from production. Staging is lower-cost, so it doesn't get the same updates. Teams start shipping directly to production "just this once." Six months of "just this once" later, staging is an ornament. Nobody catches regressions before they reach users.&lt;/p&gt;

&lt;p&gt;Infrastructure state ends up somewhere nobody planned for. Terraform state in a local file, or an S3 bucket without state locking. Secrets committed to environment variables in deployment YAML. A load balancer rule that was added manually in the cloud console and never codified. A Kubernetes namespace with resource limits last touched 14 months ago. None of this is visible until it breaks under pressure.&lt;/p&gt;

&lt;p&gt;Then the bus factor arrives. The founding DevOps engineer leaves, moves to a different project, or takes parental leave. Nobody else fully understands the cluster. The next time something breaks at 2am, a backend engineer is SSHing into a jump host from a runbook that is nine months out of date. This is when leadership usually realizes they have an infrastructure problem, which is the worst possible time to discover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Berlin Fintech Was Losing 60% of a Senior Engineer to Infrastructure Overhead
&lt;/h2&gt;

&lt;p&gt;At a Berlin fintech we worked with, the lead backend engineer was spending three days a week on infrastructure. Not because they wanted to. Because they were the only person who could. Two of their five Kubernetes namespaces had no live documentation. GitHub Actions pipelines ran only on main branch merges, with no PR-level checks or preview environments. Terraform state lived in an S3 bucket without DynamoDB state locking, and two engineers had accidentally run concurrent applies on the same workspace in the same quarter. Both applied cleanly. Nobody knew which one won until they checked the cloud console.&lt;/p&gt;

&lt;p&gt;Their engineering team was capable and the product was shipping. But 60% of one senior engineer's time had been redirected to infrastructure maintenance, and the team had stopped tracking it as overhead because it had become normal.&lt;/p&gt;

&lt;p&gt;The cost wasn't visible in velocity metrics because nobody had a baseline to compare against. It showed up in conversations: "we need to hire another backend engineer." The real problem was that one backend engineer wasn't doing backend engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitOps Migration: What Six Weeks Buys You
&lt;/h2&gt;

&lt;p&gt;The fix is not glamorous, and it doesn't require a full-time platform engineering team. For a team running 6-20 backend services on Kubernetes, six weeks of focused work by one or two embedded platform engineers will address 80% of the operational risk.&lt;/p&gt;

&lt;p&gt;The pattern that works consistently at this scale:&lt;/p&gt;

&lt;p&gt;Start with ArgoCD for cluster state management. Every Kubernetes resource that should exist in the cluster lives in a Git repository. ArgoCD continuously reconciles that Git state to the actual cluster state. Nobody runs &lt;code&gt;kubectl apply&lt;/code&gt; manually in production. Drift is detected in the ArgoCD UI before it causes an incident. "What is running in production right now?" becomes a Git blame command, not a 20-minute investigation.&lt;/p&gt;

&lt;p&gt;Alongside ArgoCD, standardize on Helm. Running eight backend services without Helm usually means maintaining eight nearly-identical collections of deployment YAML. A base Helm chart template eliminates that duplication. Security patches, resource limit adjustments, and environment variable structure changes become a one-line update per service instead of eight separate PRs. For a team deploying twice a week, this compounds quickly.&lt;/p&gt;

&lt;p&gt;Secrets need to leave environment variables in deployment YAML. External Secrets Operator with AWS Secrets Manager (or GCP Secret Manager) handles this: credentials are managed in the cloud provider's secrets service and synced into Kubernetes at deploy time. Rotating a credential becomes an AWS console action. It never touches Git history.&lt;/p&gt;

&lt;p&gt;Terraform gets remote state and DynamoDB state locking: one workspace per environment, &lt;code&gt;terraform plan&lt;/code&gt; output reviewed in CI before any apply is permitted. Concurrent apply accidents, the kind that silently overwrite each other, become structurally impossible. Infrastructure changes go through the same review process as application code.&lt;/p&gt;

&lt;p&gt;The deployment pipeline itself: GitHub Actions on merge to main builds and pushes a Docker image to ECR, updates the image tag in the ArgoCD Git repository, and triggers sync. Staging runs the pipeline first, with a manual promotion gate before production. This is not sophisticated engineering. It is reliable, auditable, and understandable by any engineer on the team without three hours of context transfer.&lt;/p&gt;

&lt;p&gt;For the Berlin fintech team above, this migration took one embedded DevOps engineer six weeks working alongside their existing backend team. By week eight, the engineer who had been spending three days a week on infrastructure was down to two hours a week on operational tasks. Four months later, they hired a second backend engineer, and the new hire was contributing to production within three weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Signals That the Inflection Point Has Already Arrived
&lt;/h2&gt;

&lt;p&gt;The right moment to address Kubernetes infrastructure debt is before a key person leaves, not after. These signals indicate the inflection point:&lt;/p&gt;

&lt;p&gt;Deploy frequency has dropped over six months without a deliberate decision to slow down. More than one engineer raised infrastructure as a blocker in the last quarter. Your on-call rotation is one person. Nobody can confidently answer "what is deployed in production right now?" without SSHing somewhere.&lt;/p&gt;

&lt;p&gt;The wrong solution at this stage is hiring a full-time senior DevOps engineer unless you already have four to six backend engineers generating infrastructure work. One platform hire at 10 engineers spends their first three months fixing accumulated problems and their next three months maintaining the fix, with no capacity for the proactive work that actually scales a team.&lt;/p&gt;

&lt;p&gt;The pattern that works for teams between 8 and 25 engineers: one to two embedded platform engineers for a defined migration scope (12-16 weeks), with explicit knowledge transfer and runbooks before handoff. The goal is not a permanent platform team. The goal is an infrastructure that any backend engineer can operate without calling the one person who set it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure debt at Series A compounds faster than application debt because it affects every engineer, not just the team working on the affected service. When your most experienced backend engineer becomes your de facto DevOps engineer, you've lost your most expensive resource to the wrong problem.&lt;/li&gt;
&lt;li&gt;GitOps with ArgoCD pays back within eight weeks, not six months. The setup investment is real, and the operational improvement is faster than most teams expect.&lt;/li&gt;
&lt;li&gt;The founding DevOps pattern is a rational seed-stage decision that needs active dismantling in the first 12 months post-Series A. The window before it becomes a crisis is shorter than it looks.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;SifrVentures builds dedicated engineering teams for tech companies. Based in Berlin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Published: 2026-08-27&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>startup</category>
      <category>engineering</category>
      <category>business</category>
    </item>
    <item>
      <title>Why Market Five Breaks Your Payment Backend</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:46:04 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/why-market-five-breaks-your-payment-backend-55dp</link>
      <guid>https://dev.to/hassan_4e2f0901edda/why-market-five-breaks-your-payment-backend-55dp</guid>
      <description>&lt;p&gt;&lt;em&gt;The integration debt that compounds quietly across payment providers, and the architecture decision that stops it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first payment integration is never the problem. You pick Stripe for Germany, wire up the webhook endpoint, handle &lt;code&gt;payment_intent.succeeded&lt;/code&gt;, &lt;code&gt;payment_intent.payment_failed&lt;/code&gt;, and &lt;code&gt;charge.refunded&lt;/code&gt;, build a nightly reconciliation job, and ship. It takes five weeks. Everything works.&lt;/p&gt;

&lt;p&gt;Market two takes four weeks. You add a second provider, extend the webhook handler with a conditional block, duplicate the reconciliation logic with minor differences for the new export format. The provider uses different event names for the same lifecycle states. You normalize them manually. Still fine.&lt;/p&gt;

&lt;p&gt;By market five, the PR review for a new provider integration takes four days before anyone approves it, because no one fully understands the webhook handler anymore. Each provider fails differently. Refund flows are all slightly wrong in different ways. The reconciliation job is a 700-line script that runs for three hours and occasionally produces negative discrepancies no one can explain without reading the original engineer's Slack messages from two years ago.&lt;/p&gt;

&lt;p&gt;This is not a code quality problem. It is a structural one, caused by a decision made at integration one that does not show its cost until integration five.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Integration Sets a Pattern
&lt;/h2&gt;

&lt;p&gt;The natural approach when building the first payment integration is to model the provider's API surface directly. Stripe gives you &lt;code&gt;PaymentIntent&lt;/code&gt;, &lt;code&gt;Charge&lt;/code&gt;, &lt;code&gt;Refund&lt;/code&gt;, and &lt;code&gt;Webhook&lt;/code&gt;. You build types that mirror those, write handlers that know they are talking to Stripe, and ship. The code is clean, specific, and correct.&lt;/p&gt;

&lt;p&gt;The problem is that Stripe-shaped code only supports Stripe. Every subsequent provider has a different data model, different webhook vocabulary, different lifecycle semantics, different retry contracts, and different reconciliation export formats. When you add provider two, you have two options: abstract, or duplicate. Most teams duplicate, because abstracting at that point requires a refactor that feels risky for a provider that has not shipped yet.&lt;/p&gt;

&lt;p&gt;That decision does not hurt immediately. It hurts when:&lt;/p&gt;

&lt;p&gt;A provider sends a duplicate webhook for an event the others send once. Your idempotency logic was written for Stripe's guarantee, not this provider's behavior. Payments start double-counting in the settlement report.&lt;/p&gt;

&lt;p&gt;A payment that fails at provider A needs retrying at provider B. There is no shared payment entity in your system — just provider-specific records with incompatible status enums. You build the retry logic as a special case, which becomes technical debt the moment you add provider three.&lt;/p&gt;

&lt;p&gt;A compliance audit asks for a full payment lifecycle audit trail across all markets. You can produce it per-provider. You cannot produce a unified view without writing a query layer that joins across three separate data schemas authored at different times.&lt;/p&gt;

&lt;p&gt;None of these are hypothetical. They are the standard second-year failures of a payment backend built integration-by-integration without a shared domain model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens Without the Abstraction
&lt;/h2&gt;

&lt;p&gt;At one client operating across five European markets, each payment integration had been built at the time of market entry — different retry policies, different webhook validation approaches, different error categorizations. The reconciliation process required a separate script per provider because each had a different export format, and those formats had drifted as providers released API updates.&lt;/p&gt;

&lt;p&gt;A new market entry, which should have been a contained backend task, required touching all five existing integrations to normalize the shared error taxonomy the new provider's SDK introduced. Engineers who had not written the original integrations were modifying them. Code review was slow. The timeline slipped.&lt;/p&gt;

&lt;p&gt;The engineering team was not slow. The code was not poorly written. The problem was that five provider adapters had been built in isolation rather than against a shared interface. Every cross-provider operation required coordinating across five data models with no common language between them.&lt;/p&gt;

&lt;p&gt;The rebuild took one backend engineer six weeks: a shared &lt;code&gt;Payment&lt;/code&gt; domain model with a normalized lifecycle (created, authorized, captured, settled, failed, refunded), a provider adapter pattern, and reconciliation logic that ran once against the shared model instead of per-provider. Three additional market entries since then have required writing one adapter each. The reconciliation job has not been touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture That Survives Scale
&lt;/h2&gt;

&lt;p&gt;The abstraction layer that makes multi-provider payment backends maintainable has three components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shared Payment domain model.&lt;/strong&gt; Define the payment lifecycle in your domain vocabulary, not your first provider's vocabulary. &lt;code&gt;PaymentState.Authorized&lt;/code&gt; is not &lt;code&gt;PaymentIntent.requires_capture&lt;/code&gt;. It is a domain concept that maps to different provider states per context. Internal systems — reconciliation, reporting, refund orchestration, customer notifications — work against this model exclusively. They never import provider-specific types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider adapters with explicit contracts.&lt;/strong&gt; Each adapter handles translation for one provider: inbound webhook events to domain events, outbound commands to provider API calls. The adapter boundary contains the inconsistencies. Stripe's idempotency contract is handled inside the Stripe adapter. If Adyen's is different, that is the adapter's problem, not the reconciliation engine's.&lt;/p&gt;

&lt;p&gt;A minimal TypeScript adapter interface covers the surface area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentProviderAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;initiatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PaymentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DomainEvent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;issueRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RefundResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AsyncIterable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ReconciliationRecord&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every provider implementation satisfies this contract. The reconciliation engine, retry scheduler, and refund handler call only the interface. Adding a new provider does not change those systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency at the domain level.&lt;/strong&gt; Build idempotency around a client-generated key your system controls, not around provider-specific guarantees. Store the key with the payment record. Deduplicate incoming webhooks against it before processing. Provider idempotency contracts vary significantly between vendors and change between API versions. Depending on them makes your idempotency guarantees only as strong as your weakest provider's — which you discover during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Build It
&lt;/h2&gt;

&lt;p&gt;The abstraction is not technically complex. The reason most teams skip it at integration one is that the engineering team is also building the core product, and the one-provider path is shorter. That is a rational call in the moment.&lt;/p&gt;

&lt;p&gt;The teams that pay the rebuild cost earliest — at integration two or three, before the pattern is entrenched — do so because they had dedicated backend capacity for the refactor alongside the integration work. The teams that delay do so because the engineers who understand the existing integrations are maintaining them while also shipping the next one.&lt;/p&gt;

&lt;p&gt;Senior backend engineers with payments domain experience in Germany take 38-54 days to hire at the junior end and considerably longer at senior level (Source: Stack Overflow Developer Survey, DACH 2025). For a fintech team entering two new markets per quarter, that hiring timeline conflicts with delivery if the refactor and the next integration are scheduled concurrently on the same people.&lt;/p&gt;

&lt;p&gt;The abstraction is a two-sprint engineering project. The rebuild, once you have five providers, is a six-week one. The earlier it happens, the cheaper it is. That arithmetic is straightforward. What is less obvious is that it requires engineering capacity that is not already allocated to maintaining what you shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Multi-provider payment backends accumulate structural debt when each integration is built against the previous provider's API surface rather than a shared domain model. The cost is invisible until integration four or five.&lt;/li&gt;
&lt;li&gt;Define a &lt;code&gt;Payment&lt;/code&gt; domain model before integration two. Internal systems should never import provider-specific types.&lt;/li&gt;
&lt;li&gt;Provider adapters with a common interface contain inconsistencies at the integration boundary. Reconciliation, retry logic, and refund orchestration stay stable as providers change.&lt;/li&gt;
&lt;li&gt;Build idempotency around a key your system controls. Provider idempotency guarantees are not consistent enough to be your foundation.&lt;/li&gt;
&lt;li&gt;The abstraction rebuild is a two-sprint task at integration two and a six-week task at integration five. Scheduling it requires engineering capacity that is not already allocated to delivery — which means planning it ahead of the next market expansion, not alongside it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>hiring</category>
      <category>engineering</category>
      <category>business</category>
    </item>
    <item>
      <title>Grafting AI Agents onto a Working SaaS Platform Is a Different Engineering Problem</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 13 Aug 2026 05:51:25 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/grafting-ai-agents-onto-a-working-saas-platform-is-a-different-engineering-problem-hgi</link>
      <guid>https://dev.to/hassan_4e2f0901edda/grafting-ai-agents-onto-a-working-saas-platform-is-a-different-engineering-problem-hgi</guid>
      <description>&lt;p&gt;&lt;em&gt;You have a reliable platform, 5,000 customers, and an AI roadmap. The engineering work ahead is not what you planned for.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your platform works. It has been in production for years, serves enterprise customers, and your engineering team knows every corner of it. Then the Series C closes and the board mandate lands: add AI agents. The roadmap is clear. The engineering cost is not.&lt;/p&gt;

&lt;p&gt;Adding an agentic layer to a working SaaS platform is architecturally different from building an AI-native product from scratch. The teams that discover this early ship faster. The ones that discover it in production spend Q3 doing things they thought were done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Existing Platforms Break Under Agentic Load
&lt;/h2&gt;

&lt;p&gt;A mature SaaS backend is built for request/response. A user action triggers a synchronous call, a service handles it, a response comes back. That contract is reliable because it is bounded: one input, one output, predictable latency.&lt;/p&gt;

&lt;p&gt;AI agents break that contract in three specific ways.&lt;/p&gt;

&lt;p&gt;Agent workflows are stateful and multi-step. An agent processing an invoice does not do it in a single call. It extracts data, calls a validation tool, queries an enrichment service, makes a classification decision, and writes back a result. Each step can fail independently. The state has to survive across steps. In a request/response system, there is no standard place for that state to live. Teams improvise, usually with a combination of Redis and database rows, and end up with consistency problems neither their existing monitoring nor their existing retry logic was built to catch.&lt;/p&gt;

&lt;p&gt;Tool calls are synchronous by default in most LLM client libraries, but the tools themselves are often slow. An agent calling an internal ERP endpoint, a regulatory data provider, or a document processing service can block for two to thirty seconds per call. Synchronous tool calls on an existing service mesh, under the thread budget of a Java microservice or a Node.js worker, produce cascading timeouts that look like infrastructure problems but are actually concurrency budget problems.&lt;/p&gt;

&lt;p&gt;Context windows are a new kind of resource to manage. A standard backend service does not manage prompt state. The agentic layer does. As workflows grow more complex and the number of tool calls per agent session increases, the total token cost per workflow grows. Teams that treat LLM API calls like database queries discover at scale that their per-request cost is variable in a way nothing else in their stack is. Rate limits hit in ways that do not resemble any failure mode their on-call runbook anticipated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Have Seen in Practice
&lt;/h2&gt;

&lt;p&gt;At one client running a financial document processing workflow, the existing backend handled thousands of transactions per day across a Java microservice architecture on Kubernetes. Adding an agentic review layer seemed like a contained engineering task. The agent extracted fields from documents, cross-checked them against existing database records, and flagged discrepancies.&lt;/p&gt;

&lt;p&gt;Three weeks into production, two problems appeared that no load test had surfaced. First: agent sessions that processed large documents hit the context limit of the chosen model silently, producing partial outputs that passed schema validation but contained truncated reasoning. The LLM client swallowed the overflow error and returned a 200. The workflow logged success. The flagging was wrong.&lt;/p&gt;

&lt;p&gt;Second: the internal enrichment service called by the agent had a rate limit of 60 requests per minute per API key. Under normal conditions, this was never a problem. The agentic layer sent three to five tool calls per document. At 20 concurrent agent sessions, the enrichment service started returning 429s. The retry logic in the agent client queued retries on the same thread, consuming the worker pool while new agent sessions waited for capacity that was not coming.&lt;/p&gt;

&lt;p&gt;Both problems were solvable. The solutions were not complex: a tokenizer check before each API call that hard-rejected requests over the context limit, and a dedicated async worker queue for tool calls with a circuit breaker on the enrichment endpoint. What took time was diagnosing two failure modes that did not exist before the agentic layer arrived and did not resemble any failure mode the team had seen before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Patterns That Hold Up
&lt;/h2&gt;

&lt;p&gt;Three structural decisions determine whether an agentic layer integrates cleanly or fights with the rest of the platform.&lt;/p&gt;

&lt;p&gt;Keep agent orchestration out of the application service layer. LangGraph, Temporal, and similar workflow engines provide the state management, retry logic, and step visibility that agentic workflows need. Running orchestration inside an existing application service mixes two different execution models in one process: the bounded, synchronous request handler and the stateful, long-running workflow. They do not share resource assumptions well. A dedicated orchestration runtime isolates the failure domains.&lt;/p&gt;

&lt;p&gt;Instrument tool calls as first-class spans, not log lines. Standard OpenTelemetry instrumentation covers infrastructure and application-layer latency. Tool calls are neither. Every call from an agent to an external or internal service should emit a trace span with the tool name, input size, latency, and result code. This makes it possible to answer the question "which tool call is slowing agent sessions?" in minutes rather than hours. Without it, the debugging path is log-searching across systems that were not designed to correlate.&lt;/p&gt;

&lt;p&gt;Treat model version as a deployment artifact. Every team eventually has a production incident caused by a provider-side model behavior change. An alias like &lt;code&gt;gpt-4o&lt;/code&gt; or &lt;code&gt;claude-3-5-sonnet&lt;/code&gt; hides which exact version handled each request. When output quality shifts, there is no clean path from "when did this start?" to "what changed?" Pinning to exact model identifiers and treating upgrades as PRs with evaluation runs removes an entire class of hard-to-diagnose regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Capacity Question No One Plans for in Advance
&lt;/h2&gt;

&lt;p&gt;The architectural problems are solvable, but they require engineering time that most teams have not budgeted when they scope the agentic feature.&lt;/p&gt;

&lt;p&gt;An established platform team is already fully allocated to the platform. The engineers who know the data model and the service boundaries are the ones maintaining it, handling incidents, and shipping the next product quarter. Adding a net-new architectural pattern, with new failure modes and new monitoring requirements, on top of that team's existing capacity is the decision that delays most agentic rollouts by two to four months.&lt;/p&gt;

&lt;p&gt;The teams that ship agentic features on schedule tend to add capacity specifically for the integration work, not because they are understaffed on the core platform but because the integration work requires people who are not already context-switching between production reliability and new development. The work of connecting an LLM orchestration layer to an existing event-driven backend, and making it observable and reliable, is full-time work for the period it takes to do it.&lt;/p&gt;

&lt;p&gt;In Berlin's engineering market, a senior backend engineer with LLM integration experience commands EUR 90-120k annually, and takes four to six months to hire from first posting to first commit (Source: Glassdoor DACH 2025). For a company shipping on a Series C mandate, that timeline is the product risk. The agentic roadmap is not waiting for the hire to clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Request/response architectures do not extend cleanly to stateful, multi-step agent workflows. The failure modes are different and will not show up in your existing monitoring until they hit production.&lt;/li&gt;
&lt;li&gt;Tool call concurrency and model context limits are resource constraints with no equivalent in standard backend services. They need dedicated instrumentation before you need it in an incident.&lt;/li&gt;
&lt;li&gt;Run agent orchestration in a dedicated runtime rather than inside existing application services. The execution models are incompatible at scale.&lt;/li&gt;
&lt;li&gt;Pin model versions exactly. Treat model upgrades like dependency upgrades: deliberate, in a PR, gated on evaluation results.&lt;/li&gt;
&lt;li&gt;The engineers who know your platform are not free to build the agentic layer. Adding capacity specifically for the integration period is the decision that determines whether the agentic feature ships on the Series C timeline or slips into the next cycle.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>engineering</category>
      <category>business</category>
    </item>
    <item>
      <title>Production LLM Observability: What You're Missing When Your AI Feature Goes Dark</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:56:19 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/production-llm-observability-what-youre-missing-when-your-ai-feature-goes-dark-3nli</link>
      <guid>https://dev.to/hassan_4e2f0901edda/production-llm-observability-what-youre-missing-when-your-ai-feature-goes-dark-3nli</guid>
      <description>&lt;p&gt;&lt;em&gt;Three silent failure modes that standard APM misses, and the instrumentation layer that catches them before your users do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your AI feature passes load testing. Latency is under two seconds, error rate is under one percent, the product demo runs clean. You ship it. Three weeks later, a customer reports that the AI-powered output "stopped making sense." Your dashboard shows nothing wrong. The endpoint is returning 200. The logs are quiet.&lt;/p&gt;

&lt;p&gt;This is the observability gap in production AI backends. It is different from anything traditional monitoring tools were built to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Standard APM Fails for LLM-Backed Services
&lt;/h2&gt;

&lt;p&gt;HTTP response codes, p95 latency, and error rates describe the transport layer. For a REST endpoint serving cached data or querying a relational database, that is enough. For an LLM-backed feature, a 200 response tells you almost nothing about whether the system is working correctly.&lt;/p&gt;

&lt;p&gt;A 200 from OpenAI or Anthropic can mean: the model returned a valid response. It can also mean: the model silently truncated your input because it exceeded the context window and returned a coherent-but-incomplete answer. Or: the response is syntactically correct but output quality has shifted because someone updated the system prompt two weeks ago without an evaluation run.&lt;/p&gt;

&lt;p&gt;The failure modes that hit AI-native backends in production do not show up as error spikes on a Grafana dashboard. They show up as a slow erosion of output quality that accumulates over days until a user complains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Silent Context Truncation
&lt;/h3&gt;

&lt;p&gt;Most LLM clients truncate inputs silently when they hit the model's context limit, or throw an exception that gets swallowed by a generic catch block. Your feature continues returning 200s, but the model is working from a partial view of the data. Depending on what gets cut, the output degrades in ways subtle enough to pass automated validation but wrong enough that attentive users notice. The truncation is systematic, predictable, and completely invisible to standard monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limit Cascades
&lt;/h3&gt;

&lt;p&gt;Token-per-minute limits are enforced per API key. At low traffic you will never hit them. At 200 concurrent users, a single high-traffic hour can push you into 429s that your retry logic converts into a thread-blocking cascade. If you are not tracking cumulative token consumption per minute per model, you have no signal before you hit the wall. By the time 429s appear in your error logs, you are already degraded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt Regression
&lt;/h3&gt;

&lt;p&gt;Output quality is a function of three things: your prompt, your model version, and your input data distribution. All three change over time. Model providers update default behavior in minor versions. Engineers edit prompts without evaluation runs. Real user traffic exposes edge cases that your staging environment never did. Without a structured evaluation pipeline, prompt regressions surface as customer complaints rather than CI failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We've Seen
&lt;/h2&gt;

&lt;p&gt;At one client running an AI-powered document processing feature, the backend was handling several hundred LLM calls per day across two providers. Standard monitoring showed nothing unusual. A code review surfaced a context overflow condition that had been silently truncating inputs for eleven days. No user had seen enough degraded outputs to raise a ticket, but the truncation was systematic for documents over a certain length.&lt;/p&gt;

&lt;p&gt;The fix took two hours: a tokenizer check before each API call, plus a hard rejection of requests that would overflow the context window. What was missing before the fix was a dashboard showing the distribution of input token counts per endpoint. That would have flagged the problem on day one.&lt;/p&gt;

&lt;p&gt;A second pattern: a provider-side model version bump. One team was pinned to &lt;code&gt;gpt-4-turbo&lt;/code&gt; by alias, not by exact identifier. The provider's new default version had different formatting behavior for a structured JSON output schema the feature depended on. Schema validation failures started appearing in the retry logs. It took four hours to diagnose because there was no span attribute recording which exact model version had handled each call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Instrumentation Stack
&lt;/h2&gt;

&lt;p&gt;Standard APM covers infrastructure observability. AI-native backends need a second layer on top of it. Four components cover the surface area.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Structured OpenTelemetry Spans on Every LLM Call
&lt;/h3&gt;

&lt;p&gt;Every call to an LLM provider should emit an OpenTelemetry span recording: exact model identifier, input token count, output token count, latency, and provider HTTP status. This is the raw data for everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NestJS — LLM call wrapped in an OpenTelemetry span&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActiveSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm.completion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm.model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm.prompt_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;estimateTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm.completion_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm.total_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;total_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OK&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token count and exact model version per call flow into Prometheus via an OTEL collector. From there, you have the foundation for rate limit alerting and model version attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Token Budget Alerts
&lt;/h3&gt;

&lt;p&gt;Set a Prometheus alert at 70% of your per-key TPM limit. That gives you a two-minute window to queue requests, shed load, or page on-call before hitting 429s.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus alert rule&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LLMTokenBudgetHigh&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;sum(rate(llm_tokens_total[1m])) by (api_key, model)&lt;/span&gt;
    &lt;span class="s"&gt;&amp;gt; 0.70 * on(api_key) llm_tpm_limit&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2m&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;budget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;humanizePercentage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.model&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 70% threshold is not arbitrary. At 70%, you have runway to respond before degradation starts. At 90%, you are already mid-cascade.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Exact Model Version Pinning
&lt;/h3&gt;

&lt;p&gt;Pin to exact model identifiers, not aliases. &lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt; not &lt;code&gt;gpt-4o&lt;/code&gt;. When you upgrade, do it in a PR with a structured evaluation run in CI. Treat model upgrades like dependency upgrades: deliberate, reviewed, gated on test results.&lt;/p&gt;

&lt;p&gt;This is the lowest-effort, highest-leverage change most AI-native teams can make this week. Add one line to your model config, and the four-hour diagnosis of "why did output quality shift" becomes a one-minute log query.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Evaluation Pipeline in CI
&lt;/h3&gt;

&lt;p&gt;For any feature where output quality matters — which is most LLM features — run structured evaluations against a held-out test set as part of your deployment pipeline. Langfuse integrates directly into most AI backends and can gate deployments on score thresholds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Langfuse trace for evaluation tracking (TypeScript)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;langfuse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Langfuse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;langfuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document-summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o-2024-08-06&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;generation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your evaluation script queries Langfuse for the latest generation traces against your test set and fails the deployment if quality metrics drop more than 5% from baseline. Prompt regressions become CI failures, not support tickets.&lt;/p&gt;

&lt;p&gt;The evaluation test set does not need to be large. Twenty to thirty representative inputs with labeled expected outputs catches the vast majority of regressions in our experience. The effort to build it is one day. The effort to maintain it is twenty minutes per sprint, when new edge cases surface from production traffic and get added to the set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Cost
&lt;/h2&gt;

&lt;p&gt;Getting this stack in place takes one backend engineer two to three days, assuming they have shipped LLM features in production before. OpenTelemetry setup is the longest part. Prometheus alerts and Langfuse integration each take a few hours once the spans are flowing.&lt;/p&gt;

&lt;p&gt;The alternative is discovering these failure modes after they have been running silently for a week. That is a four-hour diagnosis, a customer apology, and a post-mortem. Every team we have worked with that shipped this observability stack before their first production incident has been glad they did. Every team that shipped it after their first production incident has said the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Standard APM covers transport-layer health. The three AI-specific failure modes — silent context truncation, rate limit cascades, and prompt regression — each require dedicated instrumentation.&lt;/li&gt;
&lt;li&gt;OpenTelemetry spans on every LLM call are the foundation. Token count, exact model version, and latency per call give you the raw data for everything else.&lt;/li&gt;
&lt;li&gt;Pin to exact model identifiers, not aliases. One config change. Eliminates a class of four-hour debugging sessions.&lt;/li&gt;
&lt;li&gt;Evaluation pipelines should be in CI before your first production incident, not after. The cost is two to three engineer-days. The cost of not having it is a week of silent degradation followed by a customer complaint.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>engineering</category>
      <category>business</category>
    </item>
    <item>
      <title>Engineering While the CTO Role Is Empty: How DACH Scale-Ups Are Staying on Roadmap</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 30 Apr 2026 05:47:53 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/engineering-while-the-cto-role-is-empty-how-dach-scale-ups-are-staying-on-roadmap-5bg0</link>
      <guid>https://dev.to/hassan_4e2f0901edda/engineering-while-the-cto-role-is-empty-how-dach-scale-ups-are-staying-on-roadmap-5bg0</guid>
      <description>&lt;p&gt;&lt;em&gt;The three to six months between "we need a CTO" and "we have one onboarded" is the most technically expensive period a scale-up can go through. Most companies find out the hard way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Across Berlin's tech scene in Q1 and Q2 2026, a specific hiring pattern has become visible: companies with 50 to 150 employees, real revenue, and active engineering teams posting for a CTPO or CTO while simultaneously shipping product, preparing a funding round, or scaling into new markets. These are not early-stage companies still figuring out product. They are operational businesses with engineering orgs that need technical leadership now.&lt;/p&gt;

&lt;p&gt;The search will take four to six months, minimum. The question is not whether the company can survive that window. It is what gets broken, deferred, or quietly abandoned inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Breaks During a CTO Search
&lt;/h2&gt;

&lt;p&gt;The most visible risk is velocity. When there is no technical anchor, architecture decisions get deferred. A senior engineer who would normally escalate a database schema question waits for someone with the authority to decide. The decision gets made in a sprint review instead of an architecture session. Six decisions like that, and the data model has cracks that will take a quarter to fix.&lt;/p&gt;

&lt;p&gt;But velocity is the measurable symptom. The harder problems are invisible until they compound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical debt accumulates faster under consensus.&lt;/strong&gt; With no CTO, architectural decisions often default to team consensus or the most vocal engineer in the room. Consensus-driven architecture is not inherently bad, but it tends toward local optimizations. Each team member solves their piece well. Nobody owns the system boundaries. Six months later, a new CTO arrives to find four services that should have been one, two different authentication patterns, and three ORMs in the same codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onboarding new engineers stalls.&lt;/strong&gt; A clear technical authority accelerates onboarding by providing definitive answers on stack decisions, code standards, and architectural direction. When that authority is absent, new engineers spend their first four weeks navigating informal consensus. Time-to-contribution stretches from three weeks to six. In a company that needs to scale its engineering capacity during a CTO search, this is a direct constraint on output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attrition risk increases among senior engineers.&lt;/strong&gt; This is the one that surprises founders most. Senior engineers do not leave because the CTO role is empty. They leave because decision-making becomes slow, frustrating, and politically complicated in the vacuum. The engineer who was happy building in a clear system starts spending meeting cycles relitigating settled questions. After two months of that, they start taking recruiter calls.&lt;/p&gt;

&lt;p&gt;According to the DORA 2024 Accelerate State of DevOps report, teams reporting low psychological safety and unclear technical ownership show measurably lower deployment frequency and higher change failure rates. The effect is not subtle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Search Process Does to the Engineering Org
&lt;/h2&gt;

&lt;p&gt;A CTO search is not a background process. It pulls on the same people who are supposed to be shipping product.&lt;/p&gt;

&lt;p&gt;Someone has to define the role. Someone has to run first-round interviews. Someone has to evaluate technical assessments. In companies without an existing CTO, that work lands on the founder, a VP of Product, or the most senior engineer, none of whom were hired to run a C-suite search while also managing a sprint cycle.&lt;/p&gt;

&lt;p&gt;The most effective DACH companies navigating this transition have separated two things that are often conflated: the CTO search process and the technical leadership function.&lt;/p&gt;

&lt;p&gt;The search is a hiring project. The leadership function is an operational requirement that cannot pause for three to six months. Treating them as the same problem is why so many companies arrive at the end of a CTO search with a strong new hire and a codebase that needs six weeks of architectural triage before they can get to actual roadmap work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Interim Technical Leadership Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;The companies that come through a CTO gap cleanest tend to do three things.&lt;/p&gt;

&lt;p&gt;First, they document the implicit architecture. Before the search begins, the founding team or most senior engineers produce an honest snapshot of the current system: service boundaries, data models, known debt, and open architectural questions. This is not a polished document for investors. It is a working reference that stabilizes team decisions during the vacuum and gives the incoming CTO a real starting point rather than months of archaeology.&lt;/p&gt;

&lt;p&gt;Second, they assign decision authority explicitly. Not with a title, but with a scope. "On API contract changes, this person has final say until we hire a CTO" is more effective than leaving decisions to consensus. The assignment should cover: system architecture, infrastructure spending, hiring decisions, and external technical commitments. Each category needs one owner.&lt;/p&gt;

&lt;p&gt;Third, they extend the engineering capacity before the search ends. The instinct is to wait for the CTO to arrive and then hire. But the incoming CTO needs a functioning team to lead. Embedding one or two engineers with context on the specific stack before the hire lands means the new CTO spends their first weeks orienting on strategy rather than emergency staffing.&lt;/p&gt;

&lt;p&gt;We have seen this pattern play out across DACH teams across multiple industries. The companies that handled the gap well had done the groundwork on people and process before the new technical leader arrived. The ones that struggled had paused engineering capacity decisions while waiting for the CTO to own them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compounding Argument for Not Waiting
&lt;/h2&gt;

&lt;p&gt;There is a financial case here that does not require any projections.&lt;/p&gt;

&lt;p&gt;A Series A engineering team in Berlin has an average loaded cost of EUR 85 to 120K per engineer per year. Each month of reduced velocity during a CTO gap costs somewhere between two and four weeks of that team's effective output, depending on how well the gap is managed. Across a team of eight engineers and a five-month search, that is a conservative EUR 80 to 160K in reduced throughput, before accounting for any attrition.&lt;/p&gt;

&lt;p&gt;The gap itself is not avoidable. The compounding is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question Worth Asking Before You Start the Search
&lt;/h2&gt;

&lt;p&gt;Most technical founders spend their energy on finding the right CTO candidate. Fewer spend equivalent energy on answering a more immediate question: what has to stay stable in our engineering org for the next six months regardless of who is leading it?&lt;/p&gt;

&lt;p&gt;The answer to that question determines whether the incoming CTO inherits a team in good condition or a team that has been slowly falling apart since the day the search started.&lt;/p&gt;

&lt;p&gt;What architectural decisions in your current codebase would cause the most damage if made by committee for the next four months?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>startup</category>
      <category>engineering</category>
      <category>business</category>
    </item>
    <item>
      <title>AI Automated 50% of Your Operations. Your Backend Team Is Busier Than Ever.</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 23 Apr 2026 05:26:32 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/ai-automated-50-of-your-operations-your-backend-team-is-busier-than-ever-a31</link>
      <guid>https://dev.to/hassan_4e2f0901edda/ai-automated-50-of-your-operations-your-backend-team-is-busier-than-ever-a31</guid>
      <description>&lt;p&gt;&lt;em&gt;The AI deployment paradox: every workflow you automate creates three new engineering surfaces.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The companies building AI into production — nursing documentation, accounts payable, energy ops, patient intake — are discovering something uncomfortable. The AI is working. Response rates are up, manual tasks are shrinking, the demo looks great. And the backend engineering queue is longer than it was before the model shipped.&lt;/p&gt;

&lt;p&gt;This is not a bug. It's the physics of AI at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happens After AI Ships
&lt;/h2&gt;

&lt;p&gt;When you automate a manual workflow with AI, you don't reduce complexity. You transform it. The human who used to do the task understood context implicitly, recovered from edge cases, and escalated when something felt wrong. Your AI doesn't. It generates output, and the engineering team owns everything that happens next.&lt;/p&gt;

&lt;p&gt;Three surfaces appear immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data pipeline.&lt;/strong&gt; Your model is only as good as what feeds it. Clinical notes need cleaning before transcription. Invoice data needs normalization before extraction. Meter readings need validation before pricing decisions. The data engineers who were on the roadmap but not urgent? Now they're urgent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The monitoring layer.&lt;/strong&gt; Humans notice drift. Models don't. A nurse documentation system that starts categorizing wound care as medication administration will keep going until someone builds the detection logic to catch it. For every inference endpoint you put in production, you need latency monitoring, accuracy regression tracking, and a human escalation path. None of that ships with the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration surface.&lt;/strong&gt; Your AI touches existing systems. EHR APIs, ERP connectors, billing modules, IoT device streams. Each integration is a live dependency with its own versioning, rate limits, and failure modes. As you expand across facilities, clients, or markets, every new customer brings a new integration variation.&lt;/p&gt;

&lt;p&gt;The companies in DACH seeing this most acutely are the ones who shipped AI fastest: healthcare documentation platforms integrating with 50+ EHR systems, energy management platforms wiring IoT meter networks into dynamic pricing, HR API companies adding AI layers on top of 200+ existing integrations. Their engineering teams didn't shrink. They grew, and still couldn't keep pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 12-18 Months Post-Launch Looks Like
&lt;/h2&gt;

&lt;p&gt;We work with companies that have lean engineering teams, usually 10-20 engineers, building technically complex AI products. The same situation surfaces consistently around 12-18 months post-Series A or B:&lt;/p&gt;

&lt;p&gt;The product is working. Customer count is growing. And the engineering team, which was sized for product build-out, is now also responsible for production reliability, data quality, and integration maintenance. The CTO is hiring for three roles simultaneously. Berlin's senior backend pool takes 4-6 months per hire. The roadmap slips because the people who could build the new features are keeping the existing system alive.&lt;/p&gt;

&lt;p&gt;At one company building an AI product in a regulated sector, we started with a single backend engineer embedded in their team. Within a few months, as the data pipeline complexity grew, two more engineers joined to own the integration layer and monitoring infrastructure. The original engineer never left the team. That's the trajectory.&lt;/p&gt;

&lt;p&gt;The work is additive, not a temporary spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Engineering Work Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;For teams in this position, the backlog typically breaks into three tracks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track 1: Data reliability.&lt;/strong&gt; Write the validation jobs, anomaly detectors, and reconciliation scripts that catch model input failures before they corrupt output. This is Python and SQL work. It's not glamorous, and it compounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track 2: Integration maintenance.&lt;/strong&gt; The HIS in hospital A updated their API. The ERP at customer B sends timestamps in a different timezone. The IoT hub at site C drops packets under load. Each customer is an integration, and each integration has an owner. For companies expanding across Germany and into Austria or Switzerland, this surface grows with every new contract signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track 3: AI observability.&lt;/strong&gt; Latency tracking per model version, accuracy regression tests, alerting on output distribution shifts. None of this is in the LLM provider's dashboard. Your team builds it. TypeScript or Python, depending on stack. Deploys to the same Kubernetes cluster as the rest of the application. Requires engineers who understand both the ML context and production systems.&lt;/p&gt;

&lt;p&gt;None of these tracks are one-time projects. They're ongoing engineering capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hiring Math Doesn't Work in Berlin
&lt;/h2&gt;

&lt;p&gt;Berlin has over 300 funded tech startups in active growth mode, all hiring from the same senior backend pool. The DACH software engineering salary range for senior backend roles sits between EUR 80k-120k (Source: Glassdoor DACH, 2025). Time-to-hire for a verified senior engineer runs 4-6 months including sourcing, interviews, and notice periods.&lt;/p&gt;

&lt;p&gt;If you need two backend engineers now, you're making a bet that your production system holds for six months while you hire. In a regulated industry, with contractual SLA obligations and integration dependencies, that bet is expensive.&lt;/p&gt;

&lt;p&gt;The alternative most teams reach for is contractors. That solves the speed problem but creates a different one: contractors don't stay on your codebase. Context doesn't accumulate. The integration engineer who joined to wire in the third EHR system is gone before the fourth one arrives, and the next contractor starts from scratch.&lt;/p&gt;

&lt;p&gt;What we've observed across engagements: engineers who stay on a codebase long enough to own a domain ship faster and break fewer things than engineers who rotate through. The institutional knowledge compounds, and the codebase reflects it. Contractors break that cycle by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI automation expands backend engineering scope. It doesn't reduce it. Plan for data pipelines, monitoring, and integrations as ongoing headcount, not one-time projects.&lt;/li&gt;
&lt;li&gt;The 4-6 month Berlin hiring timeline is a product risk, not just a cost. If your AI is in production with SLA commitments, the gap is measured in reliability incidents.&lt;/li&gt;
&lt;li&gt;Contractors solve the speed problem but break the context accumulation that makes the second and third integrations faster than the first.&lt;/li&gt;
&lt;li&gt;The engineering team you need 12 months post-launch is 2-3 people larger than the one you budgeted for at Series A. The companies that plan for this hire ahead. The ones that don't, hire in crisis mode.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>typescript</category>
      <category>hiring</category>
    </item>
    <item>
      <title>The AI Capacity Trap: Why Lean Teams Need More Engineers After They Automate</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 16 Apr 2026 05:26:45 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/the-ai-capacity-trap-why-lean-teams-need-more-engineers-after-they-automate-3ia1</link>
      <guid>https://dev.to/hassan_4e2f0901edda/the-ai-capacity-trap-why-lean-teams-need-more-engineers-after-they-automate-3ia1</guid>
      <description>&lt;p&gt;&lt;em&gt;The companies that used AI to stay lean are now discovering they need backend engineers to keep the AI running.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The pitch was compelling: instead of hiring 15 operations people, build AI workflows that handle 70% of tickets automatically. Keep the team small. Move fast. Raise on the story.&lt;/p&gt;

&lt;p&gt;It worked. A wave of DACH scale-ups raised Series A and B rounds in 2025-2026 with exactly this model. Some had 50 employees doing what two years ago required 100. Some built care coordination AI agents that reduced manual case routing by half. Some shipped AI-assisted customer resolution that meant one support engineer could handle four times the volume.&lt;/p&gt;

&lt;p&gt;Then the AI layer needed to scale. And the team that built it on sprint weekends while maintaining the core product hit a wall they did not see coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI Infrastructure Is Not a Side Project
&lt;/h2&gt;

&lt;p&gt;There is a category error that compounds here. When a team ships an AI feature quickly, they demonstrate that it can be built. What they do not demonstrate is that it can be maintained, scaled, and made reliable at production volume.&lt;/p&gt;

&lt;p&gt;The difference matters in ways that are invisible until you hit them.&lt;/p&gt;

&lt;p&gt;A care coordination AI agent that routes 50 cases a day needs different infrastructure than one routing 5,000. The prompt engineering that worked in development drifts when the model provider pushes a new version. The evaluation pipeline that caught quality regressions in staging needs continuous care as edge cases accumulate in production. The latency that was acceptable at low volume becomes a user experience problem at high volume.&lt;/p&gt;

&lt;p&gt;None of this is research. It is plumbing. Backend engineers who understand queue management, observability, retry logic, and model versioning in production systems.&lt;/p&gt;

&lt;p&gt;The problem is that the team who built the AI feature was the same team maintaining the core product. They are good engineers. But they are running at capacity on two incompatible modes simultaneously: the stability instincts of core product ownership and the iteration instincts of AI product development. The DORA State of DevOps research quantifies this directly: teams that split attention across two distinct product tracks have roughly half the deployment frequency of teams with focused ownership.&lt;/p&gt;

&lt;p&gt;At 50-150 employees, you cannot absorb that tax for long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern Across DACH Scale-Ups in 2026
&lt;/h2&gt;

&lt;p&gt;This is not a prediction. It is already visible across the current cohort of DACH mid-market companies.&lt;/p&gt;

&lt;p&gt;A Berlin healthtech company raised €37M in January 2026 with an AI agent as the core differentiation. Three months later, their job board lists backend engineering roles specifically for the AI workflow layer — separate from the core platform roles they have always hired for. The AI agent is working. Now it needs its own engineering team.&lt;/p&gt;

&lt;p&gt;A Berlin HR-API company closed a $25M Series A in February 2026 and immediately opened "Product Engineer - AI Apply" roles alongside their standard full-stack positions. Their core integration product runs on a proven team. The AI product line is a second surface that needs dedicated ownership.&lt;/p&gt;

&lt;p&gt;A Berlin design SaaS company with 59 engineers and $27M ARR is hiring for AI backend capacity while simultaneously hiring for core platform reliability. Two different engineering profiles, two different skill sets, same team posting.&lt;/p&gt;

&lt;p&gt;The pattern: AI product launches with the existing team stretched across it. Traction follows. The AI layer grows. The existing team cannot own both the core product and the AI infrastructure at the required depth. Hiring starts — but now for a different profile than before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI Backend Engineering Profile Actually Requires
&lt;/h2&gt;

&lt;p&gt;The engineers who maintain production AI systems are not the same profile as the engineers who built your MVP.&lt;/p&gt;

&lt;p&gt;A backend engineer on a traditional product track optimizes for stability: migration safety, contract versioning, rollback plans. A backend engineer on an AI infrastructure track optimizes for iteration speed and observability: A/B evaluation pipelines, prompt version management, model fallback logic, latency profiling across inference providers.&lt;/p&gt;

&lt;p&gt;Concretely, the AI backend role requires:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt version control in production.&lt;/strong&gt; Not just &lt;code&gt;.env&lt;/code&gt; file management, but tracked, reviewed, and staged prompt changes with rollback capability. A prompt change is a code change. It needs a deployment workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation pipelines, not unit tests.&lt;/strong&gt; Unit tests verify that functions return expected values. Evaluation pipelines verify that AI outputs meet quality thresholds across representative samples. Building and maintaining these pipelines is engineering work, not prompt engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model provider abstraction.&lt;/strong&gt; Inference providers release API changes, deprecate models, and adjust rate limits. AI backend engineers build abstraction layers that decouple application logic from provider contracts. This is the same discipline as building an integration API layer — it just applies to model calls instead of third-party REST APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability at the output layer.&lt;/strong&gt; Standard APM tools measure latency and error rates. AI backend observability also measures output quality drift, prompt-to-response fidelity, and hallucination rates in production. Instrumenting this requires engineers who understand both the observability stack and the model behavior.&lt;/p&gt;

&lt;p&gt;This is a hireable profile. It is not rare. But it is a distinct hiring brief from "senior backend engineer," and the sourcing process is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We've Seen Work
&lt;/h2&gt;

&lt;p&gt;At one client, the AI product workstream was assigned to the same backend engineers maintaining the core platform. Within eight weeks, two things had degraded: the AI features were shipping with hardcoded model configurations instead of versioned prompt management, and a core platform refactor was deferred twice because the engineers were context-switching.&lt;/p&gt;

&lt;p&gt;The fix was structural, not motivational. A dedicated squad took ownership of the AI infrastructure track. They ran separate standups, used different tooling, and operated on an evaluation-driven definition of done instead of a test-coverage definition. Within two months, both tracks had clearer velocity and the core platform team stopped accumulating deferred technical debt.&lt;/p&gt;

&lt;p&gt;The staffing model that made this work was not hiring three new senior engineers in Berlin over six months. It was embedding two engineers hired specifically for the client's Node.js and Python stack, with AI infrastructure experience, in under three weeks. They joined the client's Slack on day one, attended the engineering standup on day two, and had a pull request reviewed by the end of week one.&lt;/p&gt;

&lt;p&gt;The ramp worked because the engineering brief was specific before the hire happened. Not "backend engineer with AI experience." The client's deployment model, inference provider, evaluation framework, and prompt management approach were documented and used as the hiring filter. Engineers who matched that brief needed no ramp time to understand the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI-lean teams that achieved scale through automation now face a different engineering problem: maintaining and scaling the AI layer itself requires dedicated backend capacity.&lt;/li&gt;
&lt;li&gt;The engineers who built the AI feature on sprint weekends are the same engineers maintaining the core product. This split attention halves deployment frequency on both tracks, per DORA research.&lt;/li&gt;
&lt;li&gt;AI backend engineering is a distinct profile: prompt version management, evaluation pipelines, model provider abstraction, and AI-specific observability. It is hireable but not the same brief as "senior full-stack."&lt;/li&gt;
&lt;li&gt;The structural fix is a dedicated squad with separate ownership, not sprint allocation. Team topology determines track velocity more reliably than headcount.&lt;/li&gt;
&lt;li&gt;Embedded engineers hired to a specific AI backend brief can integrate in two to three weeks. The ramp speed depends entirely on how specific the brief was before the hire.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>typescript</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Your AI Feature Track Is Stalling Your Core Product</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 09 Apr 2026 05:27:54 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/your-ai-feature-track-is-stalling-your-core-product-4oaf</link>
      <guid>https://dev.to/hassan_4e2f0901edda/your-ai-feature-track-is-stalling-your-core-product-4oaf</guid>
      <description>&lt;p&gt;&lt;em&gt;Why launching an AI workstream with your existing team creates two failure modes at once — and what to do instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You closed your Series A or B six months ago. The roadmap committed to investors includes an AI-powered product track: an AI agent, an ML recommendation layer, an LLM-backed workflow. Your engineering team is good. You shipped the core product with them. Now they're stretched across two futures simultaneously, and both are moving slower than they should.&lt;/p&gt;

&lt;p&gt;This is the most common engineering bottleneck we see at DACH scale-ups right now. It has a name, a cause, and a structural fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Same Team Cannot Own Both Tracks
&lt;/h2&gt;

&lt;p&gt;The core product and the AI feature track have fundamentally different engineering rhythms.&lt;/p&gt;

&lt;p&gt;Core product work runs on predictability. You have a schema, a deployment cadence, a test suite, SLAs that customers depend on. Engineers managing this track optimize for stability. Breaking changes are expensive. The cost of a wrong migration at 3am is high. Teams working here develop instincts around caution.&lt;/p&gt;

&lt;p&gt;AI feature work runs on experimentation. Prompt engineering iterations happen daily. Model providers release new API versions every six weeks. Evaluation pipelines replace unit tests. A feature that "works" at demo quality needs three more weeks of evals before it works reliably in production. Engineers on this track need to move fast, break things in staging, and rebuild. The instincts are opposite.&lt;/p&gt;

&lt;p&gt;When you assign the same engineers to both, neither track gets the right instincts. Core product engineers ship the AI feature defensively, adding complexity and slowing iteration. The AI track accrues caution debt. Meanwhile, the core product slips because the senior engineers are context-switching across two incompatible modes.&lt;/p&gt;

&lt;p&gt;The DORA State of DevOps research consistently shows that context-switching is not a minor inefficiency. Teams that split attention across two distinct products have deployment frequency that is roughly half that of teams with focused ownership. At 50-200 employees, you cannot absorb that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We've Seen
&lt;/h2&gt;

&lt;p&gt;At one client, the AI agent track was staffed by pulling three backend engineers off core product delivery. Within six weeks, two things happened: the AI features shipped with hard-coded model configs instead of proper prompt versioning (because the engineers' mental model was "function, not experiment"), and a core product module that needed a refactor got deferred twice. By month three, the CTO was managing two teams that each felt under-resourced despite having the same total headcount.&lt;/p&gt;

&lt;p&gt;The fix was splitting ownership at the team level, not the sprint level. A separate squad took over the AI workstream, with different tooling, different evaluation criteria, and different standups. The core product team stopped context-switching. Within eight weeks, both tracks had clearer velocity.&lt;/p&gt;

&lt;p&gt;This pattern holds across the DACH scale-ups we work with. Berlin HealthTech companies launching care coordination AI agents. HR-API companies building AI-powered application flows. Design SaaS companies adding generative image features. The story is the same: net-new AI product, existing team stretched, two tracks bleeding into each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Structural Fix: Separate the Squad, Not the Sprint
&lt;/h2&gt;

&lt;p&gt;The principle is team topology, not sprint planning. Two parallel tracks need two teams with coherent ownership.&lt;/p&gt;

&lt;p&gt;The AI workstream squad typically needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend engineer comfortable with Python, async processing, and working directly with LLM APIs (OpenAI, Anthropic, Gemini). This person writes the prompt management layer, the evaluation harness, the retry logic, and the streaming response handlers.&lt;/li&gt;
&lt;li&gt;A data or ML engineer who can build evaluation pipelines, manage dataset versioning (think DVC or Weights and Biases), and interpret evals beyond vibes. At mid-market scale, this person does not need to train models — they need to work with pre-trained models and measure output quality reliably.&lt;/li&gt;
&lt;li&gt;Optionally, a second backend engineer if the AI product has significant integration surface (webhooks, API consumers, OAuth flows connecting to third-party SaaS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core product team stays intact. They set the contracts the AI squad integrates against: API schemas, event topics, database access patterns. The AI squad treats the core product as a dependency, not a shared codebase.&lt;/p&gt;

&lt;p&gt;This separation has a counterintuitive benefit: it forces interface clarity. When the AI squad cannot just reach into shared code, both teams end up with cleaner boundaries. The core API gets documented. Events get proper schemas. The architectural debt that "we'll clean up later" gets flushed out by necessity.&lt;/p&gt;

&lt;p&gt;On tooling: the AI squad should own its own deployment path. A separate service, deployed independently, with its own CI pipeline and its own evaluation gate before promotion to production. Use LangSmith, Langfuse, or a homegrown eval harness — the specific choice matters less than having one. If your AI feature has no evaluation pipeline, it is not production-ready regardless of how good it looked in the demo.&lt;/p&gt;

&lt;p&gt;For infrastructure, Kubernetes namespaces work well for isolation without separate clusters. Your platform team (or whoever owns your Terraform and Helm charts) adds the AI service namespace to existing infrastructure — typically a half-day of work, not a new greenfield setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Splitting AI and core product engineering at the sprint level does not solve the underlying context-switch problem. The fix is team ownership, not task allocation.&lt;/li&gt;
&lt;li&gt;An AI workstream squad at this stage needs a backend engineer with LLM API experience and a data engineer who can build eval pipelines — not necessarily ML specialists.&lt;/li&gt;
&lt;li&gt;Interface contracts forced by team separation improve your core architecture as a side effect. The pressure to define clean APIs and event schemas has long-term value beyond the AI track.&lt;/li&gt;
&lt;li&gt;The cost of building this second squad in-house — recruiting, interviewing, onboarding — is 4-6 months on Berlin timelines. Embedding a dedicated squad hired for your stack cuts that to 3-4 weeks.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>hiring</category>
      <category>startup</category>
    </item>
    <item>
      <title>Launching a Second Product? Your Engineering Team Can't Build Both.</title>
      <dc:creator>Hassan</dc:creator>
      <pubDate>Thu, 02 Apr 2026 05:20:10 +0000</pubDate>
      <link>https://dev.to/hassan_4e2f0901edda/launching-a-second-product-your-engineering-team-cant-build-both-5b4e</link>
      <guid>https://dev.to/hassan_4e2f0901edda/launching-a-second-product-your-engineering-team-cant-build-both-5b4e</guid>
      <description>&lt;p&gt;&lt;em&gt;Why shared engineering resources guarantee that your new product track ships late — and what a purpose-built team changes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You've validated the first product. You have paying customers, a functioning team, and a roadmap your engineers know by heart. Now there's a second product. A new SaaS track. An AI suite. A platform for a vertical you weren't in before. Leadership is aligned, the market timing is right, and you need to ship.&lt;/p&gt;

&lt;p&gt;The question is: who builds it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Borrowed Engineer Problem
&lt;/h2&gt;

&lt;p&gt;The first answer is always the same. You pull one or two engineers from the core team. Temporarily. Just to get the foundation down, scope the architecture, unblock the first sprint. They know the codebase, they know how you work, and they're available right now.&lt;/p&gt;

&lt;p&gt;Temporary rarely ends. Three months later, those engineers are context-switching between two codebases, two roadmaps, and two sets of stakeholder expectations. The core product slows down because they're unavailable for the work only they understand. The new product slows down because they're still on-call for the old one. You've created two half-staffed teams where you needed one focused team.&lt;/p&gt;

&lt;p&gt;This isn't a management failure. It's a structural one. Borrowed engineers carry the cognitive cost of the thing they came from. They can't fully own the new product because they haven't left the old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Headcount Takes Longer Than Your Window
&lt;/h2&gt;

&lt;p&gt;The alternative is to hire. Post the roles, run the pipeline, make the offers. For a three-person engineering team covering frontend, backend, and infrastructure, you're looking at nine to eighteen months of elapsed hiring time if everything goes well. One slow candidate, one declined offer, one extended notice period, and you're past the window you thought you had.&lt;/p&gt;

&lt;p&gt;The German market compounds this. Senior engineers in Berlin and Munich face outreach from three or four employers simultaneously. A 2024 analysis of DACH tech hiring found median time-to-hire for senior software roles at 4.2 months, not counting ramp time to first meaningful contribution. By the time your new hires are shipping independently, six months have passed and the competitive dynamics have shifted.&lt;/p&gt;

&lt;p&gt;The second product doesn't have six months. It has the urgency that justified building it in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Independence Is What Makes Small Teams Fast
&lt;/h2&gt;

&lt;p&gt;The reason small teams can outship large ones is focus. A team of four engineers working on one product, one codebase, one set of user problems can move at a pace that a fifty-person team never can. They're not waiting for reviews from people who don't know the context. They're not blocked by decisions made for the other product. They own the outcome completely.&lt;/p&gt;

&lt;p&gt;That independence disappears the moment the team is shared. A team that splits attention between two products is optimized for neither. The review cycles lengthen. The context-switching tax compounds. The product that feels secondary to the team becomes secondary in practice, regardless of what the roadmap says.&lt;/p&gt;

&lt;p&gt;The second product needs its own team from day one. Not eventually. From the first sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Purpose-Built Team Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;We've built this exact structure for a client. The engagement started with one engineer, specifically hired for that client's stack and that product's requirements. Not pulled from a bench, not rotated from another client. Hired to be part of their team. That engineer embedded into their engineering org, learned the codebase, and started shipping in the first two weeks.&lt;/p&gt;

&lt;p&gt;As the product scope expanded, the team expanded with it. Each engineer brought in was hired for the specific gap: a frontend specialist when the UI complexity increased, a data engineer when the pipeline work became the bottleneck. The team that started small is now a complete cross-functional team, fully integrated into the client's engineering org. The second product track they were built for is now the primary delivery engine.&lt;/p&gt;

&lt;p&gt;This is the build-to-staff model. The developers are hired for you, not assigned to you. They join your team, use your tools, follow your process, and report into your engineering organization. The difference from contracting is ownership. The difference from hiring is speed: two to four weeks from scoping to first commit, not six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timing Question
&lt;/h2&gt;

&lt;p&gt;If you're planning a second product track and asking where the engineering capacity comes from, the answer matters more than most structural decisions you'll make this quarter. Borrowed engineers slow both products. Open headcount misses the window. Purpose-built teams can start in weeks.&lt;/p&gt;

&lt;p&gt;If your second product has a real timeline and you want to talk through the engineering structure, we're straightforward to reach. A thirty-minute conversation is enough to scope whether this model fits your situation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://sifrventures.com" rel="noopener noreferrer"&gt;SifrVentures&lt;/a&gt; builds dedicated engineering teams for tech companies. Based in Berlin. &lt;a href="https://sifrventures.com/how-we-work" rel="noopener noreferrer"&gt;Learn how we work&lt;/a&gt; | &lt;a href="https://sifrventures.com/blog" rel="noopener noreferrer"&gt;Read more on our blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hiring</category>
      <category>engineering</category>
      <category>business</category>
    </item>
  </channel>
</rss>
