<?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: Damir Karimov</title>
    <description>The latest articles on DEV Community by Damir Karimov (@damir-karimov).</description>
    <link>https://dev.to/damir-karimov</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%2F2575304%2Fe501ae75-9f5b-4d85-9dd7-670b54fe522c.png</url>
      <title>DEV Community: Damir Karimov</title>
      <link>https://dev.to/damir-karimov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/damir-karimov"/>
    <language>en</language>
    <item>
      <title>Beyond Vector Search: Building Better RAG Retrieval with Hybrid Search and Reranking</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Wed, 12 Aug 2026 11:34:40 +0000</pubDate>
      <link>https://dev.to/damir-karimov/beyond-vector-search-building-better-rag-retrieval-with-hybrid-search-and-reranking-p0e</link>
      <guid>https://dev.to/damir-karimov/beyond-vector-search-building-better-rag-retrieval-with-hybrid-search-and-reranking-p0e</guid>
      <description>&lt;p&gt;The first two parts of this series covered why production RAG systems fail and how the quality of the data foundation directly affects everything that comes after it. We looked at document ingestion, parsing, chunking, and metadata design—the layers responsible for turning raw information into something a retrieval system can actually work with.&lt;/p&gt;

&lt;p&gt;But even perfectly processed documents are useless if retrieval cannot find the right information.&lt;/p&gt;

&lt;p&gt;In this third part, we'll move into the retrieval layer itself. We'll look at why vector search alone is often insufficient, how semantic and lexical search complement each other, and how reranking can turn a large set of possible matches into a small set of highly relevant documents. We'll also cover query optimization, metadata filtering, and context compression—key techniques for building retrieval pipelines that perform reliably on real-world queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production RAG Architecture Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Why Most RAG Systems Fail in Production: The Hidden Architecture Problems Behind AI Search&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Building a Production RAG Pipeline: Document Processing, Chunking, and Metadata Design&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Beyond Vector Search: Building Better RAG Retrieval with Hybrid Search and Reranking&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling RAG Systems: Production Architecture, Performance, and Cost Optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluating Production RAG Systems: Metrics, Monitoring, and Common Failure Patterns&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Chapter 7 — Embeddings
&lt;/h2&gt;

&lt;p&gt;Embeddings are not magic.&lt;br&gt;&lt;br&gt;
They are coordinates.&lt;/p&gt;

&lt;p&gt;That is the whole trick.&lt;br&gt;&lt;br&gt;
A piece of text goes in, a vector comes out, and now similar meanings sit close to each other in space.&lt;/p&gt;

&lt;p&gt;If chunking decides what the system sees, embeddings decide how it remembers it.&lt;/p&gt;

&lt;p&gt;That sounds abstract until you try to build retrieval on top of it. Then it becomes the center of the whole system.&lt;/p&gt;


&lt;h2&gt;
  
  
  What an embedding really is
&lt;/h2&gt;

&lt;p&gt;Imagine a map.&lt;/p&gt;

&lt;p&gt;On that map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“dog” sits near “wolf”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“invoice” sits near “payment”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“upgrade” sits near “billing policy”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“password reset” sits somewhere else.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is not understanding meaning the way a human does. It is learning a geometry where related things end up near each other. That geometry is what retrieval uses later.&lt;/p&gt;

&lt;p&gt;And that is why embeddings matter so much.&lt;br&gt;&lt;br&gt;
If the geometry is good, retrieval feels smart.&lt;br&gt;&lt;br&gt;
If the geometry is bad, everything downstream starts guessing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why this is not enough
&lt;/h2&gt;

&lt;p&gt;This is where people usually make the first mistake.&lt;/p&gt;

&lt;p&gt;They think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If I use a good embedding model, retrieval will work.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It won’t.&lt;/p&gt;

&lt;p&gt;A good embedding model can only work with the text you give it. If the chunk is messy, too broad, too short, or stuffed with unrelated ideas, the vector will still be messy. Just in a more expensive way.&lt;/p&gt;

&lt;p&gt;A bad chunk becomes a bad vector.&lt;br&gt;&lt;br&gt;
A bad vector becomes a bad candidate.&lt;br&gt;&lt;br&gt;
A bad candidate becomes a confident wrong answer.&lt;/p&gt;


&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Take these chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Active invoices must be closed before upgrading.
2. Customers can upgrade from Professional to Enterprise.
3. How to reset your password.
4. Downgrading is allowed only if no active trials exist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A decent embedding model should understand that 1 and 2 belong near upgrade-related questions, while 3 is clearly off in another part of the world.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but in real systems it gets messy fast.&lt;/p&gt;

&lt;p&gt;Because now you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;legal docs,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;support docs,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;product policies,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;release notes,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tables,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;code snippets,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and old versions of the same document all mixed together.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point embeddings are not a detail anymore.&lt;br&gt;&lt;br&gt;
They are the shape of the search space.&lt;/p&gt;


&lt;h2&gt;
  
  
  What makes a good embedding model
&lt;/h2&gt;

&lt;p&gt;A good model for production should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;understand your language,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;behave well on short queries,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;not collapse technical terms into generic similarity,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and work on your actual domain, not just “general text.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model that is decent for blog posts may be weak for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;policy documents,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;multilingual corpora,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;technical manuals,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;product docs with version numbers,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or support data full of exact identifiers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real question is not “which embedding model is popular?”&lt;br&gt;&lt;br&gt;
The real question is “which model gives me the right geometry for my corpus?”&lt;/p&gt;
&lt;h2&gt;
  
  
  Example code
&lt;/h2&gt;

&lt;p&gt;python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Active invoices must be closed before upgrading.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customers can upgrade from Professional to Enterprise.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How to reset your password.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Downgrading is allowed only if no active trials exist.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Can Enterprise customers upgrade directly from Professional while keeping active invoices?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;query_vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="n"&gt;ranked&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="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&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="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the smallest possible version of the idea.&lt;/p&gt;

&lt;p&gt;Query becomes a vector.&lt;br&gt;&lt;br&gt;
Chunk becomes a vector.&lt;br&gt;&lt;br&gt;
Similarity becomes a number.&lt;/p&gt;

&lt;p&gt;The number is not truth.&lt;br&gt;&lt;br&gt;
It is only a signal.&lt;br&gt;&lt;br&gt;
But in a good system, that signal is useful enough to move the right chunk to the top.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why chunk shape changes embedding quality
&lt;/h2&gt;

&lt;p&gt;A short query and a long chunk do not behave the same way.&lt;/p&gt;

&lt;p&gt;A query like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Enterprise upgrade active invoices”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is compact and vague.&lt;/p&gt;

&lt;p&gt;A chunk like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Customers can upgrade from Professional to Enterprise. Active invoices must be closed before upgrading. Contact billing if invoices remain open.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;contains multiple ideas.&lt;/p&gt;

&lt;p&gt;The embedding becomes a compressed summary of all of that. If the chunk contains too many unrelated ideas, the vector turns into an average of everything, which is another way of saying it gets blurrier.&lt;/p&gt;

&lt;p&gt;That is why embeddings and chunking are inseparable.&lt;br&gt;&lt;br&gt;
You cannot fix one without thinking about the other.&lt;/p&gt;


&lt;h2&gt;
  
  
  The domain problem
&lt;/h2&gt;

&lt;p&gt;General embeddings are often good enough to impress in demos. Production is where they start revealing their limits.&lt;/p&gt;

&lt;p&gt;A support system might need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;plan names,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;billing states,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;status codes,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;product tiers,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;policy phrases,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;internal jargon.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generic model may know the words, but not the importance of those words in your system.&lt;/p&gt;

&lt;p&gt;That is why evaluation on real queries matters.&lt;br&gt;&lt;br&gt;
Not benchmark queries.&lt;br&gt;&lt;br&gt;
Your queries.&lt;/p&gt;


&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;Embeddings are not magic meaning detectors.&lt;br&gt;&lt;br&gt;
They are a way to build a space where retrieval can do its job.&lt;/p&gt;

&lt;p&gt;If the space is designed well, the system can find the right things.&lt;br&gt;&lt;br&gt;
If the space is noisy, the retriever will still return something plausible, and plausible is often the most dangerous kind of wrong.&lt;/p&gt;

&lt;p&gt;That is the entire game.&lt;/p&gt;


&lt;h2&gt;
  
  
  Chapter 8 — Hybrid Search
&lt;/h2&gt;

&lt;p&gt;Vector search is good at meaning.&lt;br&gt;&lt;br&gt;
Keyword search is good at precision.&lt;/p&gt;

&lt;p&gt;Production needs both.&lt;/p&gt;

&lt;p&gt;That is the whole chapter.&lt;/p&gt;

&lt;p&gt;If you only use embeddings, the system understands the idea of the query but can miss the exact phrase that actually matters. If you only use keywords, the system catches exact matches but misses the intent behind the question. Hybrid search exists because real users do both things at once.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why vector search is not enough
&lt;/h2&gt;

&lt;p&gt;Vector search is great when a person asks naturally.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do I upgrade my plan?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That kind of question has room for interpretation. The model can infer the intent even if the wording is loose.&lt;/p&gt;

&lt;p&gt;But then the user asks something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now exact words matter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Professional.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enterprise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;active invoices.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A vector model may understand the general billing theme, but it can still miss the exact policy sentence because the answer depends on precise terms, not just conceptual similarity.&lt;/p&gt;

&lt;p&gt;That is where pure semantic retrieval starts lying politely.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why keyword search is not enough
&lt;/h2&gt;

&lt;p&gt;Now flip the problem.&lt;/p&gt;

&lt;p&gt;A keyword system is brilliant when the query contains exact tokens.&lt;/p&gt;

&lt;p&gt;If the question includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;product names,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version numbers,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;error codes,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;clause IDs,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;policy names,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact phrases,&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then BM25 or another lexical retriever often finds the right passage instantly.&lt;/p&gt;

&lt;p&gt;But if the user says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can a customer move to the top tier if they still owe money?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;a pure keyword search may fail because the document says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Active invoices must be closed before upgrading.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the same idea, but not the same wording.&lt;/p&gt;

&lt;p&gt;So keyword search is precise, but not smart.&lt;br&gt;&lt;br&gt;
Vector search is smart, but not precise enough.&lt;/p&gt;


&lt;h2&gt;
  
  
  The answer is both
&lt;/h2&gt;

&lt;p&gt;Hybrid search is not some fancy optimization.&lt;br&gt;&lt;br&gt;
It is the basic admission that no single retrieval signal is enough.&lt;/p&gt;

&lt;p&gt;The flow usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  ↓
Vector Search
  ↓
Keyword Search
  ↓
Fuse Results
  ↓
Rerank
  ↓
Send to LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;semantic retrieval finds the concept,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;keyword retrieval finds the exact phrase,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fusion combines the strengths,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;reranking picks the best final candidates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

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

&lt;blockquote&gt;
&lt;p&gt;“Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vector search might return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;billing policy chunks,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;plan upgrade chunks,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;invoice-related chunks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keyword search might return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;exact mention of “Professional”,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact mention of “Enterprise”,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact mention of “active invoices”.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you merge both lists, suddenly the system has a much better chance of building the full answer instead of just a vaguely related one.&lt;/p&gt;

&lt;p&gt;That is the difference between “sounds right” and “is right.”&lt;/p&gt;




&lt;h2&gt;
  
  
  The fusion problem
&lt;/h2&gt;

&lt;p&gt;The tricky part is that vector scores and BM25 scores do not live on the same scale.&lt;/p&gt;

&lt;p&gt;You cannot just add them blindly and hope the universe respects your optimism.&lt;/p&gt;

&lt;p&gt;That is why production systems use score fusion methods like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;weighted sum,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;rank-based fusion,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reciprocal Rank Fusion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact method matters less than the principle:&lt;br&gt;&lt;br&gt;
do not force two different ranking systems to pretend they are the same thing.&lt;/p&gt;


&lt;h2&gt;
  
  
  RRF in plain English
&lt;/h2&gt;

&lt;p&gt;Reciprocal Rank Fusion is popular because it rewards documents that rank well in both systems without caring too much about score scale.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fuse_rrf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector_ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bm25_ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&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="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector_ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bm25_ranked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rrf_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&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="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not the exact formula.&lt;br&gt;&lt;br&gt;
The point is that the system stops trusting one retriever too much.&lt;/p&gt;


&lt;h2&gt;
  
  
  Metadata comes first
&lt;/h2&gt;

&lt;p&gt;Hybrid search should not run across everything in the universe.&lt;/p&gt;

&lt;p&gt;Before you search, you usually want to narrow the field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;language = en&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;document_type = policy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version = latest&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;department = billing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That way the retrievers are not wasting time on content that should never have been considered in the first place.&lt;/p&gt;

&lt;p&gt;This matters because a good production system is not just about finding more.&lt;br&gt;&lt;br&gt;
It is about finding less, but better.&lt;/p&gt;


&lt;h2&gt;
  
  
  Example code
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hybrid_retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bm25_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata_filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vector_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metadata_filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bm25_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bm25_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metadata_filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;fused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fuse_rrf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bm25_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;top_candidate_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fused&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;top_candidate_ids&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is the shape of the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;search twice,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fuse,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;narrow,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then rerank later.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Why hybrid search feels more natural
&lt;/h2&gt;

&lt;p&gt;Hybrid search works because people do not ask questions in one pure mode.&lt;/p&gt;

&lt;p&gt;Sometimes they say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“upgrade plan”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“active invoices”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“GPT-4.1”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“ERR-5027”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes they say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“what happens if I still owe money?”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“how do I move to a higher tier?”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“does this apply to old versions?”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They mix exact terms and fuzzy intent in the same sentence.&lt;/p&gt;

&lt;p&gt;Hybrid search is basically the system saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Fine. I’ll handle both.”&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Chapter 9 — Reranking
&lt;/h2&gt;

&lt;p&gt;Retrieval finds candidates.&lt;br&gt;&lt;br&gt;
Reranking chooses the one that actually deserves to survive.&lt;/p&gt;

&lt;p&gt;That distinction sounds small until you build a real system and realize that the first-stage retriever is often good at finding the right neighborhood, but not good enough at choosing the right house. It gives you the right area. The reranker decides which door matters.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why reranking exists
&lt;/h2&gt;

&lt;p&gt;A query like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;usually produces a decent shortlist from hybrid search. But shortlist is not answer quality. You might get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;one chunk about upgrading,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;one chunk about invoices,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;one chunk about billing support,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;one chunk about a related policy,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and one chunk that is technically similar but not actually useful.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is normal. Retrieval is designed to cast a wide net.&lt;br&gt;&lt;br&gt;
Reranking is what narrows that net into something the LLM can trust.&lt;/p&gt;


&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;Vector search and BM25 are fast.&lt;br&gt;&lt;br&gt;
Cross-encoder reranking is slower.&lt;br&gt;&lt;br&gt;
So production systems split the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  ↓
Hybrid Search
  ↓
Top 20–50 candidates
  ↓
Cross-Encoder Reranker
  ↓
Top 3–5 chunks
  ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the standard retrieve-then-rerank shape because it balances speed and precision. The first stage optimizes recall. The second stage optimizes correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the first ranking is not enough
&lt;/h2&gt;

&lt;p&gt;The first retriever often returns something that is “close enough,” which is exactly the problem.&lt;/p&gt;

&lt;p&gt;For example, imagine the shortlist contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. "Customers can upgrade from Professional to Enterprise."
2. "Active invoices must be closed before upgrading."
3. "Billing support and payment history."
4. "How to reset your password."
5. "Downgrading is allowed only if no active trials exist."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A human can instantly see that 1 and 2 matter most. But the retriever only sees statistical similarity. It knows what is related, not what is most answer-bearing.&lt;/p&gt;

&lt;p&gt;That is the gap reranking closes.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a cross-encoder thinks
&lt;/h2&gt;

&lt;p&gt;A cross-encoder takes the query and the candidate chunk together and scores the pair as one unit.&lt;/p&gt;

&lt;p&gt;That is different from embeddings.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A bi-encoder says: “these two texts look similar in space.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A cross-encoder says: “this chunk answers this query better than the other chunk.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That extra interaction is expensive, but it is much more precise.&lt;/p&gt;

&lt;p&gt;This is why rerankers are usually the cheapest way to improve answer quality once retrieval is already decent. They do not fix bad retrieval. They fix bad ordering.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Suppose the query is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the retrieved chunks are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Customers can upgrade from Professional to Enterprise.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Active invoices must be closed before upgrading.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“How to reset your password.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Downgrading is allowed only if no active trials exist.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reranker will likely push the first two to the top because they jointly answer the question. The third is irrelevant. The fourth is related but not the right policy branch.&lt;/p&gt;

&lt;p&gt;That is the difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;finding similar text,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and finding the most useful text.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example code
&lt;/h2&gt;

&lt;p&gt;Here is the shape of a basic cross-encoder reranking step:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;reranker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CrossEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cross-encoder/ms-marco-MiniLM-L-6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reranker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&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="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;top_n&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the essential idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;retrieve many,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;score each query-chunk pair,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;keep the best few.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why reranking is so valuable
&lt;/h2&gt;

&lt;p&gt;Reranking improves the part of the pipeline that users actually feel.&lt;/p&gt;

&lt;p&gt;It helps when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;chunks are semantically close but not equally useful,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the corpus contains overlapping policies,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the query is specific,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact answer selection matters,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the system has too much context noise.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, reranking is one of the highest ROI improvements in production RAG because it upgrades quality without forcing you to rebuild everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hidden benefit
&lt;/h2&gt;

&lt;p&gt;Reranking also makes debugging easier.&lt;/p&gt;

&lt;p&gt;If retrieval looks good but the answer is still wrong, the problem may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;bad reranking,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;bad chunking,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or bad context construction.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If retrieval itself is weak, reranking cannot save it.&lt;br&gt;&lt;br&gt;
That is important. Reranking is not a miracle layer. It is a refinement layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where reranking sits in the architecture
&lt;/h2&gt;

&lt;p&gt;The right mental model is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Retrieval finds enough candidates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reranking decides which candidates are worth using.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generation turns those candidates into an answer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the first stage is the net, the reranker is the hand that chooses the fish you actually keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Retrieval is not just about finding text that looks similar to a user's query. A production RAG system must determine which information is actually relevant, which sources should be trusted, and which results should be excluded before they ever reach the LLM.&lt;/p&gt;

&lt;p&gt;Vector similarity provides semantic relevance, but it does not provide enough control on its own. Hybrid search, metadata filtering, and reranking work together to narrow a large candidate set into the small amount of context that the model actually needs.&lt;/p&gt;

&lt;p&gt;That is the core idea behind production retrieval: &lt;strong&gt;don't just retrieve more information—retrieve the right information, in the right order, for the right query.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Continue the Series
&lt;/h2&gt;

&lt;p&gt;This article focused on the retrieval layer of a production RAG system: embeddings, hybrid search, query optimization, reranking, and context compression. Together, these techniques help turn a large set of possible matches into a smaller, more relevant context for the LLM.&lt;/p&gt;

&lt;p&gt;But a retrieval pipeline that works well on a small dataset can behave very differently when the system needs to handle millions of documents, concurrent users, strict latency requirements, and growing infrastructure costs.&lt;/p&gt;

&lt;p&gt;In the next article, we'll move from retrieval quality to production scale. We'll explore how to design RAG architectures that remain fast, reliable, and cost-efficient as the amount of data and traffic grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4 — Scaling RAG Systems: Production Architecture and Performance Optimization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Large-scale RAG architecture&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling ingestion and retrieval pipelines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vector database performance and indexing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Caching and latency optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Async processing and background workers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Designing RAG systems for millions of documents and concurrent users&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this series, you'll have a complete engineering framework for designing, building, scaling, and evaluating production-grade Retrieval-Augmented Generation systems.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Building a Production RAG Pipeline: Document Processing, Chunking, and Metadata Design</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Wed, 05 Aug 2026 08:47:21 +0000</pubDate>
      <link>https://dev.to/damir-karimov/building-a-production-rag-pipeline-document-processing-chunking-and-metadata-design-5djf</link>
      <guid>https://dev.to/damir-karimov/building-a-production-rag-pipeline-document-processing-chunking-and-metadata-design-5djf</guid>
      <description>&lt;p&gt;In the first article, we explored why many RAG systems fail in production and established a key principle: retrieval quality determines answer quality. We also introduced the architecture behind production-grade RAG systems and explained why a simple "embeddings + vector database + LLM" pipeline is rarely enough.&lt;/p&gt;

&lt;p&gt;In this second part, we'll move one step earlier in the pipeline—to the moment a document first enters your system. We'll examine how documents should be ingested, cleaned, parsed, chunked, and enriched with metadata before a single embedding is generated. These decisions form the foundation of every production RAG system and often have a greater impact on retrieval quality than the choice of embedding model or LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production RAG Architecture Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Why Most RAG Systems Fail in Production: The Hidden Architecture Problems Behind AI Search&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building a Production RAG Pipeline: Document Processing, Chunking, and Metadata Design&lt;/strong&gt; &lt;em&gt;(you are here)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Beyond Vector Search: Building Better RAG Retrieval with Hybrid Search and Reranking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling RAG Systems: Production Architecture and Performance Optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluating Production RAG Systems: Metrics, Monitoring, and Common Failure Patterns&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Chapter 4 — Ingestion: Where Retrieval Really Begins
&lt;/h2&gt;

&lt;p&gt;In production, retrieval does not begin when the user asks a question. It begins the moment a document enters your system. If ingestion is weak, every later step inherits the damage, and no amount of clever prompting will fully recover it.&lt;/p&gt;

&lt;p&gt;This is why ingestion is not a “PDF parsing task.” It is the process of turning messy human documents into structured, searchable, trustworthy knowledge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why ingestion matters
&lt;/h2&gt;

&lt;p&gt;A document is rarely just plain text. A contract has sections, tables, headers, footers, page numbers, and sometimes scanned signatures. A handbook may contain headings, code blocks, bullet lists, and embedded screenshots. A policy document may look readable to a person but be almost useless to a machine if the structure is flattened too early.&lt;/p&gt;

&lt;p&gt;If you simply extract all text and dump it into a chunker, you create noise. The model then sees fragments of meaning instead of complete ideas. That is how a document with a clear rule like “active invoices must be closed before upgrading” can turn into a retrieval result that feels relevant but answers the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What good ingestion does
&lt;/h2&gt;

&lt;p&gt;A good ingestion pipeline does four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It preserves structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It removes noise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It extracts metadata.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It prepares the document for retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds obvious, but most weak systems do only one of these, and often badly. They extract text, maybe split it into chunks, and then assume the rest will work itself out. In practice, ingestion is where the system either becomes searchable knowledge or remains a pile of text.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple mental model
&lt;/h2&gt;

&lt;p&gt;Think of ingestion like preparing ingredients before cooking.&lt;/p&gt;

&lt;p&gt;You do not throw whole vegetables, packaging, and labels into the pan and hope the meal will sort itself out. You wash, peel, cut, sort, and label things first. In the same way, a document should be cleaned, structured, and annotated before it is indexed.&lt;/p&gt;

&lt;p&gt;Here is the basic shape of the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload 
    ↓     
Object Storage 
    ↓ 
Parser 
    ↓ 
Cleaner 
    ↓ 
Structure-aware Chunker 
    ↓ 
Metadata Extractor 
    ↓ 
Embeddings 
    ↓ 
Indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step exists because raw documents are messy and retrieval systems are picky.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parsing is not extraction
&lt;/h2&gt;

&lt;p&gt;Parsing is about understanding format, not just getting text out. A PDF parser should know whether the document is scanned or digital, whether the content is laid out in columns, whether tables are visually important, and whether the file contains repeated headers or footers. An HTML parser should preserve semantic tags. A Markdown parser should keep headings, lists, and code blocks intact.&lt;/p&gt;

&lt;p&gt;If the parser is dumb, everything after it gets worse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cleaning is not optional
&lt;/h2&gt;

&lt;p&gt;After parsing, the document usually still contains junk. You may see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;page numbers,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;repeated company names,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“confidential” banners,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;broken line wraps,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;duplicated paragraphs,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OCR errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do not remove these, they will be embedded, stored, and later retrieved as if they mattered. That is one of the quietest ways to ruin search quality.&lt;/p&gt;

&lt;p&gt;A clean ingestion pipeline removes noise without deleting meaning. That balance matters. Over-cleaning can damage content, while under-cleaning pollutes retrieval.&lt;/p&gt;




&lt;h2&gt;
  
  
  Structure preservation
&lt;/h2&gt;

&lt;p&gt;Structure is what makes a document usable. Headings tell you what a section is about. Tables encode relationships. Lists show hierarchy. Code blocks preserve exact syntax. Losing that structure is often more harmful than losing a few words.&lt;/p&gt;

&lt;p&gt;For example, these two representations are not equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment 
Terms Invoices must be paid within 30 days. 
Failure to pay may result in service suspension.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoices must be paid within 30 days. 
Failure to pay may result in service suspension.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one is technically readable, but the first one is much more useful because the heading gives the chunk a clear semantic frame. That frame helps retrieval and helps the LLM explain the answer later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Metadata begins here
&lt;/h2&gt;

&lt;p&gt;Metadata is not something you attach at the end. It should be born during ingestion.&lt;/p&gt;

&lt;p&gt;Useful metadata often includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;document type,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;title,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;section,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;page number,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;language,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;source system,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;department,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;creation time,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;last updated time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This metadata is what lets the system answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Show me the latest policy.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Search only English documents.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Find finance documents.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Ignore deprecated versions.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Return the clause from page 12.”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without metadata, everything becomes a vague semantic soup.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical example
&lt;/h2&gt;

&lt;p&gt;Suppose you ingest this text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Title: Pricing Policy

Section: Upgrading Plans
Customers can upgrade from Professional to Enterprise.
Important: Active invoices must be closed before upgrading.

Section: Downgrading Plans
Downgrading is allowed only if no active trials exist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A weak pipeline might store it as one long blob. A better pipeline creates structured records like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customers can upgrade from Professional to Enterprise. Important: Active invoices must be closed before upgrading."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upgrading Plans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"document_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the retriever can filter, rank, and cite this chunk much more intelligently.&lt;/p&gt;




&lt;h2&gt;
  
  
  OCR and scanned documents
&lt;/h2&gt;

&lt;p&gt;Scanned documents are where ingestion often breaks in production. If text exists only as pixels, you need OCR before anything else can happen. And OCR is not just a fallback feature for edge cases. In many real organizations, a large share of useful knowledge lives in scanned forms, signed contracts, invoices, and old PDFs.&lt;/p&gt;

&lt;p&gt;If OCR is poor, the system may miss critical words or mix table cells together. If layout is ignored, the model may see nonsense like one long broken sentence or a table flattened into unreadable text. That is why good ingestion treats OCR and layout extraction as first-class concerns.&lt;/p&gt;




&lt;h2&gt;
  
  
  The main failure pattern
&lt;/h2&gt;

&lt;p&gt;The most common failure pattern is this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Document enters the system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Text is extracted too early.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structure is lost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chunking splits the wrong boundaries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata is missing or shallow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieval returns “relevant” but incomplete context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The LLM answers confidently with the wrong interpretation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That failure often looks like an AI problem, but it is really an ingestion problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to aim for
&lt;/h2&gt;

&lt;p&gt;Your ingestion pipeline should produce chunks that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;readable on their own,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tied to a known section or source,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;enriched with metadata,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;clean enough to embed,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and traceable back to the original document.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a chunk cannot tell you where it came from and why it exists, it is probably not ready for production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Ingestion is not a support task for retrieval. It is the foundation of retrieval.&lt;/p&gt;

&lt;p&gt;That is why production RAG systems spend so much effort on parsing, cleaning, structure, and metadata before they ever touch embeddings. Once that foundation is solid, everything downstream becomes easier, cheaper, and more trustworthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5 — Chunking: The Heart of Retrieval Quality
&lt;/h2&gt;

&lt;p&gt;Chunking is where most RAG systems quietly fall apart. You can have a solid model, a fast vector database, and a polished prompt, but if the chunks are badly shaped, retrieval will still miss the point.&lt;/p&gt;

&lt;p&gt;The reason is simple: retrieval does not search for “truth.” It searches for pieces of text that look useful. If those pieces are incomplete, noisy, or cut in the wrong place, the whole system starts answering with confidence and precision in all the wrong directions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why chunking matters
&lt;/h2&gt;

&lt;p&gt;Think about reading a manual where every page was cut into random strips and shuffled. You could still recognize some words, but you would not trust the result. That is exactly what happens when documents are split by token count only.&lt;/p&gt;

&lt;p&gt;A good chunk is not just “small enough.” It should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;semantically coherent,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;self-contained enough to understand,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;aligned with document structure,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and useful on its own when retrieved.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a chunk needs three other chunks just to make sense, retrieval quality drops fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The wrong way to chunk
&lt;/h2&gt;

&lt;p&gt;The most common beginner approach is fixed-size splitting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1000 tokens
↓
cut anywhere
↓
chunk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks clean in code and terrible in practice. A sentence may get broken in half. A table row may be separated from its header. A section title may end up in one chunk while the content appears in another. Then the retriever sees fragments instead of ideas.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original:
"Active invoices must be closed before upgrading from Professional to Enterprise."

Bad chunk 1:
"Active invoices must be closed before upgrading from Professional"

Bad chunk 2:
"to Enterprise."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither chunk is ideal. One is incomplete, the other is meaningless. The model may still answer, but it will be guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fixed-size chunking
&lt;/h2&gt;

&lt;p&gt;Fixed-size chunking is easy to implement and easy to explain, which is why it appears in so many tutorials. It is fine for quick experiments, but it should rarely be your final choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;raw plain text,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;prototypes,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;quick baselines.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bad for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;contracts,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;docs with headings,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tables,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;code-heavy documents,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;anything where structure matters.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is not just correctness. Fixed-size splitting also creates retrieval noise. Because chunks are arbitrarily cut, the vector representation becomes less focused. That means the search engine has to work harder just to recover meaning that was destroyed during preprocessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sliding window chunking
&lt;/h2&gt;

&lt;p&gt;Sliding window chunking is the first small improvement. Instead of cutting once and moving on, you keep overlap between adjacent chunks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chunk 1: [0..500]
Chunk 2: [400..900]
Chunk 3: [800..1300]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps preserve context at the boundaries. If an important sentence gets cut in one chunk, it may still appear in the next one. That alone can significantly improve retrieval.&lt;/p&gt;

&lt;p&gt;But overlap is not magic. If the source structure is bad, sliding windows just give you two slightly redundant bad chunks instead of one. It reduces damage; it does not solve the design problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Structure-aware chunking
&lt;/h2&gt;

&lt;p&gt;A better approach is to chunk according to the document itself.&lt;/p&gt;

&lt;p&gt;If the source is Markdown, use headings. If it is HTML, use semantic tags. If it is a contract, use clauses and sections. If it is a handbook, use chapters and paragraphs.&lt;/p&gt;

&lt;p&gt;That gives you chunks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Section: Upgrading Plans

Customers can upgrade from Professional to Enterprise.
Important: Active invoices must be closed before upgrading.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much stronger than a blind token slice because the chunk carries its own context. When the retriever finds it, the LLM sees something that already resembles an answer-ready unit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Semantic chunking
&lt;/h2&gt;

&lt;p&gt;Semantic chunking goes one step further. Instead of splitting based only on formatting, it tries to split by meaning. The system looks for topic shifts, idea boundaries, or sentence groups that belong together.&lt;/p&gt;

&lt;p&gt;This is especially useful when documents are less formal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;product documentation,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;internal notes,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;policy text,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;long articles,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mixed-format knowledge bases.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A semantic chunk should feel like a paragraph a human would naturally quote. If you would not send it to a colleague as a useful excerpt, it is probably not a good chunk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parent-child chunking
&lt;/h2&gt;

&lt;p&gt;One of the most effective production patterns is parent-child chunking.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;search with small child chunks,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;answer with larger parent chunks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;small chunks are easier to match precisely,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;large chunks provide enough surrounding context for generation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parent chunk:
Section: Upgrading Plans
Customers can upgrade from Professional to Enterprise.
Important: Active invoices must be closed before upgrading.
If invoices are open, contact billing.

Child chunks:
1. Customers can upgrade from Professional to Enterprise.
2. Important: Active invoices must be closed before upgrading.
3. If invoices are open, contact billing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child chunks help retrieval find the relevant idea. The parent chunk gives the model the full answer. This reduces both hallucination and context loss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hierarchical chunking
&lt;/h2&gt;

&lt;p&gt;Hierarchical chunking is what happens when you stop thinking in one layer.&lt;/p&gt;

&lt;p&gt;Instead of one flat list of chunks, you keep multiple levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;document,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;section,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;paragraph,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;sentence.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you more control. A broad query can retrieve a section. A precise query can retrieve a paragraph. A citation can point to the exact sentence.&lt;/p&gt;

&lt;p&gt;This is especially useful in large corpora where one document may contain many unrelated topics. If you only use flat chunks, the system may know the right document but still miss the right passage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recursive chunking
&lt;/h2&gt;

&lt;p&gt;Recursive chunking is a practical compromise. You split by large boundaries first, then smaller ones if needed.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;split by headings,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then paragraphs,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then sentences,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;then tokens if absolutely necessary.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps structure intact whenever possible and only falls back to smaller splits when the document is too large. It is a solid default for many production systems because it behaves more like a human editor than a token cutter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contextual chunking
&lt;/h2&gt;

&lt;p&gt;Sometimes a chunk is too short to stand alone, even if it is semantically clean. In that case, you add context around it.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document title: Pricing Policy
Section: Upgrading Plans

Chunk text:
Customers can upgrade from Professional to Enterprise.
Important: Active invoices must be closed before upgrading.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This extra context makes the chunk much easier to understand at retrieval time. It also helps the LLM produce better grounded answers because the chunk now carries its own label.&lt;/p&gt;




&lt;h2&gt;
  
  
  When chunking goes wrong
&lt;/h2&gt;

&lt;p&gt;Chunking failures usually show up in very human ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the answer is almost right but misses one rule,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the LLM cites the wrong section,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;two different policies get blended into one answer,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a table becomes unreadable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a “yes” answer appears when the real answer is “yes, but only if...”.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why chunking is not a preprocessing detail. It is an information design problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical guidance
&lt;/h2&gt;

&lt;p&gt;If you are building a real production system, start like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;preserve structure before splitting,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;keep headings with their content,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;avoid arbitrary cuts inside tables or code blocks,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;use overlap only when needed,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;test chunks with real queries, not just with sample text.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good chunk should answer one natural question or support one natural fact. If it feels like a random slice of text, it probably is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The main idea
&lt;/h2&gt;

&lt;p&gt;Chunking is not about making text smaller. It is about making knowledge retrievable.&lt;/p&gt;

&lt;p&gt;That sounds simple, but it is the difference between a chatbot that sounds smart and a system people actually trust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6 — Metadata: The Difference Between “Similar” and “Correct”
&lt;/h2&gt;

&lt;p&gt;Metadata is where production RAG stops being a clever text search system and starts becoming a trustworthy product. Without metadata, the system can only say, “this looks related”; with metadata, it can say, “this is the right document, from the right version, for the right user, in the right context.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Why metadata matters
&lt;/h2&gt;

&lt;p&gt;Imagine a supermarket search.&lt;/p&gt;

&lt;p&gt;A customer asks for “red dry wine under 20 euros.” If you only use semantic similarity, you may get wines that are red, sweet, expensive, or merely described in a similar style. If you also apply structured filters, you immediately rule out the wrong candidates before retrieval gets expensive or confused. Metadata turns vague similarity into precise constraint handling.&lt;/p&gt;

&lt;p&gt;RAG behaves the same way. A user often does not want any semantically related chunk. They want the latest policy, the right language, the correct region, the right team, or the current version of a document. Metadata lets the system narrow the world before it starts ranking chunks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What metadata actually is
&lt;/h2&gt;

&lt;p&gt;Metadata is not extra decoration. It is structured information attached to documents and chunks so the system can reason about them later.&lt;/p&gt;

&lt;p&gt;Useful metadata usually includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;document title,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;document type,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;section or heading,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;page number,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;language,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;source system,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;department,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;created and updated timestamps,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;access level,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tags or topic labels.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the document level, metadata helps you know what something is. At the chunk level, it helps you know where a passage came from and whether it should be considered for a particular query.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why chunks need metadata too
&lt;/h2&gt;

&lt;p&gt;A chunk without metadata is just floating text. It may be semantically meaningful, but it loses the context that makes retrieval reliable.&lt;/p&gt;

&lt;p&gt;For example, this chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customers can upgrade from Professional to Enterprise.
Important: Active invoices must be closed before upgrading.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is much more useful if it also carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upgrading Plans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"document_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the retriever can do more than similarity search. It can filter out old versions, ignore unrelated departments, and keep answers tied to the source section that actually matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Metadata is a retrieval control system
&lt;/h2&gt;

&lt;p&gt;Think of retrieval as two decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Which documents are even eligible?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Among those, which chunks are best?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Metadata answers the first question.&lt;/p&gt;

&lt;p&gt;A user asking about “the latest finance policy for EMEA” does not need every chunk in your system. They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;finance documents,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;current version,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;relevant region,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;probably English,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;probably policy, not support chat or engineering notes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do this with pure vector similarity, the system has to discover all of that indirectly. That is slow, noisy, and unreliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hard filters and soft filters
&lt;/h2&gt;

&lt;p&gt;Not all metadata is equal.&lt;/p&gt;

&lt;p&gt;Some metadata is &lt;strong&gt;hard&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;tenant ID,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;permissions,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;access level,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;document status,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;version validity.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are non-negotiable. If a document is superseded, deleted, or private, it should not be considered at all.&lt;/p&gt;

&lt;p&gt;Other metadata is &lt;strong&gt;soft&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;topic tags,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;department,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;region,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;audience level,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;freshness preference.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These improve relevance, but they should not cause the system to return nothing if they are slightly off.&lt;/p&gt;

&lt;p&gt;A good production system treats hard filters as gates and soft filters as ranking hints.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: same question, different metadata
&lt;/h2&gt;

&lt;p&gt;Suppose a user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a customer upgrade from Professional to Enterprise with active invoices?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system should not just search for similar wording. It should also know whether the answer belongs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a current pricing policy,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a deprecated policy,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a support article,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a legal document,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;or an internal note.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without metadata, all of these may compete in the ranking pool. With metadata, the search space becomes much smaller and much safer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Query-time filtering
&lt;/h2&gt;

&lt;p&gt;Metadata is most powerful when it is used at query time.&lt;/p&gt;

&lt;p&gt;A good flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Query
  ↓
Extract likely filters
  ↓
Apply hard constraints
  ↓
Search within allowed subset
  ↓
Rerank results
  ↓
Send best chunks to LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if the query implies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;language = en&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;document_type = policy&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;version = latest&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then those filters should shape the retrieval before the model sees anything.&lt;/p&gt;

&lt;p&gt;That is one of the biggest reasons production RAG feels much better than tutorial RAG: it is not searching the whole world anymore. It is searching the right slice of the world.&lt;/p&gt;




&lt;h2&gt;
  
  
  Metadata-aware chunking
&lt;/h2&gt;

&lt;p&gt;Metadata is also a chunking problem.&lt;/p&gt;

&lt;p&gt;If a paragraph belongs to a section, the chunk should know that. If a table belongs to a page, the chunk should know that too. If a chunk comes from a source that is deprecated, it should carry that status.&lt;/p&gt;

&lt;p&gt;Good chunk metadata often includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;parent document ID,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;section name,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;page number,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;hierarchy path,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;extraction method,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;language,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;freshness state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;filtering,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ranking,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;citations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, the system may retrieve the right words but fail to explain where they came from.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple example schema
&lt;/h2&gt;

&lt;p&gt;Here is a practical chunk representation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pricing_policy_p12_s3_c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Active invoices must be closed before upgrading from Professional to Enterprise."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pricing Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upgrading Plans"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"document_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"department"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This chunk is now useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;semantic search,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;exact filtering,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;citations,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;debugging,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;auditing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What happens without metadata
&lt;/h2&gt;

&lt;p&gt;Without metadata, retrieval tends to become “close enough” search. That sounds fine until the stakes rise.&lt;/p&gt;

&lt;p&gt;Examples of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a user gets an answer from an outdated policy,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the system returns the wrong region,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;support sees a marketing document instead of a billing rule,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a legal question is answered from an internal note,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a multilingual corpus returns the wrong language.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model may still generate a fluent answer, but fluency is not correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Metadata is not a nice-to-have. It is the thing that turns retrieval from approximate text similarity into controlled information access.&lt;/p&gt;

&lt;p&gt;If chunking defines what a unit of knowledge looks like, metadata defines when and why that unit should be used.&lt;/p&gt;

&lt;p&gt;That is why serious RAG systems treat metadata design as a first-class architecture decision, not a last-minute indexing detail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Continue the Series
&lt;/h2&gt;

&lt;p&gt;This article covered the foundation of every production RAG system: document ingestion, parsing, chunking, and metadata design. These preprocessing decisions determine how effectively your retrieval pipeline can find relevant information long before an LLM generates an answer.&lt;/p&gt;

&lt;p&gt;In the next article, we'll move from document preparation to retrieval itself. We'll explore why vector search alone is not enough and how modern production systems combine multiple retrieval techniques to achieve significantly higher accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3 — Beyond Vector Search: Building Better RAG Retrieval with Hybrid Search and Reranking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Embeddings and semantic representation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hybrid search (vector search + BM25)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Query rewriting and optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reranking with cross-encoders&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Context compression strategies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Building a production retrieval pipeline&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this series, you'll have a complete engineering framework for designing, building, scaling, and evaluating production-grade Retrieval-Augmented Generation systems.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>ai</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Why Most RAG Systems Fail in Production: The Hidden Architecture Problems Behind AI Search</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:12:04 +0000</pubDate>
      <link>https://dev.to/damir-karimov/why-most-rag-systems-fail-in-production-the-hidden-architecture-problems-behind-ai-search-2ce3</link>
      <guid>https://dev.to/damir-karimov/why-most-rag-systems-fail-in-production-the-hidden-architecture-problems-behind-ai-search-2ce3</guid>
      <description>&lt;p&gt;Building a reliable RAG system is not about connecting an LLM to a vector database. It requires a complete architecture covering data ingestion, retrieval, ranking, evaluation, and production operations.&lt;/p&gt;

&lt;p&gt;This article is the first part of a five-part series where we will explore how modern RAG systems are designed, why naive implementations fail, and what it takes to build AI search systems that work reliably in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production RAG Architecture Series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why Most RAG Systems Fail in Production: The Hidden Architecture Problems Behind AI Search&lt;/strong&gt; (you are here)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Building a Production RAG Pipeline: Document Processing, Chunking, and Metadata Design&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Beyond Vector Search: Building Better RAG Retrieval with Hybrid Search and Reranking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling RAG Systems: Production Architecture and Performance Optimization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Evaluating Production RAG Systems: Metrics, Monitoring, and Common Failure Patterns&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Chapter 1 — Why Most RAG Systems Fail in Production
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo works. Production fails.
&lt;/h2&gt;

&lt;p&gt;Building a Retrieval-Augmented Generation system isn't hard.&lt;br&gt;&lt;br&gt;
Building one that people can trust is.&lt;/p&gt;

&lt;p&gt;You've probably seen hundreds of tutorials:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Documents → Split into Chunks → Generate Embeddings → Store in Vector DB → Similarity Search → Send Context to LLM → Answer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It looks beautifully simple.&lt;br&gt;&lt;br&gt;
You can build a working chatbot in 30 minutes with LangChain, LlamaIndex, or Haystack.&lt;/p&gt;

&lt;p&gt;Upload a PDF.&lt;br&gt;&lt;br&gt;
Generate embeddings.&lt;br&gt;&lt;br&gt;
Store them.&lt;br&gt;&lt;br&gt;
Ask questions.&lt;br&gt;&lt;br&gt;
Done.&lt;/p&gt;

&lt;p&gt;For a demo, that's enough.&lt;br&gt;&lt;br&gt;
For production, that's the beginning of your problems.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Imagine this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You built an internal AI assistant for your company's documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User: "How do I reset my password?"&lt;br&gt;&lt;br&gt;
Assistant: "Go to Settings → Security → Reset Password. You'll receive an email with a link."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perfect. Everyone applauds. Project approved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two weeks later, support starts using it.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User: "Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?"&lt;br&gt;&lt;br&gt;
Assistant: "Yes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Actual answer from the policy document:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Active invoices must be closed before upgrading from Professional to Enterprise."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody notices immediately.&lt;br&gt;&lt;br&gt;
Support gives wrong advice.&lt;br&gt;&lt;br&gt;
Customer tries to upgrade.&lt;br&gt;&lt;br&gt;
Billing rejects.&lt;br&gt;&lt;br&gt;
Support spends hours resolving.&lt;br&gt;&lt;br&gt;
Management asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I thought AI was supposed to reduce support workload."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nothing crashed.&lt;br&gt;&lt;br&gt;
Embeddings were correct.&lt;br&gt;&lt;br&gt;
Vector DB was running.&lt;br&gt;&lt;br&gt;
Similarity search returned relevant chunks.&lt;br&gt;&lt;br&gt;
LLM generated fluent text.&lt;/p&gt;

&lt;p&gt;Every component worked.&lt;br&gt;&lt;br&gt;
Yet the system failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is what makes production RAG difficult.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Failures rarely come from broken software.&lt;br&gt;&lt;br&gt;
They come from broken architecture.&lt;/p&gt;


&lt;h2&gt;
  
  
  The tutorial pipeline vs the real pipeline
&lt;/h2&gt;

&lt;p&gt;Most tutorials focus on this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  ↓
Vector Search
  ↓
LLM
  ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real production systems look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ingestion (Offline)

  Upload
    ↓
  Object Storage (S3 / GCS)
    ↓
  OCR / Parser (PDF, HTML, Markdown)
    ↓
  Content Cleaning (headers, footers, page numbers)
    ↓
  Chunking (semantic + structure-aware)
    ↓
  Metadata Extraction (doc type, language, version, section)
    ↓
  Embedding Workers
    ↓
    ├─→ Vector DB
    └─→ BM25 / Keyword Index

----------------------------

Retrieval (Online)

  User Query
    ↓
  Query Rewriting / Expansion
    ↓
  Metadata Filters (language, department, version)
    ↓
  Hybrid Search (Vector + BM25)
    ↓
  Reranking (Cross-Encoder)
    ↓
  Context Compression
    ↓
  Prompt Builder
    ↓
  LLM
    ↓
  Answer + Citations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The language model is often the &lt;strong&gt;smallest&lt;/strong&gt; part of the architecture.&lt;br&gt;&lt;br&gt;
But it receives almost all attention.&lt;/p&gt;


&lt;h2&gt;
  
  
  Retrieval is not solved
&lt;/h2&gt;

&lt;p&gt;The biggest misconception:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Retrieval is a solved problem. Just use a vector DB."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;Think about the Library of Congress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naive approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Throw all books into a giant pile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask someone to find the right one by reading random pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Eventually, they might find it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is basically what many beginner RAG systems do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Split everything into fixed-size chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Embed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieve top-k by cosine similarity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Feed to LLM.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Professional librarian:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before searching, they narrow it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Language?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Publication year?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Author?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Category?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Edition?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Print or digital?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Academic or popular?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after reducing millions of possibilities to a few hundred, they start semantic search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Professional retrieval works the same way.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Goal is not to search better.&lt;br&gt;&lt;br&gt;
Goal is to &lt;strong&gt;search less&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Vector DB is an index, not a database
&lt;/h2&gt;

&lt;p&gt;Another common mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Vector DB replaces traditional databases."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A vector DB is &lt;strong&gt;not&lt;/strong&gt; your source of truth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Think about Google:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Google doesn't own the internet.&lt;/li&gt;
&lt;li&gt;  It indexes the internet.&lt;/li&gt;
&lt;li&gt;  If Google servers disappear, websites still exist.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same with RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Your documents should live somewhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  PostgreSQL&lt;/li&gt;
&lt;li&gt;  S3 / GCS / Azure Blob&lt;/li&gt;
&lt;li&gt;  SharePoint / Confluence / Notion&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vector DB only stores an efficient representation for fast retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delete your vectors → you can rebuild them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delete your original documents → you lost everything.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many production systems fail because developers treat Vector DB as permanent storage.&lt;/p&gt;


&lt;h2&gt;
  
  
  Retrieval starts at ingestion
&lt;/h2&gt;

&lt;p&gt;Another mistake:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Retrieval begins when the user submits a question."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Retrieval begins the moment a document enters your system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine two companies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Company A:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Employee uploads PDF.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;System extracts plain text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Splits every 500 tokens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generates embeddings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stores them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Done.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Company B:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same PDF arrives.&lt;br&gt;&lt;br&gt;
Before embeddings, system asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is this real text or scanned images? (OCR needed?)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does it contain tables?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Headers / footers?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple languages?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version numbers?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Duplicate pages?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;References to other documents?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive information?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structured sections (chapters, clauses)?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after understanding the document, indexing begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which system produces better answers 6 months later?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Not the one with the better LLM.&lt;br&gt;&lt;br&gt;
The one with better ingestion.&lt;/p&gt;


&lt;h2&gt;
  
  
  RAG is an information retrieval problem
&lt;/h2&gt;

&lt;p&gt;If there's one idea to remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RAG is not an AI problem.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;RAG is an information retrieval problem that happens to use AI.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That changes everything.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which LLM should I use?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You start asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How should documents be represented?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How should information be organized?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What makes one chunk better than another?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which metadata should be extracted?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which ranking strategy should be used?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we measure retrieval quality?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we know retrieval failed?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those matter more than choosing GPT-5, Claude, or Gemini.&lt;br&gt;&lt;br&gt;
A brilliant LLM cannot generate knowledge it never received.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Garbage retrieval → garbage answers. Every time.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Chapter 2 — The Toy RAG (with real code and examples)
&lt;/h2&gt;

&lt;p&gt;Now let's see what a &lt;strong&gt;toy RAG&lt;/strong&gt; really looks like, and why it fails.&lt;/p&gt;

&lt;p&gt;We'll use Python + &lt;code&gt;sentence-transformers&lt;/code&gt; + &lt;code&gt;chromadb&lt;/code&gt; (local vector DB).&lt;br&gt;&lt;br&gt;
No LangChain magic, just raw code, so you see what's happening.&lt;/p&gt;

&lt;p&gt;We'll build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A naive RAG (fixed-size chunks).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Show where it breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compare with a slightly better version (structure-aware chunks + metadata).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Scenario: Company Knowledge Base
&lt;/h2&gt;

&lt;p&gt;Imagine you have a simple knowledge base:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;pricing_policy.md&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;onboarding_guide.md&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;support_rules.md&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example content from &lt;code&gt;pricing_policy.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Pricing Policy&lt;/span&gt;

&lt;span class="gu"&gt;## Upgrading Plans&lt;/span&gt;

Customers can upgrade from Professional to Enterprise.

&lt;span class="gs"&gt;**Important:**&lt;/span&gt; Active invoices must be closed before upgrading.

If you have any open invoices, contact billing@example.com.

&lt;span class="gu"&gt;## Downgrading Plans&lt;/span&gt;

Downgrading is allowed only if:
&lt;span class="p"&gt;-&lt;/span&gt; No active trials
&lt;span class="p"&gt;-&lt;/span&gt; No pending invoices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a user query:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Naive RAG with fixed-size chunks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chromadb.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Load documents
&lt;/span&gt;&lt;span class="n"&gt;documents_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;texts&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="n"&gt;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;documents_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Naive chunking: fixed size
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;naive_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;

&lt;span class="n"&gt;all_chunks&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="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;naive_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Embeddings
&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;convert_numpy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 4. Store in Chroma
&lt;/span&gt;&lt;span class="n"&gt;chroma_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chroma_db_impl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duckdb+parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;persist_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./chroma_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chroma_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;naive_rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 5. Query
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_naive_rag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;query_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;convert_numpy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collection&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="n"&gt;query_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Can Enterprise customers upgrade directly from the Professional plan while keeping active invoices?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;retrieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_naive_rag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chunk &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you'll likely see:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some chunks might contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"Customers can upgrade from Professional to Enterprise."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Downgrading is allowed only if..."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Random fragments with "invoices" but not the right rule.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the crucial sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Active invoices must be closed before upgrading from Professional to Enterprise."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Split across two chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Buried in a chunk with unrelated text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not in top-k at all.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then LLM will generate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Yes, they can upgrade."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the retrieved context doesn't say "No".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is exactly how naive RAG fails.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Not because embedding is wrong.&lt;br&gt;&lt;br&gt;
Because chunking + retrieval are not designed for real questions.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Slightly better RAG with structure-aware chunks + metadata
&lt;/h2&gt;

&lt;p&gt;Now let's do it more thoughtfully.&lt;/p&gt;

&lt;p&gt;We'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Split by Markdown headings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep section title + content together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add metadata: &lt;code&gt;doc_name&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;structure_aware_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Split by headings: # Section, ## Subsection
&lt;/span&gt;    &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(^#{1,2}\s+.+$)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# parts: [before_first_heading, heading1, content1, heading2, content2, ...]
&lt;/span&gt;
    &lt;span class="n"&gt;current_heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;current_content&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="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# New heading
&lt;/span&gt;            &lt;span class="n"&gt;current_heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;current_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Content
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;current_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_heading&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])):&lt;/span&gt;
                &lt;span class="c1"&gt;# End of section
&lt;/span&gt;                &lt;span class="n"&gt;content_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_heading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;section&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current_heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;
                    &lt;span class="n"&gt;current_heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
                    &lt;span class="n"&gt;current_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;

&lt;span class="n"&gt;all_chunks&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="n"&gt;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;documents_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;doc_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;structure_aware_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Embeddings + store (similar to before, but with metadata)
&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;convert_numpy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;collection2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chroma_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;better_rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;collection2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
        &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_better_rag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;query_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;convert_numpy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collection&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="n"&gt;query_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Combine text + metadata
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documents&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadatas&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;retrieved2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_better_rag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retrieved2&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chunk &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;doc_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, section: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;section&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What improves:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Chunks now preserve sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  "## Upgrading Plans" + full content.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Metadata tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Which document.&lt;/li&gt;
&lt;li&gt;  Which section.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieval more likely to find the exact section about upgrading + invoices.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now context to LLM might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Upgrading Plans&lt;/span&gt;

Customers can upgrade from Professional to Enterprise.

Important: Active invoices must be closed before upgrading.

If you have any open invoices, contact billing@example.com.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then LLM can answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"No. Active invoices must be closed before upgrading from Professional to Enterprise."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same embedding model.&lt;br&gt;&lt;br&gt;
Same LLM.&lt;br&gt;&lt;br&gt;
But better chunking + metadata → better retrieval → better answer.&lt;/p&gt;


&lt;h2&gt;
  
  
  What this shows
&lt;/h2&gt;

&lt;p&gt;Even in this tiny example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fixed-size chunks → fragmentation → wrong answer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structure-aware chunks + metadata → better context → correct answer.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, differences are even bigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Millions of documents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Many languages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Many versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complex queries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't design ingestion, chunking, and metadata properly, &lt;strong&gt;no LLM will save you&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Chapter 3 — Every Production RAG Has Two Pipelines
&lt;/h2&gt;

&lt;p&gt;If you've only followed tutorials, you probably think RAG is one pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  ↓
Retrieve
  ↓
Generate
  ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's enough for a demo.&lt;br&gt;&lt;br&gt;
It's catastrophic for production.&lt;/p&gt;

&lt;p&gt;In any real system, you actually have &lt;strong&gt;two separate pipelines&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Offline (ingestion) pipeline&lt;/strong&gt; – processes documents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Online (query) pipeline&lt;/strong&gt; – handles user requests.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They have completely different goals, constraints, and failure modes.&lt;br&gt;&lt;br&gt;
Designing them as one monolithic flow is one of the first mistakes that leads to production failures.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Offline Pipeline: Where Documents Live Their "Slow Life"
&lt;/h2&gt;

&lt;p&gt;The offline pipeline is what happens to documents before anyone ever asks a question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offline Pipeline (ingestion)

  Documents (PDF, HTML, Markdown, JSON)
    ↓
  Parser (structure-aware)
    ↓
  Cleaner (headers, footers, page numbers)
    ↓
  Chunker (semantic / structure-aware)
    ↓
  Metadata Extractor
    ↓
  Embedder
    ↓
  Indexer
    ├─→ Vector DB
    └─→ BM25 / Keyword Index

----------------------------

Online Pipeline (query)

  User Query
    ↓
  Query Processor (rewrite, expand)
    ↓
  Metadata Filters
    ↓
  Hybrid Search (Vector + BM25)
    ↓
  Reranker (Cross-Encoder)
    ↓
  Context Compression
    ↓
  Prompt Builder
    ↓
  LLM
    ↓
  Answer + Citations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this pipeline does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Parser&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Reads raw documents.

*   Extracts structure: headings, tables, lists, code blocks, paragraphs.

*   For PDFs: uses OCR if needed, understands layout.

*   For Markdown / HTML: preserves semantic tags.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Cleaner&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Removes noise:

    *   Headers / footers ("Company", "Confidential").

    *   Page numbers.

    *   Watermarks.

    *   Repeated boilerplate text.

*   Normalizes whitespace, encoding, line breaks.

*   Fixes broken sentences or artifacts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Chunker&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Splits documents into meaningful chunks.

*   Ensures:

    *   Headings are not cut in the middle.

    *   Tables stay intact.

    *   Paragraph boundaries are respected.

*   Uses strategies like:

    *   Structure-aware chunking

    *   Semantic chunking

    *   Parent-child chunking

    *   (We'll cover these in detail in Chapter 5.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Metadata Extractor&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Extracts document-level metadata:

    *   `document_type`

    *   `language`

    *   `version`

    *   `department`

    *   `created_at`, `updated_at`

*   Extracts chunk-level metadata:

    *   `section` / `heading`

    *   `page`

    *   `chapter`

    *   `parent_id`

    *   `hierarchy_path`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Embedder&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Computes embeddings for each chunk.

*   Optionally computes document-level embeddings too.

*   Uses models appropriate for:

    *   Domain (technical, legal, general).

    *   Language (English, Russian, multilingual).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Indexer&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Inserts chunks into:

    *   Vector DB (dense embeddings).

    *   BM25 / keyword index (sparse).

    *   (Optionally) graph index (for knowledge graphs).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Key properties of the offline pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It doesn't need to be fast.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Processing a document can take seconds or minutes.&lt;br&gt;&lt;br&gt;
Documents are not requested by users in real time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It must be correct and robust.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If ingestion is wrong, retrieval will never be right.&lt;br&gt;&lt;br&gt;
Bad chunks, missing metadata, wrong parsing → garbage retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It can be batched or incremental.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Batch: nightly jobs, reprocess entire corpus.&lt;/li&gt;
&lt;li&gt;  Incremental: process only new/updated documents.&lt;/li&gt;
&lt;li&gt;  Event-driven: triggered by upload events via queues.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pipeline is where you decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How documents are represented.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How information is organized.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What metadata is available for filtering.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What kind of chunks the retriever will see.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ignore this pipeline and only focus on "query → retrieve → generate", you're building a demo, not a production system.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Online Pipeline: Where Requests Live Their "Fast Life"
&lt;/h2&gt;

&lt;p&gt;The online pipeline is what happens when a user asks a question.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Online Pipeline (query)

  User Query
    ↓
  Query Processor (rewrite, expand, normalize)
    ↓
  Metadata Filters (language, department, version, etc.)
    ↓
  Hybrid Search (Vector + BM25)
    ↓
  Reranker (Cross-Encoder)
    ↓
  Context Compression
    ↓
  Prompt Builder
    ↓
  LLM
    ↓
  Answer + Citations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this pipeline does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Query Processor&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Rewrites or expands the query:

    *   "upgrade plan" → "upgrade from Professional to Enterprise plan".

    *   Adds implicit context (user role, department).

*   Normalizes:

    *   Lowercases, removes noise.

    *   Handles typos, slang, abbreviations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Metadata Filters&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Applies filters based on:

    *   User's language.

    *   User's department / team.

    *   Document version (latest, stable, deprecated).

    *   Document type (policy, doc, article).

*   Reduces the candidate set before search.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Hybrid Search&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Runs:

    *   Vector search (semantic similarity).

    *   BM25 / keyword search (exact term matching).

*   Combines scores:

    *   `final_score = α * vector_score + β * bm25_score`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Reranker&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Takes top-K candidates (e.g., 50–100).

*   Uses a cross-encoder to compute a more precise relevance score for each.

*   Sorts and picks the best N (e.g., 5–10).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Context Compression&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Limits the number of chunks sent to the LLM.

*   Drops low-relevance chunks.

*   Optionally summarizes or extracts key sentences.

*   Ensures the prompt stays within token limits.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Prompt Builder&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Constructs the final prompt:

    *   System instructions.

    *   Retrieved context.

    *   User query.

    *   Output format instructions (citations, confidence, etc.).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;LLM&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Generates the answer based on the prompt.

*   Ideally grounded in the retrieved context.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Answer + Citations&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*   Returns:

    *   The final answer.

    *   Citations (document title, section, page).

    *   Optionally: confidence score, latency, token usage.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Key properties of the online pipeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It must be fast.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Users expect answers in &amp;lt; 1 second, ideally &amp;lt; 500ms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It must be stable under load.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Handle many concurrent users.&lt;/li&gt;
&lt;li&gt;  AvoidLatency spikes.&lt;/li&gt;
&lt;li&gt;  Use caching, batching, and load balancing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;It must be observable.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Log queries, retrieval results, latency.&lt;/li&gt;
&lt;li&gt;  Track errors and failures.&lt;/li&gt;
&lt;li&gt;  Monitor metrics (latency, cost, quality).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pipeline is where you decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How queries are interpreted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How retrieval is constrained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How context is prepared for the LLM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How answers are presented and tracked.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Why You Must Design Both Pipelines
&lt;/h2&gt;

&lt;p&gt;Most tutorials only design the online pipeline:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query
  ↓
Vector Search
  ↓
LLM
  ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Documents are already chunked.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chunks are already embedded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Indices already exist.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata is perfect.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, that assumption is fatal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: A broken offline pipeline
&lt;/h2&gt;

&lt;p&gt;Company A:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uploads a PDF policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extracts raw text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Splits every 500 tokens.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Embeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stores.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Company B:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uploads the same PDF.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Detects structure: headings, sections, tables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleans headers/footers, page numbers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creates section-based chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extracts metadata: &lt;code&gt;document_type&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Embeds with domain-specific model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stores in vector + BM25 indexes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six months later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Company A's RAG returns fragmented, noisy chunks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Company B's RAG returns structured, filtered, relevant chunks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is not the LLM.&lt;br&gt;&lt;br&gt;
It's the offline pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: A broken online pipeline
&lt;/h2&gt;

&lt;p&gt;Company C:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses only vector search.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No metadata filters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No reranking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sends top-50 chunks to LLM.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Company D:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses hybrid search (vector + BM25).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Applies metadata filters (language, version, department).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reranks with cross-encoder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sends top-5 compressed chunks to LLM.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the same LLM, Company D's answers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;More precise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More grounded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More consistent.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is not the LLM.&lt;br&gt;&lt;br&gt;
It's the online pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Think About These Pipelines
&lt;/h2&gt;

&lt;p&gt;Treat them as &lt;strong&gt;two independent systems&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Offline = "data engineering" system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Online = "search engine" system.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Change chunking strategies without touching retrieval logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add new metadata fields without changing the LLM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Swap embedding models without rewriting the query pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Introduce reranking without changing ingestion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you must design &lt;strong&gt;both&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
If one is weak, the whole system fails.&lt;/p&gt;




&lt;p&gt;In the next articles, we'll go deeper into each component of a production RAG system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Building a Production RAG Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingestion pipelines (parsers, cleaners, and document processing).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chunking strategies with practical examples.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata design and how it improves retrieval quality.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Advanced Retrieval for Production RAG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Embeddings and semantic representation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hybrid search combining vector search and keyword retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reranking strategies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Context compression techniques.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Scaling RAG Systems in Production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Prompt construction patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Large-scale RAG architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance optimization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling pipelines for millions of documents.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part 5: Evaluating and Improving Production RAG Systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;RAG evaluation methods.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Production monitoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Common failure patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;15 production mistakes with real-world scenarios.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A complete production RAG checklist.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these articles cover the complete journey from a simple RAG prototype to a reliable production-grade AI system.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>Stop Starting with Microservices (Build a Monolith First)</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Thu, 16 Jul 2026 11:07:51 +0000</pubDate>
      <link>https://dev.to/damir-karimov/stop-starting-with-microservices-build-a-monolith-first-1if2</link>
      <guid>https://dev.to/damir-karimov/stop-starting-with-microservices-build-a-monolith-first-1if2</guid>
      <description>&lt;p&gt;Most startups don't fail because they chose a monolith.&lt;/p&gt;

&lt;p&gt;They fail because they never built something people actually want.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Microservices from Day One" Trap
&lt;/h2&gt;

&lt;p&gt;If you hang out on YouTube, Reddit, or X, you've probably seen this take:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Serious applications use microservices."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It sounds right.&lt;br&gt;&lt;br&gt;
It sounds modern.&lt;br&gt;&lt;br&gt;
It sounds like what Netflix, Uber, and Amazon are doing.&lt;/p&gt;

&lt;p&gt;So teams copy the architecture… before they even have 100 users.&lt;/p&gt;

&lt;p&gt;What they end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weeks (or months) of infrastructure work
&lt;/li&gt;
&lt;li&gt;Multiple repositories
&lt;/li&gt;
&lt;li&gt;Complex CI/CD pipelines
&lt;/li&gt;
&lt;li&gt;Painful debugging across services
&lt;/li&gt;
&lt;li&gt;And barely any actual product
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real mistake isn't choosing a monolith.&lt;/p&gt;

&lt;p&gt;It's solving problems you don't have yet.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Microservices Feel So Attractive
&lt;/h2&gt;

&lt;p&gt;Let's be honest — microservices are fun.&lt;/p&gt;

&lt;p&gt;You get to play with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker
&lt;/li&gt;
&lt;li&gt;Kubernetes
&lt;/li&gt;
&lt;li&gt;API Gateways
&lt;/li&gt;
&lt;li&gt;Message brokers (Kafka, RabbitMQ)
&lt;/li&gt;
&lt;li&gt;Distributed tracing
&lt;/li&gt;
&lt;li&gt;Event-driven systems
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It &lt;em&gt;feels&lt;/em&gt; like real engineering.&lt;/p&gt;

&lt;p&gt;But here's the uncomfortable truth:&lt;/p&gt;

&lt;p&gt;Complexity does not create business value.&lt;/p&gt;

&lt;p&gt;Your users don't care how your services communicate.&lt;br&gt;&lt;br&gt;
They care if your product works.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hidden Cost Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Imagine you're building a SaaS.&lt;/p&gt;

&lt;p&gt;Instead of one app, you split it into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth service
&lt;/li&gt;
&lt;li&gt;User service
&lt;/li&gt;
&lt;li&gt;Billing service
&lt;/li&gt;
&lt;li&gt;Notification service
&lt;/li&gt;
&lt;li&gt;Analytics service
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looks clean on a diagram.&lt;/p&gt;

&lt;p&gt;Now a user can't register.&lt;/p&gt;

&lt;p&gt;Where's the bug?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth service?
&lt;/li&gt;
&lt;li&gt;User service?
&lt;/li&gt;
&lt;li&gt;API Gateway?
&lt;/li&gt;
&lt;li&gt;Kafka?
&lt;/li&gt;
&lt;li&gt;Network?
&lt;/li&gt;
&lt;li&gt;Database?
&lt;/li&gt;
&lt;li&gt;Docker config?
&lt;/li&gt;
&lt;li&gt;Kubernetes?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple feature now spans multiple services and repos.&lt;/p&gt;

&lt;p&gt;Instead of building features…&lt;br&gt;&lt;br&gt;
you're chasing logs across your system.&lt;/p&gt;


&lt;h2&gt;
  
  
  What You Actually Get with a Monolith
&lt;/h2&gt;

&lt;p&gt;A monolith isn't outdated.&lt;br&gt;&lt;br&gt;
It's just one deployable application.&lt;/p&gt;

&lt;p&gt;That gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One codebase
&lt;/li&gt;
&lt;li&gt;One deployment
&lt;/li&gt;
&lt;li&gt;One database
&lt;/li&gt;
&lt;li&gt;One place to debug
&lt;/li&gt;
&lt;li&gt;One place to search
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For early-stage products, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster development
&lt;/li&gt;
&lt;li&gt;Simpler debugging
&lt;/li&gt;
&lt;li&gt;Easier onboarding
&lt;/li&gt;
&lt;li&gt;Lower infrastructure costs
&lt;/li&gt;
&lt;li&gt;Fewer production surprises
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly: you spend time building product, not infrastructure.&lt;/p&gt;


&lt;h2&gt;
  
  
  "But What About Scaling?"
&lt;/h2&gt;

&lt;p&gt;This is the default objection.&lt;/p&gt;

&lt;p&gt;Here's the reality:&lt;/p&gt;

&lt;p&gt;Most apps never reach the point where a monolith is the bottleneck.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instagram started as a monolith
&lt;/li&gt;
&lt;li&gt;GitHub ran as a monolith for years
&lt;/li&gt;
&lt;li&gt;Shopify is still largely monolithic internally
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even at scale, many companies use &lt;strong&gt;modular monoliths&lt;/strong&gt;, not pure microservices.&lt;/p&gt;

&lt;p&gt;Scaling users matters more than scaling architecture.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Real Problem: Bad Design
&lt;/h2&gt;

&lt;p&gt;People often blame monoliths for what is actually poor code structure.&lt;/p&gt;

&lt;p&gt;A bad monolith looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Massive classes
&lt;/li&gt;
&lt;li&gt;Circular dependencies
&lt;/li&gt;
&lt;li&gt;Duplicated logic
&lt;/li&gt;
&lt;li&gt;No clear boundaries
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But microservices don't fix bad design.&lt;/p&gt;

&lt;p&gt;They distribute it.&lt;/p&gt;

&lt;p&gt;Now instead of one messy codebase…&lt;br&gt;&lt;br&gt;
you have ten.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Sweet Spot: Modular Monolith
&lt;/h2&gt;

&lt;p&gt;You don't have to choose between chaos and complexity.&lt;/p&gt;

&lt;p&gt;Start with a &lt;strong&gt;modular monolith&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Structure your app by domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  auth/
  users/
  billing/
  notifications/
  analytics/
  shared/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic
&lt;/li&gt;
&lt;li&gt;Data access
&lt;/li&gt;
&lt;li&gt;Services
&lt;/li&gt;
&lt;li&gt;API layer
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modules talk through clear interfaces, not internal hacks.&lt;/p&gt;

&lt;p&gt;You keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple deployment
&lt;/li&gt;
&lt;li&gt;Clean boundaries
&lt;/li&gt;
&lt;li&gt;Flexibility for future scaling
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when a module actually needs to be extracted…&lt;br&gt;&lt;br&gt;
you already know where to cut.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Microservices Actually Make Sense
&lt;/h2&gt;

&lt;p&gt;Microservices &lt;em&gt;do&lt;/em&gt; solve real problems.&lt;/p&gt;

&lt;p&gt;But only when those problems exist.&lt;/p&gt;

&lt;p&gt;They make sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple teams need independent deployments
&lt;/li&gt;
&lt;li&gt;Different parts of the system scale differently
&lt;/li&gt;
&lt;li&gt;You need different tech stacks per service
&lt;/li&gt;
&lt;li&gt;Deployments are slowing you down
&lt;/li&gt;
&lt;li&gt;Failures must be isolated
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice something?&lt;/p&gt;

&lt;p&gt;These are not early-stage startup problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Is a Business Decision
&lt;/h2&gt;

&lt;p&gt;Architecture is not a flex.&lt;br&gt;&lt;br&gt;
It's a trade-off.&lt;/p&gt;

&lt;p&gt;Move to microservices when your &lt;em&gt;business&lt;/em&gt; needs it — not when Twitter tells you it's "industry standard."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If deploy takes 30 seconds → you don't need Kubernetes
&lt;/li&gt;
&lt;li&gt;If one DB handles your load → don't split it into 10
&lt;/li&gt;
&lt;li&gt;If your team fits in one room → you don't need 20 services
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Simple Rule
&lt;/h2&gt;

&lt;p&gt;Start simple.&lt;br&gt;&lt;br&gt;
Measure.&lt;br&gt;&lt;br&gt;
Find bottlenecks.&lt;br&gt;&lt;br&gt;
Then optimize.&lt;/p&gt;

&lt;p&gt;Not the other way around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The best architecture is not the most impressive one.&lt;/p&gt;

&lt;p&gt;It's the one that helps your team ship valuable features every week.&lt;/p&gt;

&lt;p&gt;Build the product first.&lt;br&gt;&lt;br&gt;
Earn the complexity later.&lt;/p&gt;

&lt;p&gt;Because users don't buy architectures.&lt;/p&gt;

&lt;p&gt;They buy solutions.&lt;/p&gt;

</description>
      <category>monolith</category>
      <category>microservices</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Why MCP Is Becoming the Standard Layer for AI Integrations (And Why Your Team Will Eventually Use It)</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Wed, 08 Jul 2026 10:13:58 +0000</pubDate>
      <link>https://dev.to/damir-karimov/why-mcp-is-becoming-the-standard-layer-for-ai-integrations-and-why-your-team-will-eventually-use-2pn7</link>
      <guid>https://dev.to/damir-karimov/why-mcp-is-becoming-the-standard-layer-for-ai-integrations-and-why-your-team-will-eventually-use-2pn7</guid>
      <description>&lt;p&gt;AI doesn't need another model.&lt;/p&gt;

&lt;p&gt;It needs a sane, reusable way to connect models to the real world: tools, data, APIs, and workflows.&lt;/p&gt;

&lt;p&gt;That's exactly the problem the Model Context Protocol (MCP) is solving.&lt;/p&gt;

&lt;p&gt;Right now, every AI product team is quietly rebuilding the same thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a GitHub wrapper
&lt;/li&gt;
&lt;li&gt;a Jira wrapper
&lt;/li&gt;
&lt;li&gt;a Slack client
&lt;/li&gt;
&lt;li&gt;a DB adapter
&lt;/li&gt;
&lt;li&gt;a Drive integration
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is fine.&lt;/p&gt;

&lt;p&gt;The integration layer is chaos.&lt;/p&gt;

&lt;p&gt;And that chaos is exactly why MCP is becoming the &lt;strong&gt;standard layer for AI integrations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your team is building AI products, this is not a “maybe later” problem. It's already your problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real bottleneck in AI products isn’t the model
&lt;/h2&gt;

&lt;p&gt;We love talking about reasoning, context windows, benchmarks.&lt;/p&gt;

&lt;p&gt;But in real products, the hardest part is not intelligence — it's glue code.&lt;/p&gt;

&lt;p&gt;Try building an assistant that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates a GitHub issue
&lt;/li&gt;
&lt;li&gt;checks Jira tickets
&lt;/li&gt;
&lt;li&gt;reads Slack threads
&lt;/li&gt;
&lt;li&gt;summarizes deployment logs
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge isn't the model. It's wiring four or five systems together, each with its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auth model
&lt;/li&gt;
&lt;li&gt;API shape
&lt;/li&gt;
&lt;li&gt;response format
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So teams do the same thing again and again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write custom connectors
&lt;/li&gt;
&lt;li&gt;duplicate logic
&lt;/li&gt;
&lt;li&gt;maintain brittle orchestration
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're not fighting the model.&lt;/p&gt;

&lt;p&gt;You're fighting the integration layer.&lt;/p&gt;

&lt;p&gt;MCP is not about making the model smarter.&lt;/p&gt;

&lt;p&gt;It's about making the integration path &lt;strong&gt;repeatable, reusable, and standardized&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the game-changer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What MCP actually is (without the hype)
&lt;/h2&gt;

&lt;p&gt;MCP is an open protocol that standardizes how AI hosts connect to external capabilities.&lt;/p&gt;

&lt;p&gt;Not in a vague future sense — but in a concrete architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt; — your AI app (IDE, desktop assistant, etc.)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt; — exposes capabilities
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client layer&lt;/strong&gt; — connects them via MCP
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Integrate every tool separately for every AI client"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Expose capabilities once via a standard interface"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Important nuance:&lt;/p&gt;

&lt;p&gt;MCP does &lt;strong&gt;not replace APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It standardizes how AI systems &lt;strong&gt;discover and use them&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the core reason MCP is becoming the standard integration layer for AI.&lt;/p&gt;

&lt;p&gt;Not because it's flashy.&lt;/p&gt;

&lt;p&gt;But because it removes the most expensive, repetitive work from your team's plate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The missing abstraction: tools, resources, prompts
&lt;/h2&gt;

&lt;p&gt;A lot of shallow commentary reduces MCP to "tool calling."&lt;/p&gt;

&lt;p&gt;That's incomplete.&lt;/p&gt;

&lt;p&gt;MCP is cleaner if you think in three primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; → actions (create issue, run query, deploy service)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; → data (files, logs, documents, records)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt; → reusable workflows (incident triage, debugging flows)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters.&lt;/p&gt;

&lt;p&gt;It turns "AI calling random endpoints" into a structured system.&lt;/p&gt;

&lt;p&gt;And structured systems are the only systems that scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub MCP server → tool: create issue
&lt;/li&gt;
&lt;li&gt;Docs server → resource: read documentation
&lt;/li&gt;
&lt;li&gt;Support system → prompt: incident triage workflow
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a much better mental model than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Just give the AI API access"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it's exactly the kind of structure that makes MCP a &lt;em&gt;standard layer&lt;/em&gt;, not just another integration pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the HTTP analogy is useful (and where it breaks)
&lt;/h2&gt;

&lt;p&gt;The HTTP analogy is useful — but only at a high level.&lt;/p&gt;

&lt;p&gt;HTTP made the web scalable because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clients didn't care how servers were implemented
&lt;/li&gt;
&lt;li&gt;they just spoke a shared protocol
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP aims for something similar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI hosts don't care how tools are implemented
&lt;/li&gt;
&lt;li&gt;they just use a standard interface
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But let's be precise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;HTTP standardizes communication between systems&lt;br&gt;&lt;br&gt;
MCP standardizes how AI hosts discover and use capabilities&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MCP does not replace HTTP.&lt;/p&gt;

&lt;p&gt;It complements it — by normalizing the &lt;em&gt;capability&lt;/em&gt; layer above raw APIs.&lt;/p&gt;

&lt;p&gt;That's why "MCP is the HTTP of AI" is catchy, but also imprecise.&lt;/p&gt;

&lt;p&gt;The stronger claim is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MCP is becoming the standard layer AI systems use to connect to the software around them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that claim is far more actionable for engineers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this unlocks (in practice)
&lt;/h2&gt;

&lt;p&gt;Imagine you're inside your IDE and you ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find the failed deployment, inspect logs, and create a GitHub issue."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom integrations
&lt;/li&gt;
&lt;li&gt;brittle orchestration
&lt;/li&gt;
&lt;li&gt;duplicated logic
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With MCP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The host discovers available MCP servers
&lt;/li&gt;
&lt;li&gt;Finds a logging server + GitHub server
&lt;/li&gt;
&lt;li&gt;Calls a logging tool → fetch logs
&lt;/li&gt;
&lt;li&gt;Reads structured output
&lt;/li&gt;
&lt;li&gt;Calls GitHub tool → creates issue
&lt;/li&gt;
&lt;li&gt;Returns final result
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing magical happened to the model.&lt;/p&gt;

&lt;p&gt;The system just became &lt;strong&gt;composable and reusable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the essence of a standard layer.&lt;/p&gt;

&lt;p&gt;And that's what teams will care about when they're under pressure to ship faster, with fewer bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  When MCP is actually worth it
&lt;/h2&gt;

&lt;p&gt;MCP is not a silver bullet.&lt;/p&gt;

&lt;p&gt;But it becomes incredibly powerful in exactly the scenarios where most AI teams struggle:&lt;/p&gt;

&lt;h3&gt;
  
  
  Strong fit:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;internal developer platforms
&lt;/li&gt;
&lt;li&gt;AI-powered IDEs
&lt;/li&gt;
&lt;li&gt;enterprise workflows
&lt;/li&gt;
&lt;li&gt;repeated tool access
&lt;/li&gt;
&lt;li&gt;multi-system orchestration
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Weak fit:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;simple one-off integrations
&lt;/li&gt;
&lt;li&gt;single API calls
&lt;/li&gt;
&lt;li&gt;small scripts or prototypes
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't feel integration pain yet, MCP will feel like overengineering.&lt;/p&gt;

&lt;p&gt;If you do — it starts to look like infrastructure.&lt;/p&gt;

&lt;p&gt;And once you've built even one serious AI product, you &lt;em&gt;will&lt;/em&gt; feel that pain.&lt;/p&gt;

&lt;p&gt;That's when MCP moves from "maybe" to "necessary".&lt;/p&gt;




&lt;h2&gt;
  
  
  The part no one wants to say
&lt;/h2&gt;

&lt;p&gt;MCP won't save you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bad APIs
&lt;/li&gt;
&lt;li&gt;broken auth
&lt;/li&gt;
&lt;li&gt;poor observability
&lt;/li&gt;
&lt;li&gt;flaky tools
&lt;/li&gt;
&lt;li&gt;versioning chaos
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It standardizes access.&lt;/p&gt;

&lt;p&gt;It does not guarantee quality.&lt;/p&gt;

&lt;p&gt;That's not a bug of MCP.&lt;/p&gt;

&lt;p&gt;It's just reality: protocols only remove coordination cost, not all engineering trade-offs.&lt;/p&gt;

&lt;p&gt;But even with that caveat, MCP still reduces &lt;em&gt;fragmentation&lt;/em&gt; — which is the core problem it's meant to solve.&lt;/p&gt;

&lt;p&gt;And fragmentation is the part that kills velocity in AI teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  So why MCP is becoming the standard layer
&lt;/h2&gt;

&lt;p&gt;Because this is the first serious attempt to standardize the &lt;strong&gt;integration layer of AI systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not models.&lt;br&gt;&lt;br&gt;
Not prompts.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If adoption grows, a pattern emerges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;applications expose capabilities through MCP servers
&lt;/li&gt;
&lt;li&gt;AI hosts discover those capabilities dynamically
&lt;/li&gt;
&lt;li&gt;developers stop writing one-off adapters for every client
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's exactly what standard layers do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they make patterns familiar
&lt;/li&gt;
&lt;li&gt;they reduce duplicated work
&lt;/li&gt;
&lt;li&gt;they turn bespoke glue code into reusable infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's why MCP is becoming the standard layer for AI integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final take: why you should care
&lt;/h2&gt;

&lt;p&gt;If your team is building AI products today, you're already paying the cost of fragmented integrations.&lt;/p&gt;

&lt;p&gt;You're paying it in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time spent on custom connectors
&lt;/li&gt;
&lt;li&gt;bugs in brittle orchestration
&lt;/li&gt;
&lt;li&gt;pain when you need to add a new tool
&lt;/li&gt;
&lt;li&gt;fear when you think about scaling to more systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP is not a slogan.&lt;/p&gt;

&lt;p&gt;It's a practical, concrete way to reduce that cost.&lt;/p&gt;

&lt;p&gt;Not flashy.&lt;br&gt;&lt;br&gt;
Not magical.  &lt;/p&gt;

&lt;p&gt;But if adoption keeps growing, MCP could become one of the default ways AI systems connect to everything else around them.&lt;/p&gt;

&lt;p&gt;And for anyone building real AI products, that's exactly the layer worth paying attention to — and betting on.&lt;/p&gt;

&lt;p&gt;Because the future of AI isn't just more models.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;more connected, more reusable, less fragmented integrations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And MCP is the first serious attempt to make that future real.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>llm</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Why Your Background Jobs Fail in Production</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Mon, 29 Jun 2026 10:41:23 +0000</pubDate>
      <link>https://dev.to/damir-karimov/why-your-background-jobs-fail-in-production-3nne</link>
      <guid>https://dev.to/damir-karimov/why-your-background-jobs-fail-in-production-3nne</guid>
      <description>&lt;p&gt;Most developers first meet background jobs through a deceptively simple model: put work in a queue, let a worker pick it up, and assume the job will be processed once. In production, that mental model breaks quickly because queue systems are designed for reliability under failure, not for “perfect” one-shot execution.&lt;/p&gt;

&lt;p&gt;Need to send an email? Queue it. Generate a PDF? Queue it. Process images? Queue it.&lt;/p&gt;

&lt;p&gt;That feels elegant at first. The request finishes fast, the slow work happens elsewhere, and everything looks stable.&lt;/p&gt;

&lt;p&gt;Then production happens.&lt;/p&gt;

&lt;p&gt;A worker crashes after sending the email.&lt;br&gt;&lt;br&gt;
A network hiccup prevents the ACK from reaching the broker.&lt;br&gt;&lt;br&gt;
A third-party API times out halfway through processing.&lt;br&gt;&lt;br&gt;
The same job gets retried and runs again.&lt;/p&gt;

&lt;p&gt;Now you have duplicate emails, duplicate charges, or jobs that appear to vanish and then come back later. That is not an edge case. That is normal distributed-systems behavior.&lt;/p&gt;


&lt;h2&gt;
  
  
  The happy path is a lie
&lt;/h2&gt;

&lt;p&gt;The classic flow looks clean:&lt;/p&gt;

&lt;p&gt;User registration&lt;br&gt;&lt;br&gt;
→ Create user&lt;br&gt;&lt;br&gt;
→ Push job to queue&lt;br&gt;&lt;br&gt;
→ Worker processes job&lt;br&gt;&lt;br&gt;
→ Send email&lt;/p&gt;

&lt;p&gt;That flow is useful for explaining the idea, but it hides the real failure modes. In practice, each step can fail independently: enqueue can succeed while downstream storage is unavailable, a worker can complete side effects and then crash before acknowledging the message, or a retry can happen because the system cannot tell whether the previous attempt fully finished.&lt;/p&gt;

&lt;p&gt;A queue does not guarantee execution exactly once.&lt;br&gt;&lt;br&gt;
It coordinates work, and it does so in a failure-prone environment.&lt;/p&gt;

&lt;p&gt;That distinction matters because it changes how you design the job handler, the data model, and the retry strategy.&lt;/p&gt;


&lt;h2&gt;
  
  
  At-least-once delivery
&lt;/h2&gt;

&lt;p&gt;Most production queue systems follow &lt;strong&gt;at-least-once delivery&lt;/strong&gt;. Amazon SQS explicitly documents that a message can be received again, and it recommends designing applications to be idempotent so repeated processing does not cause harm.&lt;/p&gt;

&lt;p&gt;That means a job may run more than once.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Worker receives job&lt;br&gt;&lt;br&gt;
→ Sends email&lt;br&gt;&lt;br&gt;
→ Crashes before ACK&lt;br&gt;&lt;br&gt;
→ Queue retries job&lt;br&gt;&lt;br&gt;
→ Email is sent again&lt;/p&gt;

&lt;p&gt;From the queue’s point of view, the job was never confirmed as complete. Retrying is the correct behavior. The problem is that the side effect already happened.&lt;/p&gt;

&lt;p&gt;This is why “the queue handled it” is not the same as “the system handled it.”&lt;/p&gt;


&lt;h2&gt;
  
  
  Idempotency is required
&lt;/h2&gt;

&lt;p&gt;Once duplicate delivery is possible, idempotency becomes mandatory. The handler should produce the same final result even if it runs multiple times with the same input.&lt;/p&gt;

&lt;p&gt;Bad example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this runs twice, the user gets credited twice.&lt;/p&gt;

&lt;p&gt;Better:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPaymentJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transactionId&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;userId&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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyProcessed&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="nx"&gt;paymentEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactionId&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;alreadyProcessed&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="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;$transaction&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;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentEvents&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&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="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&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="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;The important part is not just the &lt;code&gt;findUnique&lt;/code&gt; check. You also need a database constraint on &lt;code&gt;transactionId&lt;/code&gt;, plus a transactional write boundary, so two workers cannot race and apply the same effect twice. That combination is much closer to production reality than a simple in-memory guard.&lt;/p&gt;

&lt;p&gt;A good rule: store a stable business key for every side effect, then make that key unique.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exactly-once is a trap
&lt;/h2&gt;

&lt;p&gt;Engineers often ask whether exactly-once processing is possible. In practice, the answer is usually “not in the way people imagine.”&lt;/p&gt;

&lt;p&gt;A job may charge a customer successfully, then fail while persisting the result. Or the database write may succeed while the worker crashes before confirming the message. Either way, the system ends up with inconsistent state unless it is explicitly designed for idempotency and reconciliation.&lt;/p&gt;

&lt;p&gt;What people call “exactly-once” in production is usually a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deduplication,&lt;/li&gt;
&lt;li&gt;idempotent handlers,&lt;/li&gt;
&lt;li&gt;atomic state transitions,&lt;/li&gt;
&lt;li&gt;checkpointing,&lt;/li&gt;
&lt;li&gt;and careful recovery logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful mental model is not “never run twice.”&lt;br&gt;&lt;br&gt;
It is “running twice must not break anything.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Retries can make outages worse
&lt;/h2&gt;

&lt;p&gt;Retries are necessary, but uncontrolled retries can amplify a problem.&lt;/p&gt;

&lt;p&gt;Imagine 50,000 jobs depending on a third-party API. If that API slows down or goes offline and every worker retries immediately, you create a retry storm. Instead of letting the system recover, you add more pressure to the failing dependency.&lt;/p&gt;

&lt;p&gt;A better pattern is exponential backoff with jitter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry #1 after a short delay&lt;/li&gt;
&lt;li&gt;Retry #2 after a longer delay&lt;/li&gt;
&lt;li&gt;Retry #3 after an even longer delay&lt;/li&gt;
&lt;li&gt;Add random jitter so retries do not synchronize
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRetryDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&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;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jitter&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;Without jitter, thousands of jobs can wake up at the same time and hit the same dependency together. With jitter, the pressure spreads out, which is exactly what you want during recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dead letter queues matter early
&lt;/h2&gt;

&lt;p&gt;Some jobs are never going to succeed.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid payloads,&lt;/li&gt;
&lt;li&gt;missing records,&lt;/li&gt;
&lt;li&gt;deleted resources,&lt;/li&gt;
&lt;li&gt;permanent validation errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a job fails for a non-transient reason, retrying it forever wastes capacity and makes the queue harder to reason about. That is what a dead letter queue is for: a place to send messages that have exceeded their retry budget or failed in a non-recoverable way.&lt;/p&gt;

&lt;p&gt;A healthy production setup usually treats DLQ as a first-class path, not an afterthought. It gives you a clear place to inspect poison messages, track recurring failures, and keep the main queue clean.&lt;/p&gt;

&lt;p&gt;If you do one thing after adding retries, make sure you also define where failed jobs go next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Long jobs should be split up
&lt;/h2&gt;

&lt;p&gt;A common mistake is to make one job do everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate a report,&lt;/li&gt;
&lt;li&gt;download assets,&lt;/li&gt;
&lt;li&gt;resize images,&lt;/li&gt;
&lt;li&gt;upload files,&lt;/li&gt;
&lt;li&gt;update the database,&lt;/li&gt;
&lt;li&gt;send a notification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That looks convenient until step five fails and you have to decide whether to rerun the whole thing. The larger the job, the harder it is to retry safely and the more expensive each failure becomes.&lt;/p&gt;

&lt;p&gt;A better shape is a chain of smaller units:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate report,&lt;/li&gt;
&lt;li&gt;process assets,&lt;/li&gt;
&lt;li&gt;upload results,&lt;/li&gt;
&lt;li&gt;notify user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smaller jobs are easier to retry, easier to observe, and easier to scale independently. They also reduce blast radius when something breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring matters more than processing
&lt;/h2&gt;

&lt;p&gt;Many teams spend a lot of time building queue workers and very little time watching them. That is a mistake.&lt;/p&gt;

&lt;p&gt;At minimum, track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queue length,&lt;/li&gt;
&lt;li&gt;queue lag,&lt;/li&gt;
&lt;li&gt;processing latency,&lt;/li&gt;
&lt;li&gt;job duration,&lt;/li&gt;
&lt;li&gt;failure rate,&lt;/li&gt;
&lt;li&gt;retry volume,&lt;/li&gt;
&lt;li&gt;DLQ growth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These signals tell you whether workers are keeping up, whether a dependency is slowing the pipeline, and whether failures are starting to accumulate. Azure’s background-job guidance also emphasizes reliability and scaling as first-class concerns, not optional extras.&lt;/p&gt;

&lt;p&gt;If you cannot see your queue health, you are debugging blind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backpressure is the silent killer
&lt;/h2&gt;

&lt;p&gt;Backpressure is what happens when work arrives faster than you can process it.&lt;/p&gt;

&lt;p&gt;If your system receives 100 jobs per second but can only complete 50, the backlog grows continuously. Eventually that leads to rising latency, memory pressure, and a degraded user experience.&lt;/p&gt;

&lt;p&gt;Common ways to deal with it include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rate limiting,&lt;/li&gt;
&lt;li&gt;worker autoscaling,&lt;/li&gt;
&lt;li&gt;priority queues,&lt;/li&gt;
&lt;li&gt;load shedding,&lt;/li&gt;
&lt;li&gt;rejecting or delaying non-critical work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring backpressure does not remove it. It just makes the failure slower and more expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  What production systems actually look like
&lt;/h2&gt;

&lt;p&gt;What tutorials often show:&lt;/p&gt;

&lt;p&gt;Queue → Worker → Done&lt;/p&gt;

&lt;p&gt;What production systems usually need:&lt;/p&gt;

&lt;p&gt;App&lt;br&gt;&lt;br&gt;
→ Queue&lt;br&gt;&lt;br&gt;
→ Workers&lt;br&gt;&lt;br&gt;
→ Retry policy&lt;br&gt;&lt;br&gt;
→ Dead letter queue&lt;br&gt;&lt;br&gt;
→ Metrics&lt;br&gt;&lt;br&gt;
→ Alerting&lt;br&gt;&lt;br&gt;
→ Recovery process&lt;/p&gt;

&lt;p&gt;The processing code is rarely the hardest part. The hard part is designing for failure, visibility, and recovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a queue-based system, verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The job is idempotent.&lt;/li&gt;
&lt;li&gt;The same job can run multiple times safely.&lt;/li&gt;
&lt;li&gt;Retry delays use exponential backoff.&lt;/li&gt;
&lt;li&gt;Jitter is enabled.&lt;/li&gt;
&lt;li&gt;A dead letter queue exists.&lt;/li&gt;
&lt;li&gt;Metrics and alerts are in place.&lt;/li&gt;
&lt;li&gt;Failure scenarios were tested intentionally.&lt;/li&gt;
&lt;li&gt;Workers can scale horizontally.&lt;/li&gt;
&lt;li&gt;Backpressure is handled explicitly.&lt;/li&gt;
&lt;li&gt;Jobs are small enough to retry without fear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If several of these are missing, the system is probably more fragile than it looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Background jobs fail in production not because queues are bad, but because the happy-path mental model is incomplete.&lt;/p&gt;

&lt;p&gt;Real systems deal with crashes, retries, duplicate delivery, slow dependencies, and poisoned messages. The goal is not to pretend those failures do not exist. The goal is to make them survivable.&lt;/p&gt;

&lt;p&gt;Once you design for failure instead of assuming success, background jobs become much more trustworthy.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>queue</category>
    </item>
    <item>
      <title>Designing a Real-Time Chat System at Scale</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Tue, 23 Jun 2026 09:02:11 +0000</pubDate>
      <link>https://dev.to/damir-karimov/designing-a-real-time-chat-system-at-scale-53k7</link>
      <guid>https://dev.to/damir-karimov/designing-a-real-time-chat-system-at-scale-53k7</guid>
      <description>&lt;p&gt;Most developers underestimate how hard messaging systems really are.&lt;/p&gt;

&lt;p&gt;A basic chat demo with WebSockets is easy to build. A production-grade messaging platform like WhatsApp, Telegram, Discord, or Slack is a completely different engineering problem. The hard part is not rendering messages in the UI. The hard parts are keeping millions of persistent connections alive, delivering messages reliably, preserving ordering, handling offline sync, scaling group chats, surviving partial failures, and keeping latency low around the world.&lt;/p&gt;

&lt;p&gt;In this post, we'll design a scalable real-time chat architecture and walk through the trade-offs behind modern messaging systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why chat gets hard
&lt;/h2&gt;

&lt;p&gt;At first glance, chat seems simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send a message&lt;/li&gt;
&lt;li&gt;store it&lt;/li&gt;
&lt;li&gt;show it to the other user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works for a demo. It falls apart fast in production.&lt;/p&gt;

&lt;p&gt;Once you add real users, the system has to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persistent connections&lt;/li&gt;
&lt;li&gt;reconnects&lt;/li&gt;
&lt;li&gt;message ordering&lt;/li&gt;
&lt;li&gt;offline delivery&lt;/li&gt;
&lt;li&gt;read receipts&lt;/li&gt;
&lt;li&gt;typing indicators&lt;/li&gt;
&lt;li&gt;multi-device sync&lt;/li&gt;
&lt;li&gt;large group chats&lt;/li&gt;
&lt;li&gt;global latency&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where chat stops being a UI feature and becomes a distributed systems problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Before designing the system, we need clear requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Functional requirements
&lt;/h3&gt;

&lt;p&gt;Our chat system should support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-to-one chats&lt;/li&gt;
&lt;li&gt;Group chats&lt;/li&gt;
&lt;li&gt;Real-time message delivery&lt;/li&gt;
&lt;li&gt;Read receipts&lt;/li&gt;
&lt;li&gt;Typing indicators&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;li&gt;Media attachments&lt;/li&gt;
&lt;li&gt;Message history&lt;/li&gt;
&lt;li&gt;Multi-device synchronization&lt;/li&gt;
&lt;li&gt;Online and offline presence&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-functional requirements
&lt;/h3&gt;

&lt;p&gt;The system must also provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low latency&lt;/li&gt;
&lt;li&gt;High availability&lt;/li&gt;
&lt;li&gt;Horizontal scalability&lt;/li&gt;
&lt;li&gt;Fault tolerance&lt;/li&gt;
&lt;li&gt;Reliable delivery&lt;/li&gt;
&lt;li&gt;Event ordering&lt;/li&gt;
&lt;li&gt;Efficient storage&lt;/li&gt;
&lt;li&gt;Global distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At small scale, these are manageable. At millions of users, every one of them becomes a distributed systems concern.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;

&lt;p&gt;A modern chat platform is usually event-driven and distributed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clients
   │
   ▼
Load Balancer
   │
   ▼
WebSocket Gateway Cluster
   │
   ├── Authentication Service
   ├── Presence Service
   ├── Chat Service
   ├── Notification Service
   └── Media Service
          │
          ▼
    Kafka / Redis Streams
          │
          ▼
     Storage Layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a separate responsibility. That separation is what makes the system scalable and easier to evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why polling fails
&lt;/h2&gt;

&lt;p&gt;Many beginner chat apps start with polling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /messages every 2 seconds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for a demo. It fails badly at scale.&lt;/p&gt;

&lt;p&gt;Polling creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;massive request overhead&lt;/li&gt;
&lt;li&gt;unnecessary database reads&lt;/li&gt;
&lt;li&gt;increased latency&lt;/li&gt;
&lt;li&gt;battery drain on mobile devices&lt;/li&gt;
&lt;li&gt;poor real-time responsiveness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine 2 million connected users polling every 2 seconds. That becomes 1 million requests per second even when nobody is sending anything. Real chat systems avoid this by using persistent connections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why WebSockets became the default
&lt;/h2&gt;

&lt;p&gt;WebSockets keep a bidirectional connection open between the client and the server.&lt;/p&gt;

&lt;p&gt;That gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;near real-time communication&lt;/li&gt;
&lt;li&gt;lower overhead&lt;/li&gt;
&lt;li&gt;reduced latency&lt;/li&gt;
&lt;li&gt;efficient server push&lt;/li&gt;
&lt;li&gt;better mobile performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client connects once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client ───── persistent connection ───── Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, both sides can exchange events instantly. That is the foundation of modern messaging systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden WebSocket cost
&lt;/h2&gt;

&lt;p&gt;WebSockets solve one problem and introduce another.&lt;/p&gt;

&lt;p&gt;A persistent connection is not free. Every connected user consumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a TCP socket&lt;/li&gt;
&lt;li&gt;memory buffers&lt;/li&gt;
&lt;li&gt;heartbeat state&lt;/li&gt;
&lt;li&gt;authentication context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 5 million concurrent users, this is no longer a normal API problem. It becomes a connection management problem.&lt;/p&gt;

&lt;p&gt;That is why serious chat systems build specialized gateway infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebSocket gateway layer
&lt;/h2&gt;

&lt;p&gt;The gateway layer is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maintaining persistent connections&lt;/li&gt;
&lt;li&gt;authenticating users&lt;/li&gt;
&lt;li&gt;routing events&lt;/li&gt;
&lt;li&gt;managing heartbeats&lt;/li&gt;
&lt;li&gt;detecting disconnects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gateways should stay as stateless as possible. Stateless gateways are much easier to scale, replace, and recover after failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication flow
&lt;/h3&gt;

&lt;p&gt;A common flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User logs in via HTTP.&lt;/li&gt;
&lt;li&gt;Backend issues a JWT token.&lt;/li&gt;
&lt;li&gt;Client opens a WebSocket connection.&lt;/li&gt;
&lt;li&gt;Token is validated during the handshake.&lt;/li&gt;
&lt;li&gt;Connection is associated with a user session.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wss://chat.example.com?token=JWT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After authentication, the gateway knows which user owns the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sending a message
&lt;/h2&gt;

&lt;p&gt;Now let's look at the real delivery pipeline.&lt;/p&gt;

&lt;p&gt;Suppose User A sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client sends a message event.&lt;/li&gt;
&lt;li&gt;Gateway validates authentication.&lt;/li&gt;
&lt;li&gt;Chat service validates permissions.&lt;/li&gt;
&lt;li&gt;Message is persisted in durable storage.&lt;/li&gt;
&lt;li&gt;Event is published into Kafka.&lt;/li&gt;
&lt;li&gt;Recipient gateway receives the event.&lt;/li&gt;
&lt;li&gt;Message is pushed to the recipient.&lt;/li&gt;
&lt;li&gt;Delivery ACK is generated.&lt;/li&gt;
&lt;li&gt;Read receipt is generated later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This looks simple on paper. In reality, every step can fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why persistence comes first
&lt;/h2&gt;

&lt;p&gt;A lot of beginners try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deliver → save later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is dangerous.&lt;/p&gt;

&lt;p&gt;If the server crashes before persistence, the recipient saw the message, but the database lost it. Now the system is inconsistent.&lt;/p&gt;

&lt;p&gt;Production systems usually do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;persist → publish → deliver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Durability comes first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Message IDs and ordering
&lt;/h2&gt;

&lt;p&gt;Distributed systems do not guarantee ordering automatically.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message A&lt;/li&gt;
&lt;li&gt;Message B&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recipient receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Message B&lt;/li&gt;
&lt;li&gt;Message A&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why does this happen? Because messages may travel through different gateway servers, queues, and network paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common ordering strategies
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Timestamp ordering
&lt;/h4&gt;

&lt;p&gt;Simple, but unreliable. Clock drift breaks consistency.&lt;/p&gt;

&lt;h4&gt;
  
  
  Incremental sequence IDs
&lt;/h4&gt;

&lt;p&gt;More reliable.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;conversation_id: 42

messages:
1
2
3
4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees ordering inside a conversation. Most real systems only guarantee local ordering per chat. Global ordering across the entire platform is usually impossible at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database design
&lt;/h2&gt;

&lt;p&gt;Messaging systems are write-heavy. A popular group chat can generate thousands of writes per second.&lt;/p&gt;

&lt;p&gt;Typical tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conversations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conversation_members&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;messages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message_status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attachments&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real challenge is partitioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a single database eventually fails
&lt;/h3&gt;

&lt;p&gt;A single PostgreSQL instance works at the beginning. Over time, problems show up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write bottlenecks&lt;/li&gt;
&lt;li&gt;storage growth&lt;/li&gt;
&lt;li&gt;replication lag&lt;/li&gt;
&lt;li&gt;index size explosion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At scale, systems introduce sharding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharding strategy
&lt;/h3&gt;

&lt;p&gt;A common strategy is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shard by conversation_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;messages for the same chat stay colocated&lt;/li&gt;
&lt;li&gt;ordering becomes easier&lt;/li&gt;
&lt;li&gt;queries stay efficient&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad shard keys create hotspots. For example, sharding by &lt;code&gt;user_id&lt;/code&gt; can spread large group chats across multiple shards and make fan-out expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kafka and event streaming
&lt;/h2&gt;

&lt;p&gt;Modern messaging systems are heavily event-driven.&lt;/p&gt;

&lt;p&gt;Kafka is useful because it provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;durable event logs&lt;/li&gt;
&lt;li&gt;replayability&lt;/li&gt;
&lt;li&gt;partitioned scalability&lt;/li&gt;
&lt;li&gt;consumer groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of services calling each other directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chat Service → Kafka → Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delivery service&lt;/li&gt;
&lt;li&gt;notification service&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;moderation&lt;/li&gt;
&lt;li&gt;push notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This decouples the system and makes failures easier to isolate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Presence system
&lt;/h2&gt;

&lt;p&gt;Presence is deceptively expensive.&lt;/p&gt;

&lt;p&gt;Tracking online, offline, typing, and &lt;code&gt;last_seen&lt;/code&gt; for millions of users creates a huge amount of event traffic. That is why most systems isolate presence into a dedicated service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Presence implementation
&lt;/h3&gt;

&lt;p&gt;A common architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gateway → Redis → Presence Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gateways periodically send heartbeats.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PING every 30 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If heartbeats stop, the user is marked offline.&lt;/p&gt;

&lt;p&gt;Redis works well here because presence data is ephemeral. Not everything belongs in a relational database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing indicators
&lt;/h2&gt;

&lt;p&gt;Typing indicators look trivial. They are not.&lt;/p&gt;

&lt;p&gt;Problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high event frequency&lt;/li&gt;
&lt;li&gt;noisy updates&lt;/li&gt;
&lt;li&gt;unnecessary fan-out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most systems heavily throttle typing events.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User typing → emit once every 3 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Without throttling, typing indicators can overload the infrastructure faster than messages.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Fan-out in group chats
&lt;/h2&gt;

&lt;p&gt;Suppose a group contains 500,000 users.&lt;/p&gt;

&lt;p&gt;One message may require 500,000 deliveries. This is called fan-out.&lt;/p&gt;

&lt;p&gt;Large fan-out is one of the hardest messaging problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fan-out strategies
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Fan-out on write
&lt;/h4&gt;

&lt;p&gt;The server precomputes deliveries immediately.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast reads&lt;/li&gt;
&lt;li&gt;expensive writes&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Fan-out on read
&lt;/h4&gt;

&lt;p&gt;Messages are stored once.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cheaper writes&lt;/li&gt;
&lt;li&gt;more expensive reads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different platforms choose different trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Offline synchronization
&lt;/h2&gt;

&lt;p&gt;Users disconnect constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mobile app closed&lt;/li&gt;
&lt;li&gt;network loss&lt;/li&gt;
&lt;li&gt;airplane mode&lt;/li&gt;
&lt;li&gt;battery saver&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system must synchronize missed events efficiently.&lt;/p&gt;

&lt;p&gt;Typical approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch all events after last_sequence_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;last_seen_message = 10451
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10452+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incremental synchronization is critical. Full synchronization is too expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Push notifications
&lt;/h2&gt;

&lt;p&gt;Offline users still need notifications.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message event
   ↓
notification service
   ↓
APNs / FCM
   ↓
mobile device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push systems are eventually consistent. Notifications may arrive late, duplicated, or out of order. Clients need to tolerate that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-device sync
&lt;/h2&gt;

&lt;p&gt;Modern users expect sync across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;phone&lt;/li&gt;
&lt;li&gt;desktop&lt;/li&gt;
&lt;li&gt;browser&lt;/li&gt;
&lt;li&gt;tablet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each device may keep its own session. The backend tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;device_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connection_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;last_sync_state&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Events are usually delivered independently per device.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reliability guarantees
&lt;/h2&gt;

&lt;p&gt;Messaging systems need clear delivery semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  At-most-once
&lt;/h3&gt;

&lt;p&gt;Fastest. Messages may disappear.&lt;/p&gt;

&lt;h3&gt;
  
  
  At-least-once
&lt;/h3&gt;

&lt;p&gt;Reliable. Duplicates are possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exactly-once
&lt;/h3&gt;

&lt;p&gt;Extremely expensive and difficult in distributed systems.&lt;/p&gt;

&lt;p&gt;Most production systems use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;at-least-once + idempotency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the practical choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why exactly-once is mostly marketing
&lt;/h2&gt;

&lt;p&gt;Exactly-once delivery across distributed infrastructure is incredibly hard.&lt;/p&gt;

&lt;p&gt;Network failures create ambiguity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the recipient receive the message?&lt;/li&gt;
&lt;li&gt;Did the ACK get lost?&lt;/li&gt;
&lt;li&gt;Did the retry create a duplicate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the sender cannot know for sure.&lt;/p&gt;

&lt;p&gt;That is why many systems rely on retries, deduplication, and idempotent consumers instead of true exactly-once guarantees.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common failure scenarios
&lt;/h2&gt;

&lt;p&gt;Real systems fail all the time.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gateway crashes&lt;/li&gt;
&lt;li&gt;Kafka partition unavailable&lt;/li&gt;
&lt;li&gt;Redis outage&lt;/li&gt;
&lt;li&gt;slow consumers&lt;/li&gt;
&lt;li&gt;duplicate events&lt;/li&gt;
&lt;li&gt;partial synchronization&lt;/li&gt;
&lt;li&gt;network splits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliable systems assume failure is normal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling strategies
&lt;/h2&gt;

&lt;p&gt;As traffic grows, teams usually add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;horizontal scaling for gateway nodes&lt;/li&gt;
&lt;li&gt;partitioned Kafka topics&lt;/li&gt;
&lt;li&gt;regional infrastructure&lt;/li&gt;
&lt;li&gt;CDN for media&lt;/li&gt;
&lt;li&gt;caching to reduce database pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest bottleneck is usually not CPU.&lt;/p&gt;

&lt;p&gt;At scale, bottlenecks are often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network throughput&lt;/li&gt;
&lt;li&gt;memory usage&lt;/li&gt;
&lt;li&gt;hot partitions&lt;/li&gt;
&lt;li&gt;connection limits&lt;/li&gt;
&lt;li&gt;disk I/O&lt;/li&gt;
&lt;li&gt;replication lag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chat systems are infrastructure-heavy workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes messaging hard
&lt;/h2&gt;

&lt;p&gt;The frontend UI may look simple. Underneath is a distributed system balancing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;li&gt;availability&lt;/li&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;reliability&lt;/li&gt;
&lt;li&gt;scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every architectural decision introduces trade-offs. You can optimize latency, durability, throughput, or cost, but rarely all at once.&lt;/p&gt;

&lt;p&gt;That is why messaging systems remain one of the most interesting areas in system design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical takeaway
&lt;/h2&gt;

&lt;p&gt;If you are building chat, do not think only in terms of sockets and message lists. Think in terms of delivery guarantees, storage strategy, ordering, recovery, and fan-out.&lt;/p&gt;

&lt;p&gt;A chat product becomes serious very quickly. The moment you need offline sync, presence, multi-device support, and reliable delivery, you are no longer building a UI feature. You are building a distributed messaging platform.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>websockets</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>CI/CD for Modern Applications: From Manual Deployments to Reliable Delivery Pipelines</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Mon, 15 Jun 2026 10:20:05 +0000</pubDate>
      <link>https://dev.to/damir-karimov/cicd-for-modern-applications-from-manual-deployments-to-reliable-delivery-pipelines-52j5</link>
      <guid>https://dev.to/damir-karimov/cicd-for-modern-applications-from-manual-deployments-to-reliable-delivery-pipelines-52j5</guid>
      <description>&lt;p&gt;Last year my team had a production incident because someone manually deployed without running tests. The fix took 47 minutes. That's when I realized: CI/CD isn't about automation — it's about controlling risk in software delivery.&lt;/p&gt;

&lt;p&gt;In this article, you'll learn how to design reliable pipelines for frontend (React/Next.js) and backend systems, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Artifact strategies and reproducible builds&lt;/li&gt;
&lt;li&gt;Testing pyramid (unit, integration, contract, e2e)&lt;/li&gt;
&lt;li&gt;Safe database migrations&lt;/li&gt;
&lt;li&gt;Production observability and DORA metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus: a production checklist you can use immediately.&lt;/p&gt;




&lt;p&gt;Modern application development is constrained less by coding speed and more by delivery reliability. Teams can ship features quickly, but production stability breaks when deployments remain manual or partially automated. CI/CD solves this by turning software delivery into a deterministic, repeatable pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI/CD Actually Solves
&lt;/h2&gt;

&lt;p&gt;CI/CD (Continuous Integration and Continuous Delivery/Deployment) is not a toolset — it is a system design approach for software delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core problems it addresses
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Integration conflicts in branches&lt;/td&gt;
&lt;td&gt;Merge hell, delayed releases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unpredictable release cycles&lt;/td&gt;
&lt;td&gt;Business uncertainty, missed SLAs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual deployment errors&lt;/td&gt;
&lt;td&gt;Production incidents, data corruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment drift&lt;/td&gt;
&lt;td&gt;"Works in CI, fails in prod"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow rollback processes&lt;/td&gt;
&lt;td&gt;Extended MTTR, higher user impact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Not "faster deploys", but &lt;strong&gt;"reliable, repeatable deploys"&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI: Continuous Integration as a Validation Layer
&lt;/h2&gt;

&lt;p&gt;Continuous Integration ensures every change is validated in a controlled pipeline &lt;strong&gt;before merging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A minimal CI pipeline includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linting (code quality enforcement)&lt;/li&gt;
&lt;li&gt;Unit tests&lt;/li&gt;
&lt;li&gt;Type checking (TypeScript, etc.)&lt;/li&gt;
&lt;li&gt;Build verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feature branch → PR → CI pipeline → checks pass → merge to main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key principle:&lt;/strong&gt; Every commit must result in a &lt;strong&gt;deployable artifact&lt;/strong&gt; after CI. Without this rule, CI becomes cosmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  CD: Continuous Delivery vs Continuous Deployment
&lt;/h2&gt;

&lt;p&gt;These terms are often mixed but differ structurally:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Continuous Delivery&lt;/th&gt;
&lt;th&gt;Continuous Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build state&lt;/td&gt;
&lt;td&gt;Always deployable&lt;/td&gt;
&lt;td&gt;Always deployable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment trigger&lt;/td&gt;
&lt;td&gt;Manual (human approval)&lt;/td&gt;
&lt;td&gt;Automatic (after CI success)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release decision&lt;/td&gt;
&lt;td&gt;Controlled (gates, approvals)&lt;/td&gt;
&lt;td&gt;Fully automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Regulated/large systems&lt;/td&gt;
&lt;td&gt;Fast-moving products with strong test coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;CD controls to include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Approval gates&lt;/li&gt;
&lt;li&gt;Feature flags&lt;/li&gt;
&lt;li&gt;RBAC &amp;amp; audit trails&lt;/li&gt;
&lt;li&gt;Environment-specific configuration (not baked into artifacts)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Typical CI/CD Architecture
&lt;/h2&gt;

&lt;p&gt;A production-grade pipeline includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code → CI → Build artifact → Store → Deploy → Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Components
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source control&lt;/td&gt;
&lt;td&gt;GitHub, GitLab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI runner&lt;/td&gt;
&lt;td&gt;GitHub Actions, GitLab CI, Jenkins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build system&lt;/td&gt;
&lt;td&gt;Docker, native builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artifact storage&lt;/td&gt;
&lt;td&gt;Docker registry, S3, artifact repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment system&lt;/td&gt;
&lt;td&gt;Kubernetes, Vercel, ECS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Logs, metrics (Prometheus), traces (Jaeger)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Pipeline Stages in Modern Apps
&lt;/h2&gt;

&lt;p&gt;A realistic pipeline is &lt;strong&gt;layered validation&lt;/strong&gt;, not linear "test → build → deploy".&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pre-merge checks (on PR)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;ESLint / Prettier&lt;/li&gt;
&lt;li&gt;TypeScript compilation&lt;/li&gt;
&lt;li&gt;Unit tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; &amp;lt; 10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Post-merge CI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Full test suite&lt;/li&gt;
&lt;li&gt;Integration tests&lt;/li&gt;
&lt;li&gt;Security scanning (SAST)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; &amp;lt; 30 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Build stage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker image build&lt;/li&gt;
&lt;li&gt;Dependency locking&lt;/li&gt;
&lt;li&gt;Artifact versioning (semver + git SHA)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Deployment stage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Staging rollout&lt;/li&gt;
&lt;li&gt;Smoke tests&lt;/li&gt;
&lt;li&gt;Production deployment (manual or automatic)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Post-deployment validation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Health checks&lt;/li&gt;
&lt;li&gt;Observability signals (logs, metrics, traces)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pipeline Internals: Artifacts, Provenance, Reproducibility
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Artifact Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Store &lt;strong&gt;immutable artifacts&lt;/strong&gt;: Docker images, frontend build bundles, npm tarballs&lt;/li&gt;
&lt;li&gt;Tag by &lt;strong&gt;semver + git SHA&lt;/strong&gt;, store digest&lt;/li&gt;
&lt;li&gt;Deploy by &lt;strong&gt;digest&lt;/strong&gt; (not tag) to ensure consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reproducible Builds
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use Docker images with pinned base versions&lt;/li&gt;
&lt;li&gt;Lock dependency versions (&lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Deterministic builds (no random timestamps)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Caching &amp;amp; Parallelization
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cache &lt;code&gt;node_modules&lt;/code&gt; / &lt;code&gt;pnpm store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Parallelize test suites&lt;/li&gt;
&lt;li&gt;Incremental builds (Next.js, TypeScript)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Testing Strategy: The Pyramid
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Type&lt;/th&gt;
&lt;th&gt;When to Run&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;Pre-merge (PR)&lt;/td&gt;
&lt;td&gt;Fast feedback on logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Post-merge&lt;/td&gt;
&lt;td&gt;API, DB, service interactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract tests&lt;/td&gt;
&lt;td&gt;Post-merge&lt;/td&gt;
&lt;td&gt;Cross-service compatibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E2e tests&lt;/td&gt;
&lt;td&gt;Staging/Nightly&lt;/td&gt;
&lt;td&gt;Full user flow validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Nightly&lt;/td&gt;
&lt;td&gt;Latency, throughput baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flaky tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure flakiness rate (target &amp;lt; 1–2%)&lt;/li&gt;
&lt;li&gt;Quarantine flaky tests (separate suite)&lt;/li&gt;
&lt;li&gt;Retry vs fix: fix critical flakies, quarantine low-priority&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  CI/CD in Frontend-heavy Systems (React / Next.js)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Typical Pipeline
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install dependencies (cached)&lt;/li&gt;
&lt;li&gt;Lint + TypeScript check&lt;/li&gt;
&lt;li&gt;Unit tests (Jest / Vitest)&lt;/li&gt;
&lt;li&gt;Build (Next.js build)&lt;/li&gt;
&lt;li&gt;Static analysis (bundle size, unused deps)&lt;/li&gt;
&lt;li&gt;Deploy (Vercel / Docker / CDN)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Key Optimizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cache &lt;code&gt;node_modules&lt;/code&gt; / &lt;code&gt;pnpm store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Incremental builds&lt;/li&gt;
&lt;li&gt;Split preview deployments per PR&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  CI/CD for Scalable Backend Systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend Pipeline Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Database migration strategy&lt;/li&gt;
&lt;li&gt;Backward compatibility checks&lt;/li&gt;
&lt;li&gt;Blue-green or canary deployments&lt;/li&gt;
&lt;li&gt;Queue compatibility (Kafka / RabbitMQ)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deployment Models
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rolling&lt;/td&gt;
&lt;td&gt;Simple services&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue-green&lt;/td&gt;
&lt;td&gt;Critical services, fast rollback&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canary&lt;/td&gt;
&lt;td&gt;Gradual traffic shift, metrics&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Safe Database Migrations in CI/CD
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Migration Checklist
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add column &lt;strong&gt;nullable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deploy service with &lt;strong&gt;backward-compatible&lt;/strong&gt; code (read new column with fallback)&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;backfill&lt;/strong&gt; job&lt;/li&gt;
&lt;li&gt;Make column &lt;strong&gt;non-nullable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deploy service using new column &lt;strong&gt;without fallback&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Clean up old code/columns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Orchestration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migration step in pipeline&lt;/li&gt;
&lt;li&gt;Feature flag to enable new behavior&lt;/li&gt;
&lt;li&gt;Health gate: error rate &amp;lt; 0.5%, latency P95 stable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Security in CI/CD
&lt;/h2&gt;

&lt;p&gt;Modern pipelines must include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency scanning (&lt;code&gt;npm audit&lt;/code&gt;, Snyk)&lt;/li&gt;
&lt;li&gt;Secret detection&lt;/li&gt;
&lt;li&gt;Container vulnerability scanning&lt;/li&gt;
&lt;li&gt;Signed artifacts (supply chain security)&lt;/li&gt;
&lt;li&gt;SBOM (Software Bill of Materials)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CI/CD without security gates becomes an attack surface.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability as Final Stage of CI/CD
&lt;/h2&gt;

&lt;p&gt;Delivery is incomplete without feedback loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logs (structured logging)&lt;/li&gt;
&lt;li&gt;Metrics (latency, error rate)&lt;/li&gt;
&lt;li&gt;Traces (request flow)&lt;/li&gt;
&lt;li&gt;Alerting (SLA violations)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Pipeline &amp;amp; Production Metrics (DORA)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lead time for changes&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment frequency&lt;/td&gt;
&lt;td&gt;Daily or on-demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change failure rate&lt;/td&gt;
&lt;td&gt;&amp;lt; 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTTR (Mean Time to Restore)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flaky-test rate&lt;/td&gt;
&lt;td&gt;&amp;lt; 1–2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;CI/CD ends where production behavior becomes visible.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Failure Points in CI/CD Systems
&lt;/h2&gt;

&lt;p&gt;Most failures are due to &lt;strong&gt;design flaws&lt;/strong&gt;, not tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure Point&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No clear artifact strategy&lt;/td&gt;
&lt;td&gt;Inconsistent production builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing rollback strategy&lt;/td&gt;
&lt;td&gt;Increased risk, extended MTTR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overloaded pipelines&lt;/td&gt;
&lt;td&gt;Slow feedback, reduced developer productivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lack of environment parity&lt;/td&gt;
&lt;td&gt;"Works in CI, fails in prod"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rollback should be:&lt;/strong&gt; Version switch (traffic to previous artifact digest) — &lt;strong&gt;NOT&lt;/strong&gt; manual hotfix process&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;p&gt;Before promoting to production:&lt;/p&gt;

&lt;p&gt;✅ Artifact immutable and signed&lt;br&gt;&lt;br&gt;
✅ DB migration safe (nullable → backfill → non-nullable)&lt;br&gt;&lt;br&gt;
✅ Feature flag ready for gradual enable&lt;br&gt;&lt;br&gt;
✅ Health gates configured (error rate, latency)&lt;br&gt;&lt;br&gt;
✅ Monitoring dashboards updated (deploy events visible)&lt;br&gt;&lt;br&gt;
✅ Rollback plan tested (traffic switch &amp;lt; 5 min)&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CI/CD is not automation of deployment. It is a &lt;strong&gt;system for controlling risk in software delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A well-designed pipeline ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable releases&lt;/li&gt;
&lt;li&gt;Consistent environments&lt;/li&gt;
&lt;li&gt;Fast feedback loops&lt;/li&gt;
&lt;li&gt;Safe iteration speed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams that implement CI/CD correctly shift from &lt;strong&gt;"deploying code"&lt;/strong&gt; to &lt;strong&gt;"managing delivery systems"&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>webdev</category>
      <category>kubernetes</category>
      <category>node</category>
    </item>
    <item>
      <title>Real-Time Notification Systems Are Harder Than Most Teams Expect</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Tue, 09 Jun 2026 10:26:13 +0000</pubDate>
      <link>https://dev.to/damir-karimov/real-time-notification-systems-are-harder-than-most-teams-expect-od1</link>
      <guid>https://dev.to/damir-karimov/real-time-notification-systems-are-harder-than-most-teams-expect-od1</guid>
      <description>&lt;p&gt;If you’ve ever thought, “It’s just a WebSocket event,” this article is for you.&lt;/p&gt;

&lt;p&gt;Notification systems look simple on the surface, but in production they fail in annoying, expensive, and user-visible ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate notifications&lt;/li&gt;
&lt;li&gt;missing events&lt;/li&gt;
&lt;li&gt;race conditions&lt;/li&gt;
&lt;li&gt;delayed delivery&lt;/li&gt;
&lt;li&gt;mobile disconnects&lt;/li&gt;
&lt;li&gt;retry storms&lt;/li&gt;
&lt;li&gt;ordering bugs&lt;/li&gt;
&lt;li&gt;state drift across regions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part is not sending a message.&lt;/p&gt;

&lt;p&gt;The tricky part is making sure the right user gets the right notification, in the right order, with enough reliability that the system can survive crashes, retries, and mobile networks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with “just send a WebSocket event”
&lt;/h2&gt;

&lt;p&gt;A basic notification flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend → WebSocket Server → Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works in local dev. It even works for a while in production.&lt;/p&gt;

&lt;p&gt;Then real traffic arrives, and the system suddenly has to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconnects&lt;/li&gt;
&lt;li&gt;offline users&lt;/li&gt;
&lt;li&gt;multiple devices&lt;/li&gt;
&lt;li&gt;persistence&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;fan-out&lt;/li&gt;
&lt;li&gt;backpressure&lt;/li&gt;
&lt;li&gt;push fallbacks&lt;/li&gt;
&lt;li&gt;deduplication&lt;/li&gt;
&lt;li&gt;ordering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, your “WebSocket feature” has become a distributed messaging system.&lt;/p&gt;

&lt;p&gt;And that is a very different problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delivery semantics matter first
&lt;/h2&gt;

&lt;p&gt;Before you design the system, decide what guarantees you need.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Semantics&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Messages may be lost, but won’t be duplicated&lt;/td&gt;
&lt;td&gt;Low-priority updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Messages won’t be lost, but may be duplicated&lt;/td&gt;
&lt;td&gt;Payments, security alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effectively-once&lt;/td&gt;
&lt;td&gt;Duplicates are removed with dedupe logic&lt;/td&gt;
&lt;td&gt;Critical product events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most teams make a mistake here.&lt;/p&gt;

&lt;p&gt;They start building transport first, then discover later that they actually needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;durable cursors&lt;/li&gt;
&lt;li&gt;sequence numbers&lt;/li&gt;
&lt;li&gt;replay support&lt;/li&gt;
&lt;li&gt;acknowledgements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why notification systems become expensive: the real problem is not delivery, it is &lt;strong&gt;delivery semantics&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why fan-out breaks systems
&lt;/h2&gt;

&lt;p&gt;One event is easy.&lt;/p&gt;

&lt;p&gt;One event to 10,000 users is not.&lt;/p&gt;

&lt;p&gt;A single action can trigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feed updates&lt;/li&gt;
&lt;li&gt;badge counter updates&lt;/li&gt;
&lt;li&gt;push notifications&lt;/li&gt;
&lt;li&gt;email digests&lt;/li&gt;
&lt;li&gt;analytics events&lt;/li&gt;
&lt;li&gt;moderation triggers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queue amplification&lt;/li&gt;
&lt;li&gt;retry cascades&lt;/li&gt;
&lt;li&gt;hot partitions&lt;/li&gt;
&lt;li&gt;uneven load&lt;/li&gt;
&lt;li&gt;latency spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the system stops being “send a notification” and becomes “shape traffic safely under failure.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Why duplicates happen
&lt;/h2&gt;

&lt;p&gt;Duplicates usually do not come from one single bug. They appear from the interaction of retries, crashes, and missing idempotency.&lt;/p&gt;

&lt;p&gt;A common chain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Message is written to Kafka
2. Consumer processes it
3. Consumer crashes before committing offset
4. Partition is reassigned
5. Another consumer reads the same message
6. User gets the notification twice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not random.&lt;/p&gt;

&lt;p&gt;That is at-least-once delivery with missing deduplication.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;idempotency keys&lt;/li&gt;
&lt;li&gt;dedupe storage&lt;/li&gt;
&lt;li&gt;sequence numbers&lt;/li&gt;
&lt;li&gt;consumer-side protection before side effects&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Ordering is harder than throughput
&lt;/h2&gt;

&lt;p&gt;Most users don’t complain about 200ms of delay.&lt;/p&gt;

&lt;p&gt;They absolutely notice this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Payment refunded” arrives before “Payment received”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That destroys trust immediately.&lt;/p&gt;

&lt;p&gt;Global ordering is usually too expensive. In practice, teams often choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-user ordering&lt;/li&gt;
&lt;li&gt;per-conversation ordering&lt;/li&gt;
&lt;li&gt;approximate ordering&lt;/li&gt;
&lt;li&gt;causal consistency where needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most products, per-user ordering is the best balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: sequence numbers per user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sequence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notifications&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a simple way to keep ordering stable inside a user shard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mobile makes everything worse
&lt;/h2&gt;

&lt;p&gt;Desktop clients are relatively stable.&lt;/p&gt;

&lt;p&gt;Mobile clients are not.&lt;/p&gt;

&lt;p&gt;You have to deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;app backgrounding&lt;/li&gt;
&lt;li&gt;battery optimization&lt;/li&gt;
&lt;li&gt;network switching&lt;/li&gt;
&lt;li&gt;silent disconnects&lt;/li&gt;
&lt;li&gt;delayed push delivery&lt;/li&gt;
&lt;li&gt;OS throttling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why real systems often combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WebSockets for active sessions&lt;/li&gt;
&lt;li&gt;APNs for iOS&lt;/li&gt;
&lt;li&gt;FCM for Android&lt;/li&gt;
&lt;li&gt;polling or pull fallback&lt;/li&gt;
&lt;li&gt;local persistence&lt;/li&gt;
&lt;li&gt;sync checkpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Important detail
&lt;/h3&gt;

&lt;p&gt;APNs and FCM are not guaranteed single-delivery transport.&lt;/p&gt;

&lt;p&gt;They can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delay notifications&lt;/li&gt;
&lt;li&gt;drop messages under pressure&lt;/li&gt;
&lt;li&gt;coalesce updates&lt;/li&gt;
&lt;li&gt;expire tokens&lt;/li&gt;
&lt;li&gt;throttle traffic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if the notification matters, the server still needs durable state.&lt;/p&gt;




&lt;h2&gt;
  
  
  A real incident example
&lt;/h2&gt;

&lt;p&gt;At 3AM, an on-call engineer gets paged because one user received dozens of duplicate payment emails.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;the consumer crashed mid-batch&lt;/li&gt;
&lt;li&gt;the offset was not committed&lt;/li&gt;
&lt;li&gt;Kafka redelivered the same event&lt;/li&gt;
&lt;li&gt;the email sender had no dedupe check&lt;/li&gt;
&lt;li&gt;the user got spammed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That kind of issue is painful because it is not one bug.&lt;/p&gt;

&lt;p&gt;It is a chain of small design decisions that only becomes visible under failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  The practical fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;email_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dedupe:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the code style.&lt;/p&gt;

&lt;p&gt;It is the fact that the system now assumes duplicates can happen and is built to survive them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability is not optional
&lt;/h2&gt;

&lt;p&gt;If you cannot observe the pipeline, you cannot debug it.&lt;/p&gt;

&lt;p&gt;Useful signals include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;queue lag&lt;/li&gt;
&lt;li&gt;retry count&lt;/li&gt;
&lt;li&gt;delivery success rate&lt;/li&gt;
&lt;li&gt;connection churn&lt;/li&gt;
&lt;li&gt;consumer health&lt;/li&gt;
&lt;li&gt;fan-out latency&lt;/li&gt;
&lt;li&gt;push provider errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did we send the event?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we prove the user received it?&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  Metrics worth tracking
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Good target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery success rate&lt;/td&gt;
&lt;td&gt;&amp;gt;99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 delivery latency&lt;/td&gt;
&lt;td&gt;&amp;lt;500ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer lag&lt;/td&gt;
&lt;td&gt;low and stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry rate&lt;/td&gt;
&lt;td&gt;close to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection churn&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What to do in production
&lt;/h2&gt;

&lt;p&gt;A good notification system needs a runbook, not just code.&lt;/p&gt;

&lt;p&gt;If retries spike:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Throttle producers.&lt;/li&gt;
&lt;li&gt;Pause non-critical workers.&lt;/li&gt;
&lt;li&gt;Increase retry backoff.&lt;/li&gt;
&lt;li&gt;Check consumer lag.&lt;/li&gt;
&lt;li&gt;Check push provider errors.&lt;/li&gt;
&lt;li&gt;Rehydrate missed clients from durable state.&lt;/li&gt;
&lt;li&gt;Replay safely with idempotency keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is what makes the system operationally survivable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scaling path
&lt;/h2&gt;

&lt;p&gt;A lot of teams go through the same evolution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Startup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API → WebSocket Server → Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mid-scale
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API → Kafka → Notification Workers → WebSocket Gateway Cluster → Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  High-scale
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API → Multi-region event bus → regional workers → regional gateways → clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At higher scale, the hardest problems are usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;state distribution&lt;/li&gt;
&lt;li&gt;per-user ordering&lt;/li&gt;
&lt;li&gt;region routing&lt;/li&gt;
&lt;li&gt;dedupe&lt;/li&gt;
&lt;li&gt;offline recovery&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;State.&lt;/p&gt;




&lt;h2&gt;
  
  
  What strong teams optimize for
&lt;/h2&gt;

&lt;p&gt;Early teams optimize for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speed of delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strong teams optimize for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;correctness&lt;/li&gt;
&lt;li&gt;recoverability&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;graceful degradation&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That difference matters a lot in production.&lt;/p&gt;

&lt;p&gt;A notification that is slightly late is usually acceptable.&lt;/p&gt;

&lt;p&gt;A notification that is duplicated, lost, or out of order is not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the system
&lt;/h2&gt;

&lt;p&gt;You should test the failure modes, not just the happy path.&lt;/p&gt;

&lt;p&gt;Try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dropping WebSocket connections mid-message&lt;/li&gt;
&lt;li&gt;killing consumers during processing&lt;/li&gt;
&lt;li&gt;simulating mobile sleep/wake cycles&lt;/li&gt;
&lt;li&gt;forcing Kafka rebalances&lt;/li&gt;
&lt;li&gt;replaying duplicate events&lt;/li&gt;
&lt;li&gt;load testing fan-out spikes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the system only works when nothing fails, it is not ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Real-time notification systems look simple until scale, retries, mobile behavior, ordering, and distributed state show up.&lt;/p&gt;

&lt;p&gt;Then they become one of the hardest backend problems in the product.&lt;/p&gt;

&lt;p&gt;The goal is not just to send events.&lt;/p&gt;

&lt;p&gt;The goal is to make sure the right user gets the right notification, with the right semantics, even when the system is under stress.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>websockets</category>
      <category>kafka</category>
      <category>backend</category>
    </item>
    <item>
      <title>AI Wrappers Are Dying: Why Most AI Products Fail</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Wed, 27 May 2026 12:01:10 +0000</pubDate>
      <link>https://dev.to/damir-karimov/ai-wrappers-are-dying-why-most-ai-products-fail-ano</link>
      <guid>https://dev.to/damir-karimov/ai-wrappers-are-dying-why-most-ai-products-fail-ano</guid>
      <description>&lt;p&gt;In 2026, building an app on top of OpenAI or Anthropic is easier than ever. But wrappers are dying.&lt;/p&gt;

&lt;p&gt;A polished UI and a few RAG pipelines can get you to launch. They will not get you lasting advantage.&lt;/p&gt;

&lt;p&gt;OpenAI API is not a competitive moat.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrappers Are Dying
&lt;/h2&gt;

&lt;p&gt;The first wave of AI startups was inevitable. Foundation models became powerful enough that developers could ship useful products without training models from scratch. The barrier to entry dropped dramatically.&lt;/p&gt;

&lt;p&gt;The market filled up with wrappers.&lt;/p&gt;

&lt;p&gt;That was not irrational. It was the fastest way to test demand and prove people would pay for AI-enabled outcomes. For many founders, a wrapper was the right starting point. It reduced time-to-market and let them focus on distribution.&lt;/p&gt;

&lt;p&gt;But wrappers that worked for speed do not work for defensibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Wrappers Are Fragile
&lt;/h2&gt;

&lt;p&gt;A wrapper around an LLM is a thin interface over someone else's intelligence.&lt;/p&gt;

&lt;p&gt;When the underlying model improves, your product advantage shrinks. When a competitor copies your UX, your edge disappears. When the model provider ships your core feature natively, your differentiation collapses overnight.&lt;/p&gt;

&lt;p&gt;The closer your product is to a generic interface over a foundation model, the easier it is to clone.&lt;/p&gt;

&lt;p&gt;Three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The UI is visible and easy to imitate.&lt;/li&gt;
&lt;li&gt;The prompts and workflows are often not deeply proprietary.&lt;/li&gt;
&lt;li&gt;The core model capability is rented, not owned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Many AI products compete on packaging rather than infrastructure.&lt;/p&gt;

&lt;p&gt;If your product can be described as "ChatGPT, but for X," you have product-market fit risk before you have a moat.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Creates Real Moat
&lt;/h2&gt;

&lt;p&gt;A real moat in AI is not "we use GPT." It is owning something the next startup cannot easily replicate.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proprietary data&lt;/li&gt;
&lt;li&gt;Embedded workflows&lt;/li&gt;
&lt;li&gt;Deep enterprise integration&lt;/li&gt;
&lt;li&gt;Distribution advantages&lt;/li&gt;
&lt;li&gt;Domain-specific expertise&lt;/li&gt;
&lt;li&gt;Feedback loops that improve the product over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model access is replaceable. Workflow capture is sticky.&lt;/p&gt;

&lt;p&gt;If your product becomes part of how a team actually works, not just a tool they try once, you build defensibility. If you own the system of record, the approval flow, the compliance layer, or the operational pipeline, you are selling infrastructure, not AI.&lt;/p&gt;

&lt;p&gt;The more your product learns from user behavior, customer data, and domain-specific outcomes, the harder it becomes to copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moat Patterns That Survive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Proprietary Data Moat
&lt;/h3&gt;

&lt;p&gt;If your product collects high-signal, domain-specific data that competitors cannot access, you improve faster over time.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;labeled support cases&lt;/li&gt;
&lt;li&gt;medical annotations&lt;/li&gt;
&lt;li&gt;legal review outcomes&lt;/li&gt;
&lt;li&gt;sales conversation feedback&lt;/li&gt;
&lt;li&gt;codebase-specific assistant traces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moat works only if the data turns into better predictions, better retrieval, or better workflow decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow Moat
&lt;/h3&gt;

&lt;p&gt;If your product becomes the place where work starts, gets reviewed, and gets approved, switching becomes painful.&lt;/p&gt;

&lt;p&gt;Workflow moats require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;native integrations&lt;/li&gt;
&lt;li&gt;permissions and access control&lt;/li&gt;
&lt;li&gt;human-in-the-loop steps&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;reliable outputs that fit existing processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enterprise AI products win by becoming infrastructure, not assistants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distribution Moat
&lt;/h3&gt;

&lt;p&gt;If your product is embedded in Slack, email, CRM, IDEs, or internal tooling, it becomes harder to displace. Adoption is already inside the user's daily flow.&lt;/p&gt;

&lt;p&gt;The best model in the world loses if users never reach it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust and Compliance Moat
&lt;/h3&gt;

&lt;p&gt;In regulated environments, trust is product value.&lt;/p&gt;

&lt;p&gt;If you can prove data handling, retention rules, access controls, auditability, and predictable behavior, you compete on more than output quality. For enterprise buyers, this is the difference between a demo and a contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost and Infrastructure Moat
&lt;/h3&gt;

&lt;p&gt;Some AI products create advantage by reducing inference cost, latency, or operational overhead at scale.&lt;/p&gt;

&lt;p&gt;This moat is weaker than proprietary data or workflow lock-in. It matters when usage volume is high. If you deliver similar quality at lower cost, your margin improves and pricing flexibility increases.&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG Alone Is Not Enough
&lt;/h2&gt;

&lt;p&gt;RAG is useful. It is not a moat.&lt;/p&gt;

&lt;p&gt;Retrieval connects foundation models to private corpora, internal docs, and customer-specific context. But if every competitor can index similar documents and call the same model, the architecture is not defensible.&lt;/p&gt;

&lt;p&gt;RAG becomes valuable when paired with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;proprietary corpora&lt;/li&gt;
&lt;li&gt;strong ranking and retrieval quality&lt;/li&gt;
&lt;li&gt;feedback loops&lt;/li&gt;
&lt;li&gt;domain-specific evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moat is not the retrieval layer. It is retrieval, data quality, and embedded usage over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Platform Dependency Is a Liability
&lt;/h2&gt;

&lt;p&gt;The biggest hidden risk in AI startups is platform dependency.&lt;/p&gt;

&lt;p&gt;If your roadmap depends on a single provider, you inherit their pricing, latency, policy changes, rate limits, and feature roadmap. That is not a moat. That is a liability.&lt;/p&gt;

&lt;p&gt;When OpenAI improves a capability, it helps the whole market, including your competitors. When OpenAI ships a built-in feature that overlaps with your product, your differentiation evaporates overnight.&lt;/p&gt;

&lt;p&gt;Relying entirely on external model APIs is dangerous for long-term architecture. The more your product is a front-end to a general model, the more exposed you are to commoditization.&lt;/p&gt;

&lt;p&gt;Ask this: if model prices change, if output quality improves, or if the model vendor ships your core feature natively, what still makes you valuable?&lt;/p&gt;




&lt;h2&gt;
  
  
  Enterprise Workflows Are Where Winners Live
&lt;/h2&gt;

&lt;p&gt;The strongest AI products solve a workflow that already exists inside a company. They do more than "answer questions."&lt;/p&gt;

&lt;p&gt;Enterprise buyers care about more than output quality. They care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access control&lt;/li&gt;
&lt;li&gt;Compliance&lt;/li&gt;
&lt;li&gt;Auditability&lt;/li&gt;
&lt;li&gt;Data retention&lt;/li&gt;
&lt;li&gt;Integrations with existing systems&lt;/li&gt;
&lt;li&gt;Human approval steps&lt;/li&gt;
&lt;li&gt;Reliability at scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workflow-based products have stronger moats than generic assistants. They do not just generate text. They become part of operational machinery.&lt;/p&gt;

&lt;p&gt;Once AI is embedded in billing, support, procurement, legal review, or internal knowledge systems, switching costs rise quickly.&lt;/p&gt;

&lt;p&gt;The best products feel "boring" from the outside. They are not flashy consumer apps. They are operational systems that save time, reduce risk, or increase throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vertical AI Wins
&lt;/h2&gt;

&lt;p&gt;Vertical AI is stronger than horizontal AI because it combines domain data, workflow design, and distribution.&lt;/p&gt;

&lt;p&gt;A vertical product knows the problem deeply. It understands terminology, edge cases, compliance rules, and customer expectations in a specific domain. This makes it harder to replace with a generic chatbot.&lt;/p&gt;

&lt;p&gt;Proprietary data becomes especially important here. The more your product learns from a narrow, high-value domain, the more its quality ties to data that others do not have.&lt;/p&gt;

&lt;p&gt;Winners connect three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;domain-specific data&lt;/li&gt;
&lt;li&gt;operational workflow&lt;/li&gt;
&lt;li&gt;recurring business value&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A good vertical AI product is deeply fitted to a single job. That fit becomes harder to copy with every interaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which AI Companies Survive
&lt;/h2&gt;

&lt;p&gt;AI companies that survive are not the ones with the flashiest demos. They turn model capability into durable product advantage.&lt;/p&gt;

&lt;p&gt;They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;own proprietary or hard-to-access data&lt;/li&gt;
&lt;li&gt;sit inside critical workflows&lt;/li&gt;
&lt;li&gt;integrate deeply into enterprise systems&lt;/li&gt;
&lt;li&gt;build operational infrastructure, not interfaces&lt;/li&gt;
&lt;li&gt;create switching costs through usage, trust, and process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model may be replaceable. The product around it should not be.&lt;/p&gt;

&lt;p&gt;This is the difference between a temporary AI app and a lasting business.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Measure Moat
&lt;/h2&gt;

&lt;p&gt;Signals that the moat is getting stronger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retention stays high even when model quality changes&lt;/li&gt;
&lt;li&gt;Customers rely on the product as part of a repeatable workflow&lt;/li&gt;
&lt;li&gt;The cost to replicate your dataset is high&lt;/li&gt;
&lt;li&gt;More value comes from your proprietary layer than from the base model&lt;/li&gt;
&lt;li&gt;Integrations increase switching costs over time&lt;/li&gt;
&lt;li&gt;Unit economics improve as usage and feedback grow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test: if a competitor copied your UI tomorrow, would they still need the same data, trust, integrations, and operational context to match your product?&lt;/p&gt;

&lt;p&gt;If yes, you are building a real moat.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The problem with most AI products is not that they use AI. They confuse access to AI with defensibility.&lt;/p&gt;

&lt;p&gt;A great interface gets attention. It rarely creates a moat. Real technical moats come from data, workflow, infrastructure, and integration — things hard to copy and harder to unwind.&lt;/p&gt;

&lt;p&gt;The right question is not "How can we add a model?" The right question is: What do we own that becomes more valuable over time?&lt;/p&gt;

&lt;p&gt;The best AI companies are not the ones with the loudest demo. They are the ones whose product gets more embedded, more trusted, and more expensive to replace every quarter.&lt;/p&gt;

&lt;p&gt;Wrappers are dying. Build a moat instead.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>startup</category>
      <category>openai</category>
    </item>
    <item>
      <title>Why Good Abstractions Make Debugging Harder</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Thu, 21 May 2026 15:03:00 +0000</pubDate>
      <link>https://dev.to/damir-karimov/why-good-abstractions-make-debugging-harder-lo</link>
      <guid>https://dev.to/damir-karimov/why-good-abstractions-make-debugging-harder-lo</guid>
      <description>&lt;p&gt;Good abstractions are great when you are building software.&lt;/p&gt;

&lt;p&gt;They are much less great when you are debugging production.&lt;/p&gt;

&lt;p&gt;The reason is simple: abstraction hides details, and debugging often depends on the details you hoped to ignore.&lt;/p&gt;

&lt;p&gt;In small codebases, this is barely noticeable. In real systems, especially with caches, async flows, optimistic UI, and multiple state owners, it becomes a serious problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core issue
&lt;/h2&gt;

&lt;p&gt;The more layers you add, the easier it is for the system to become “locally correct” and “globally wrong”.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the frontend thinks the payment succeeded,&lt;/li&gt;
&lt;li&gt;the backend committed the transaction,&lt;/li&gt;
&lt;li&gt;the event was published,&lt;/li&gt;
&lt;li&gt;the cache still serves the old value,&lt;/li&gt;
&lt;li&gt;the UI shows stale data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every layer is doing something reasonable.&lt;/p&gt;

&lt;p&gt;The problem is that they are not all talking about the same version of reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple example
&lt;/h2&gt;

&lt;p&gt;Imagine this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User clicks &lt;strong&gt;Retry payment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Frontend updates UI optimistically&lt;/li&gt;
&lt;li&gt;API returns &lt;code&gt;200 OK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database is updated&lt;/li&gt;
&lt;li&gt;Event is sent to downstream systems&lt;/li&gt;
&lt;li&gt;Redis still serves old state&lt;/li&gt;
&lt;li&gt;UI refreshes from cache and shows stale data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the kind of bug that wastes hours.&lt;/p&gt;

&lt;p&gt;Not because any single line of code is hard, but because the truth is spread across several places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example in code
&lt;/h2&gt;

&lt;p&gt;Let’s say the frontend uses optimistic updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onRetryPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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;setPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAID&lt;/span&gt;&lt;span class="dl"&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/payments/retry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Retry failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPaymentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="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;At first glance, this looks fine.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;the API succeeds,&lt;/li&gt;
&lt;li&gt;the DB is updated,&lt;/li&gt;
&lt;li&gt;an event is emitted,&lt;/li&gt;
&lt;li&gt;a consumer deduplicates the event incorrectly,&lt;/li&gt;
&lt;li&gt;Redis still contains the old value,&lt;/li&gt;
&lt;li&gt;the UI re-renders from stale cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bug is no longer in this function.&lt;/p&gt;

&lt;p&gt;The bug is in the &lt;strong&gt;propagation path&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why abstractions make this worse
&lt;/h2&gt;

&lt;p&gt;Abstractions hide the exact mechanics that matter during incidents.&lt;/p&gt;

&lt;p&gt;They hide things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who owns the state,&lt;/li&gt;
&lt;li&gt;when the state changes,&lt;/li&gt;
&lt;li&gt;whether the update is synchronous or async,&lt;/li&gt;
&lt;li&gt;whether caches are invalidated,&lt;/li&gt;
&lt;li&gt;whether retries are safe,&lt;/li&gt;
&lt;li&gt;whether events can arrive out of order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is useful in normal development.&lt;/p&gt;

&lt;p&gt;It is terrible during debugging.&lt;/p&gt;

&lt;p&gt;Because when something is wrong, you do not need another clean interface. You need visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typical failure patterns
&lt;/h2&gt;

&lt;p&gt;These are the patterns I see most often in real systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Stale read
&lt;/h3&gt;

&lt;p&gt;The data was updated, but one layer still serves an old version.&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;// DB updated successfully&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="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAID&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="c1"&gt;// Cache not invalidated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DB = &lt;code&gt;PAID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;cache = &lt;code&gt;PENDING&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;UI = &lt;code&gt;PENDING&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Lost update
&lt;/h3&gt;

&lt;p&gt;Two writes happen close together, and one silently overwrites the other.&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;await&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&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="s2"&gt;Alex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&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="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the system uses last-write-wins without proper locking or versioning, the final state may not match user intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ghost update
&lt;/h3&gt;

&lt;p&gt;One layer changes, but another never receives the update.&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="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;updateOrderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// but query cache is never invalidated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a UI that looks stuck even though the backend is correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Event reorder bug
&lt;/h3&gt;

&lt;p&gt;Events arrive in a different order than they were produced.&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;// Event B processed before Event A&lt;/span&gt;
&lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the final state may be wrong even if both handlers are valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The debugging trap
&lt;/h2&gt;

&lt;p&gt;The trap is assuming this is a code bug.&lt;/p&gt;

&lt;p&gt;Very often it is not.&lt;/p&gt;

&lt;p&gt;It is a &lt;strong&gt;state ownership bug&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the real question is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Which function crashed?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real question is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Which layer is the source of truth right now?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer that clearly, debugging becomes guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better way to think about it
&lt;/h2&gt;

&lt;p&gt;Instead of thinking in terms of “where is the bug?”, think in terms of “where does state live?”&lt;/p&gt;

&lt;p&gt;A useful checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where is the canonical value stored?&lt;/li&gt;
&lt;li&gt;Which layer may cache it?&lt;/li&gt;
&lt;li&gt;Which layer may derive it?&lt;/li&gt;
&lt;li&gt;Which layer may overwrite it?&lt;/li&gt;
&lt;li&gt;Which layer may delay it?&lt;/li&gt;
&lt;li&gt;Which layer may retry it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the same value exists in five places, you now have five opportunities for disagreement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging strategy
&lt;/h2&gt;

&lt;p&gt;When a bug crosses abstraction boundaries, I usually inspect it in this order:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Check the source of truth
&lt;/h3&gt;

&lt;p&gt;Confirm where the canonical data lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Rebuild the timeline
&lt;/h3&gt;

&lt;p&gt;Trace the state from user action to backend write to cache update to UI read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Check invalidation
&lt;/h3&gt;

&lt;p&gt;If a cache exists, verify it is updated or cleared at the right moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Check idempotency
&lt;/h3&gt;

&lt;p&gt;If retries or events are involved, verify the operation can safely happen more than once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Check ordering
&lt;/h3&gt;

&lt;p&gt;If events are async, verify the system does not depend on strict ordering unless it actually guarantees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When abstractions do help
&lt;/h2&gt;

&lt;p&gt;This is not an anti-abstraction argument.&lt;/p&gt;

&lt;p&gt;Good abstractions are still valuable when they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce search space,&lt;/li&gt;
&lt;li&gt;make ownership clear,&lt;/li&gt;
&lt;li&gt;keep state local,&lt;/li&gt;
&lt;li&gt;expose transitions explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a small component with local state is easier to debug than three caches and two event consumers trying to keep the same value in sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Count: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&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;This is easy to reason about because there is one owner of the state.&lt;/p&gt;

&lt;p&gt;That is the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do in real systems
&lt;/h2&gt;

&lt;p&gt;If you want abstractions to stay helpful in production, make them observable.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add logs at boundaries,&lt;/li&gt;
&lt;li&gt;use trace IDs,&lt;/li&gt;
&lt;li&gt;keep ownership explicit,&lt;/li&gt;
&lt;li&gt;invalidate caches intentionally,&lt;/li&gt;
&lt;li&gt;design retries to be safe,&lt;/li&gt;
&lt;li&gt;avoid hidden duplicated state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good abstraction should reduce complexity, not hide the mechanics that make incidents debuggable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The best abstractions are honest.&lt;/p&gt;

&lt;p&gt;They do not pretend the system is simpler than it is. They make the system easier to understand &lt;strong&gt;without hiding where truth lives&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is why debugging gets harder as systems grow: not because abstraction is bad, but because abstraction is often too successful at hiding the exact thing you need under pressure.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>frontend</category>
      <category>software</category>
    </item>
    <item>
      <title>AI-generated code doesn't fail loudly. It fails correctly-looking.</title>
      <dc:creator>Damir Karimov</dc:creator>
      <pubDate>Wed, 13 May 2026 12:39:58 +0000</pubDate>
      <link>https://dev.to/damir-karimov/ai-generated-code-doesnt-fail-loudly-it-fails-correctly-looking-1acc</link>
      <guid>https://dev.to/damir-karimov/ai-generated-code-doesnt-fail-loudly-it-fails-correctly-looking-1acc</guid>
      <description>&lt;p&gt;AI-generated code rarely breaks in obvious ways. It passes review, ships&lt;br&gt;
to production, and behaves correctly in controlled scenarios. The&lt;br&gt;
problem is what happens after: failures appear only under timing, load,&lt;br&gt;
retries, or inconsistent state transitions.&lt;/p&gt;

&lt;p&gt;The core issue is not obvious bugs. It is code that looks structurally&lt;br&gt;
correct while silently ignoring real-world failure modes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why AI code feels correct
&lt;/h2&gt;

&lt;p&gt;AI tends to generate implementations with strong surface-level signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  consistent TypeScript types&lt;/li&gt;
&lt;li&gt;  standard architectural patterns&lt;/li&gt;
&lt;li&gt;  clean async/await flows&lt;/li&gt;
&lt;li&gt;  readable naming conventions&lt;/li&gt;
&lt;li&gt;  familiar framework usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This produces a strong cognitive bias during review. The code does not&lt;br&gt;
look "risky", so it is assumed to be correct.&lt;/p&gt;

&lt;p&gt;The gap appears because readability is not equivalent to correctness&lt;br&gt;
under production conditions.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where AI-generated code typically fails
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Concurrency and race conditions
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This assumes a single linear execution.&lt;/p&gt;

&lt;p&gt;In real systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  multiple requests can run in parallel&lt;/li&gt;
&lt;li&gt;  responses can resolve out of order&lt;/li&gt;
&lt;li&gt;  later responses can overwrite newer state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: stale state overwrite without errors or crashes.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Optimistic updates without consistency guarantees
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTodos&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTodo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTodo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This assumes success.&lt;/p&gt;

&lt;p&gt;Failure scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  request fails but UI is not rolled back&lt;/li&gt;
&lt;li&gt;  retry creates duplicate entries&lt;/li&gt;
&lt;li&gt;  frontend state diverges from backend state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system remains "visually correct" while data integrity is broken.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Stale closures and lifecycle assumptions
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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;This pattern locks in initial state.&lt;/p&gt;

&lt;p&gt;In production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  values become stale over time&lt;/li&gt;
&lt;li&gt;  UI desynchronization occurs&lt;/li&gt;
&lt;li&gt;  behavior depends on render timing rather than logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No runtime error occurs, so the issue is often missed.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Weak caching and invalidation logic
&lt;/h3&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;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`user-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&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;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  stable data shape&lt;/li&gt;
&lt;li&gt;  stable identity rules&lt;/li&gt;
&lt;li&gt;  single write path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In real systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  partial updates invalidate assumptions&lt;/li&gt;
&lt;li&gt;  multiple services mutate the same entity&lt;/li&gt;
&lt;li&gt;  cache becomes silently stale rather than obviously wrong&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  5. Hidden assumptions about APIs
&lt;/h3&gt;

&lt;p&gt;AI can introduce plausible but non-existent APIs:&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;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateAllQueries&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These patterns often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  look consistent with ecosystem conventions&lt;/li&gt;
&lt;li&gt;  pass code review without deep verification&lt;/li&gt;
&lt;li&gt;  fail only at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shifts errors from compile-time to production-time.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Accumulated lifecycle leaks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Individually correct, but when repeated across systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  inconsistent cleanup patterns accumulate&lt;/li&gt;
&lt;li&gt;  aborted requests still resolve in edge cases&lt;/li&gt;
&lt;li&gt;  memory usage grows gradually&lt;/li&gt;
&lt;li&gt;  behavior becomes harder to reproduce&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Systemic issue: reduced verification depth
&lt;/h2&gt;

&lt;p&gt;The main shift introduced by AI-generated code is not implementation&lt;br&gt;
speed, but review behavior.&lt;/p&gt;

&lt;p&gt;Before AI, writing code required reasoning during implementation. After&lt;br&gt;
AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  code already looks complete&lt;/li&gt;
&lt;li&gt;  structure appears correct by default&lt;/li&gt;
&lt;li&gt;  reviewers focus on surface validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a subtle degradation in engineering discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  fewer edge-case simulations&lt;/li&gt;
&lt;li&gt;  less reasoning about concurrency&lt;/li&gt;
&lt;li&gt;  weaker validation of failure states&lt;/li&gt;
&lt;li&gt;  acceptance of "looks correct" as correctness&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Impact on real systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Frontend state drift
&lt;/h3&gt;

&lt;p&gt;UI remains stable visually while backend state diverges.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Authentication and session issues
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  race conditions during token refresh&lt;/li&gt;
&lt;li&gt;  inconsistent logout handling&lt;/li&gt;
&lt;li&gt;  background requests using invalid sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Payments and idempotency problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  duplicate transactions&lt;/li&gt;
&lt;li&gt;  retries without deduplication&lt;/li&gt;
&lt;li&gt;  partial failure inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Distributed system inconsistencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  assumption of ordering guarantees&lt;/li&gt;
&lt;li&gt;  reliance on immediate consistency&lt;/li&gt;
&lt;li&gt;  incorrect retry semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues are not immediately visible. They surface as rare,&lt;br&gt;
non-reproducible incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real risk
&lt;/h2&gt;

&lt;p&gt;AI does not generate obviously wrong code.&lt;/p&gt;

&lt;p&gt;It generates code that satisfies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  type safety&lt;/li&gt;
&lt;li&gt;  structural conventions&lt;/li&gt;
&lt;li&gt;  expected patterns&lt;/li&gt;
&lt;li&gt;  readable abstractions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates false confidence during review.&lt;/p&gt;

&lt;p&gt;The critical failure is not bugs themselves, but reduced skepticism&lt;br&gt;
toward code that appears correct.&lt;/p&gt;

&lt;p&gt;Once that happens, correctness is no longer actively verified. It is&lt;br&gt;
assumed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI increases development speed, but it also changes how correctness is&lt;br&gt;
perceived.&lt;/p&gt;

&lt;p&gt;The danger is code that looks correct enough that nobody questions it deeply.&lt;/p&gt;

&lt;p&gt;When that happens, production issues stop being introduced by obvious mistakes and start emerging from unexamined assumptions embedded in clean-looking code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>codequality</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
