<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Swiggy Bytes — Tech Blog - Medium]]></title>
        <description><![CDATA[Stories on how technology enables Swiggy to provide unparalleled convenience to consumers - Medium]]></description>
        <link>https://medium.com/swiggy-bytes?source=rss----9556560f659---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Swiggy Bytes — Tech Blog - Medium</title>
            <link>https://medium.com/swiggy-bytes?source=rss----9556560f659---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 15:09:59 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/swiggy-bytes" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Search That Gets You: Inside Instamart’s LLM-Powered Search]]></title>
            <link>https://medium.com/swiggy-bytes/search-that-gets-you-inside-instamarts-llm-powered-search-b95b6a93ed60?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/b95b6a93ed60</guid>
            <category><![CDATA[ai-search]]></category>
            <category><![CDATA[information-retrieval]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[quick-commerce]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Aarav Nigam]]></dc:creator>
            <pubDate>Wed, 28 Oct 2026 04:28:58 GMT</pubDate>
            <atom:updated>2026-10-28T04:28:56.710Z</atom:updated>
            <content:encoded><![CDATA[<h4><strong>How large language models help shoppers find what they mean, not just what they type.</strong></h4><p><strong>Author:</strong> <a href="https://medium.com/u/63f5f0827aa6"><strong>Aarav Nigam</strong></a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ywjY2pVjYgyNkOiZHxY3Og.png" /></figure><h3>Introduction</h3><p><em>Every search box eventually hits a wall where traditional search stops being useful.</em></p><p>Searching <strong>tide powder 100g</strong> on Instamart works seamlessly. Exact keywords match product titles — a problem classical search solved decades ago.</p><p>Then someone types this:</p><ul><li><strong>Get me ready for beach holidays</strong></li><li><strong>Snacks for a 5-year-old birthday party</strong></li><li><strong>No added sugar biscuits for my diabetic grandfather</strong></li><li><strong>I’m tired</strong></li></ul><p>These natural queries derail traditional keyword search:</p><ul><li><strong>No exact match:</strong> There is no product named <em>“i’m tired,”</em> nor does the catalog metadata capture such situational intent.</li><li><strong>Misleading keywords: “</strong>Get me ready for beach holidays<em>”</em> matched “holidays,” returning party supplies instead of beach items.</li><li><strong>Contextual intent:</strong> A query like “snacks for a 5-year-old birthday party” specifies an occasion and an age — neither of which appears on a packet of crisps.</li></ul><p><strong>The engine isn’t broken. It’s answering a question nobody asked.</strong></p><p>To bridge this gap, we replaced Instamart’s query-understanding layer with an AI search agent that interprets the underlying intent in real time and translates it into relevant results.</p><p>Handling <strong>intent-driven queries at scale daily</strong>, this is India’s first quick-commerce search stack with a live <strong>large language model (LLM)</strong>. It unlocks three capabilities:</p><ul><li><strong>Situational queries:</strong> Interprets complex situational requests like “Snacks for a 5-year-old birthday party” without needing explicit product or department keywords.</li><li><strong>Multiple constraints:</strong> Narrows results with complex criteria like “No added sugar biscuits for my diabetic grandfather.”</li><li><strong>Multilingual support:</strong> Processes Hindi, Hinglish, Kannada, and Telugu in native scripts directly.</li></ul><p>Understanding vague queries is straightforward. The real architectural challenge was validation: <strong>we assume the model is wrong until it’s verified.</strong> One LLM call interprets; subsequent passes verify.</p><p>We start with what a shopper sees, then the designs that failed, and the idea most of this system leans on: turning words into numbers so meaning can be looked up.</p><h3>What the shopper actually sees</h3><p>Before diving into the underlying machinery, let’s examine the user experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*i6ZSmc5ud6ooUkeGTQb7zA.png" /><figcaption><em>Same query, same catalog. On the left, the engine matches the only word it recognises and returns a flat list. On the right, the query is understood as a single intent spanning four different areas, creating interactive category chips — clickable filter buttons at the top of the screen — that let shoppers quickly browse products by category.</em></figcaption></figure><p>The category chips dynamically display broad results across category shelves (e.g., <em>Swimwear, Beach Gear</em>, <em>Sun Care</em>), allowing immediate navigation instead of endless scrolling as shown in the figure.</p><p>The structure underneath that right-hand screenshot looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XayEx_mmm84uqphVrvHaJA.png" /><figcaption>The shopper asked for an occasion; the engine mapped it to the necessary shelves.</figcaption></figure><p>The harder case is a query with no relevant product in it at all.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bNTTNMMOf4xZ67ooFK_P1A.png" /><figcaption><em>While modern lexical search relies on sophisticated techniques — including tokenization, fuzzy matching, prefix matching, and synonym expansion — it still fundamentally relies on explicit text signals. A traditional search engine fails here because no product title or tag contains the phrase “i’m tired.” Instead, it incorrectly matches the query to detergent powder because the string “tired” sounds lexically similar to “Tide” (referencing Tide detergent powder). By contrast, the AI search agent goes beyond keyword mapping to interpret the underlying situational need. It responds with products a helpful shop assistant would suggest, accompanied by a brief explanation so the recommendations feel clear and logical to the user.</em></figcaption></figure><p>The search bar is natively multilingual with AI Search. Because the agent processes semantic meaning rather than English string matches, queries in Hinglish or native scripts like Kannada work out of the box.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NHcdKGBxY7yObFf5lra3Vw.png" /><figcaption><em>No transliteration step, no separate language pipeline. The examples above show queries like “ಏನೇನ್ ಬಾತ್ ಮಾಡಲು ಏನೆಲ್ಲಾ ಬೇಕು” (‘What is needed to make Bisi Bele Bath?’) in Kannada script and “Pet Dard Kar Raha Hai” (‘My stomach is hurting’) in Hinglish. The query is understood as meaning, and meaning is language-agnostic once it’s been turned into numbers — which is the subject of a later section.</em></figcaption></figure><p>Unshoppable queries (e.g., <em>“What is the capital of India?”</em>) receive a conversational reply without product results, maintaining catalog quality and reducing unnecessary overhead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EM8HY0U00ZSUNAGGWlUMzw.png" /><figcaption>Example of an Unshoppable Query</figcaption></figure><p>That’s the third shape of answer this section has shown. First, chips — a broad query fanned out into browsable categories. Then, a single ranked list with a line of explanation — for “i’m tired,” the catalog can’t name the need, but the shelves still can stock it. Now, a plain conversational reply — for “What is the capital of India?” there is nothing to shop, so no products appear at all. The search box never changed; the query did.</p><p>Which shape a query gets is decided before a single product is fetched. That decision is where the design starts.</p><h3>Two decisions before anything else</h3><p>Not every query should go to the language model, and not every query is a request to buy something. Before anything is fetched, the system settles two questions about each query. They are independent of each other.</p><p><strong>Question 1: Which path does this query take?</strong> There are three.</p><p><strong>Plain search.</strong> “milk” and “almond milk” name the thing the shopper wants. Keyword search handles these quickly and well, and there is nothing for a model to interpret, so they stay on the classic path.</p><p><strong>AI Search.</strong> “Get me ready for beach holidays” and “i’m tired” do not name a product. The shopper describes a need, and someone has to work out which products meet it. This is the conversational search the agent was built for.</p><p><strong>Remark.</strong> “Who is the president of India” has nothing to shop for, so the shopper gets a conversational reply and no products. The bar for this path is deliberately high. “My friend is in hospital” is not a purchase request on its face, but flowers and fruit are exactly right, so it goes to AI Search. A remark is the last resort, not the first.</p><p><strong>Question 2: How broad is the answer?</strong> This applies to plain search and AI Search alike. A broad query covers many kinds of product, so the results come with category chips that let the shopper jump to the one they meant. A narrow query points at one kind of product, so the shopper gets a single list and no chips.</p><p>“milk” is broad: protein milk, almond milk, and flavoured milk are different shelves, so it gets chips. “almond milk” is narrow, so it gets one list.</p><p>“Get me ready for beach holidays” is broad: swimwear, beach gear, and sun care all belong to it, so it gets chips.</p><p>“no added sugar biscuits for my diabetic grandfather” is narrow, because the shopper needs one kind of product, so it gets one list.</p><p><strong>Conversational or not is about how the shopper asks. Broad or narrow is about how much ground the answer covers. Because the two are independent, both plain search and AI Search can end up with chips or with a list.</strong></p><p>Both questions are policy, not linguistics, and both lean deliberately. Routing leans towards shopping: a remark is the last resort. Breadth leans towards restraint: for a described need, extra product types are added only when the query is a real plan, like a trip or a party, and not just because something else could go with it.</p><p>Classic search would have served ‘milk’ and ‘almond milk’ all along. The other two need the agent — and they are why it exists.</p><p>Knowing which path a query should take is the easy half. Making AI Search answers reliable is harder, and our first three designs for doing that did not survive testing.</p><h3>Why the obvious approaches did not work</h3><p>We evaluated three alternative designs that failed in testing:</p><ol><li><strong>Separate model calls per task:</strong> Introduced unacceptable network latency.</li><li><strong>Displaying raw model outputs:</strong> Caused broken UI rails due to hallucinated non-existent catalog terms.</li><li><strong>Passing full category trees:</strong> Led to incorrect pairings between valid shelves and parent categories.</li></ol><p>Additionally, early prompts over-indexed on grocery examples, causing the agent to skew heavily toward grocery results even for non-grocery items. Re-framing prompt context quickly unlocked the entire catalog.</p><p><strong>Key takeaways:</strong> Request minimal model output, verify everything against catalog truth, and ensure framing doesn’t inadvertently restrict capabilities.</p><p>What replaced those designs leans on one underlying trick: turning words into numbers, so that meaning can be compared and looked up. It shows up so often in this system that it deserves its own explanation.</p><h3>Turning words into numbers</h3><p>Most of our verification pipeline relies on vector embeddings to convert semantic intent into actionable numbers.</p><p>An <strong>embedding</strong> <strong>maps text into a numeric vector space based on semantic meaning rather than spelling</strong>. Our in-house embedding model understands our exact catalog vocabulary, brand names, and pack sizes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hYbNx60jyToQaD92DF1vWQ.png" /><figcaption><em>Illustrative, not real data — a real embedding has hundreds of dimensions, and this is flattened to two so it fits on a page. Every catalog term sits somewhere in this space. An incoming query gets placed in the same space, and the answer is simply whatever is nearest. </em>Notice that rash guard <strong>lands</strong> next to swim shorts despite sharing no words<em>.</em></figcaption></figure><p>Storing these vectors enables fast proximity lookups in live requests via a <strong>vector store</strong>.</p><p>That single mechanism does three different jobs here:</p><ol><li><strong>Map invented/typo terms:</strong> Proposed terms map directly to the nearest catalog terms, bypassing explicit spell-checking or exact string matching.</li><li><strong>Dynamic few-shot retrieval:</strong> Fetches the top-3 closest query-answer pairs to populate the prompt with relevant context dynamically.</li><li><strong>Semantic caching:</strong> Reuses verified historical responses for close vector matches.</li></ol><p>With that lookup in hand, we can put the pieces together and follow a single query through the system.</p><h3>One call to understand, two passes to verify</h3><p>Our production architecture balances deep semantic understanding with strict performance budgets:</p><ol><li><strong>Single Understanding Call:</strong> A primary model call handles routing, intent sizing, query expansion, and attribute extraction in one pass.</li><li><strong>Parallel Verification:</strong> Two independent validation routines verify attributes and expansion terms simultaneously against catalog data.</li><li><strong>Response Assembly:</strong> A lightweight final pass synthesizes validated results into client-ready category rails.</li></ol><p>Here is one query, <strong>“get me beach wear for men”</strong>, moving through all three steps. The last box is what the shopper finally sees:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/930/1*ksqwBcc3Ar7EXpeKN--VfA.png" /><figcaption>One query through understand → verify → assemble; the last box is what the shopper sees</figcaption></figure><p>Consolidating understanding into one primary LLM call reduces latency. Downstream verification uses lightweight lookups or dedicated micro-prompts.</p><p>Every proposed term and attribute is checked against inventory; unmatched terms are discarded.</p><p>This part is unglamorous and it is the load-bearing wall. The model is allowed to be creative about <strong><em>what to suggest</em></strong>. It is never allowed to be creative about <strong><em>what exists</em></strong>.</p><p>Checking terms against the catalog settles what exists. It does not settle where each term belongs in our category tree, and that turned out to be the model’s weakest spot.</p><h3>Keeping the model out of the hierarchy</h3><p>This is the piece we’d most want another engineer to take away.</p><p>To assign terms to our 3-level category tree safely, we avoid exposing the full hierarchy to the LLM.</p><p>Instead, the model evaluates a flat list of shelves solely to remove invalid options. Deterministic catalog mappings then apply the correct parent categories.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/300/1*aCTjrNUVLylAfe0dB2XxFw.png" /><figcaption>How we perform validation, without showing the whole category hierarchy to the model</figcaption></figure><p>The model sees a flat list of shelves and answers exactly one question: which of these don’t belong with this term? It returns things to delete. A lookup table built directly from the catalog’s own data then supplies the parent categories for whatever survived.</p><p>This structural separation prevents invalid pairings, minimizes prompt size, and adds virtually no latency overhead.</p><p><strong>Takeaway:</strong> Give LLMs smaller, evaluative tasks rather than structural data responsibilities. Rebuild structure deterministically.</p><p>So far every answer has been built fresh. But the fastest answer is one we have already verified, which brings us to reusing past answers, and to the ways that can go wrong.</p><h3>Reusing answers without repeating mistakes</h3><p>The fastest possible response occurs when the model call is bypassed entirely via semantic caching. However, relying solely on similarity scores introduces specific failure modes where distinct intents appear vector-adjacent:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vreTqRBzPVbwDYJHJH2aFQ.png" /><figcaption>Every row is a pair that a similarity score alone would happily serve, and every one would be the wrong answer for a real person. The lesson is not “raise the threshold.” No threshold separates sugar biscuits from sugar-free biscuits, because what makes them different is a single word carrying a negation, and negation is close to invisible to an embedding.</figcaption></figure><p>To prevent incorrect cache hits, queries pass through strict deterministic pre-filters:</p><ul><li><strong>Exact match validation:</strong> Exact string matches pass through immediately.</li><li><strong>Language consistency:</strong> Mismatched scripts or writing systems are rejected.</li><li><strong>Length disparity checks:</strong> Large length differences indicate distinct query specificity, and such pairs are discarded.</li><li><strong>Negation handling:</strong> Queries containing asymmetric negations (e.g., “sugar free”) are immediately rejected.</li><li><strong>Numeric matching:</strong> Cached queries containing numbers (model versions, pack sizes) require exact numerical agreement.</li></ul><p>Only candidates passing all pre-filters are evaluated by similarity scoring.</p><p>The ordering matters more than any individual rule. A wrong cache hit doesn’t degrade the experience quietly — it serves one shopper another shopper’s answer. So the cheap, categorical checks go first, and the fuzzy one only gets to say yes after all of them have failed to say no.</p><p>Whether an answer is fresh or reused, one thing is left to get right: the order in which its results appear.</p><h3>Ordering results so the breadth shows</h3><p>Raw model outputs group results sequentially by category. Showing one full category first pushes the remaining broad results off-screen.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N_tYRkSNVMqoPieSMwZKug.png" /><figcaption><em>Same nine results, same relevance, two different orderings. On the left the shopper has to scroll past an entire category to discover there was a second one. On the right the first screen answers the question “what did this query mean?” — which is the actual question a broad query is asking.</em></figcaption></figure><p>We apply <strong>diversity-aware interleaving</strong> by cycling through category rails to immediately showcase overall result breadth.</p><p>This reordering maximizes visual diversity on mobile viewports without altering item relevance.</p><p>Ordering shapes what the shopper sees first. The other thing they experience is the wait before they see anything at all.</p><h3>Designing around the wait</h3><p>To reduce perceived latency during LLM reasoning, a fast micro-model streams dynamic status updates (e.g., <em>“Searching for cute small clothes for little children…”</em>).</p><p>This makes the brief wait legible and reassuring to the shopper without significant system cost.</p><p>The lesson generalises beyond search. When you can’t remove a wait, make it legible.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iIKaBwR6p-p29am502gz6A.png" /></figure><p>Making the wait legible helps when the system is merely slow. What should happen when part of it fails outright?</p><h3>Failing in the right places</h3><p>The whole pipeline runs against a hard latency ceiling, and the budget is nested: each stage asks for what it needs but only ever receives what’s actually left. A slow first call doesn’t blow the deadline — it squeezes everything downstream — and if the clock runs out, whatever finished in time is assembled into a response rather than the request collapsing.</p><p>Under strict latency budgets, sub-tasks degrade gracefully based on criticality:</p><p><strong>Non-critical failures:</strong> Dropped cleanly (e.g., missing status captions or attributes) to deliver partial valid results.</p><p><strong>Core understanding failure:</strong> Fails explicitly rather than silently serving confident empty states.</p><p>The rule that came out of it: for each component, ask whether a wrong answer is worse than no answer. Where it isn’t, degrade quietly. Where it is, fail where someone will notice.</p><p>To see how these pieces work together, let’s trace one query from the moment it is typed to the moment the results appear.</p><h3>A query from start to finish</h3><p>Tracing <em>“get me beach wear for men”</em> through the full pipeline illustrates how these components interact:</p><ol><li><strong>Cache Lookup:</strong> The query misses the semantic cache, triggering live execution.</li><li><strong>Understanding Pass:</strong> The primary model classifies the query as shoppable and broad, outputting three rails (Swimwear, Beach Gear, Sun Care), six expansion terms, and the attribute <em>“men.”</em></li><li><strong>Parallel Verification:</strong> The attribute <em>“men”</em> is validated against catalog metadata. Expansion terms are verified against real inventory. Ambiguous terms like <em>“flip flops”</em> have their candidate shelves pruned via the lightweight verification prompt.</li><li><strong>Interleaving and Cleanup:</strong> Results are interleaved across category rails to ensure UI diversity, with any empty rails dropped.</li><li><strong>UI Delivery:</strong> Three populated category chips ship to the mobile app alongside curated product listings.</li></ol><p>That is the design in motion. The next question is whether it actually works.</p><h3>How we judge whether it works</h3><p>Evaluation relies on two complementary feedback loops:</p><p><strong>LLM-as-a-judge:</strong> Blind side-by-side scoring across specific rubrics catches regressions before deployment.</p><p><strong>A blind judge.</strong> When a prompt or model changes, old and new outputs for the same query are scored side by side by a separate model, the two candidates are shuffled and scored against a per-criterion rubric. Routing, keywords, and the explanation line each get their own rubric, because “good” means something different for each. It’s an imperfect grader but a consistent one, and it catches regressions that hand-checking a dozen queries does not.</p><p><strong>Funnel metrics:</strong> Real-world CTR, add-to-cart, and order conversions validate actual relevance.</p><p>Judge scores decide whether to ship a change. The funnel decides whether we were right.</p><p>Judges and funnels guided each change along the way. The larger question was whether the agent as a whole was better for shoppers and for the business, which we tested in an A/B experiment.</p><h3>Impact</h3><p>A/B testing demonstrated significant performance and business improvements:</p><ol><li><strong>Order volume boost:</strong> Reduced null search results to nearly zero by resolving conversational requests.</li><li><strong>Latency reduction:</strong> Single-pass LLM routing, parallel validation, and semantic caching significantly reduced response times.</li><li><strong>Multilingual engagement:</strong> Organic adoption across Indic languages without explicit marketing.</li><li><strong>Architectural stability:</strong> Deterministic hierarchy mapping eliminated misclassification bugs entirely.</li></ol><p>Along the way we picked up a handful of lessons that apply well beyond search.</p><h3>What we learned</h3><p>Building and scaling this architecture provided several key technical takeaways for production AI systems:</p><ul><li><strong>Latency lives in round trips, not work:</strong> Consolidating multiple model tasks into a single rich prompt yields significant latency savings compared to chaining smaller calls.</li><li><strong>Model output is a proposal, not a fact:</strong> Generative outputs must always be grounded and verified against hard catalog constraints before reaching the user interface.</li><li><strong>Scope the model tightly:</strong> Use AI for intent interpretation while relying on deterministic lookups for static data structures like category hierarchies.</li><li><strong>Similarity is not equality:</strong> Guard vector search and caching mechanisms with explicit categorical rules to catch fine-grained distinctions like negations and numbers.</li><li><strong>Design explicit failure modes:</strong> Allow non-critical UI elements (such as captions) to degrade gracefully while failing loudly on foundational understanding calls.</li><li><strong>Verify prompt assumptions:</strong> Ensure prompt examples reflect the entire catalog breadth rather than narrow product subsets that unintentionally bias model output.</li></ul><p>None of this is finished, of course. Here is what we are working on next.</p><h3>Limitations and what comes next</h3><p>Current focus areas for ongoing development:</p><ul><li><strong>Feature discoverability:</strong> Guiding shoppers to use conversational natural language queries.</li><li><strong>Sharpening the routing</strong> between the language model and classic semantic search, so each gets the queries it’s genuinely better at.</li><li><strong>Voice integration:</strong> Enabling complex, hands-free queries.</li><li><strong>Conversational assistant:</strong> Evolving into a full-featured in-app shopkeeper (<em>dukaandaar</em>).</li></ul><p>Further out, the thing we actually want to build is an in-app shopkeeper — the <em>dukaandaar</em> who knows your household. Not a search box but an assistant you can hand a whole task to: <strong><em>“plan my week’s groceries,”</em></strong> or <strong><em>“top up the staples I usually buy.”</em></strong> Every piece described in this post — reading intent, grounding it in a real catalog, verifying before showing — is a prerequisite for that. A shopkeeper who confidently hands you something you didn’t ask for isn’t one you go back to.</p><h3>Conclusion</h3><p>Closing the semantic gap in quick commerce requires combining LLM natural language understanding with strict deterministic validation against ground-truth catalog data.</p><p>The model does the reading. The system keeps it honest.</p><p><strong>Special thanks to </strong><a href="https://medium.com/u/c2a93860b934"><strong>Rutvik Reddy</strong></a>,<strong> </strong><a href="https://medium.com/u/63c72026f9e5"><strong>Devendra Kumar Sahu</strong></a>, <a href="https://medium.com/u/761ee7af4a62"><strong>Yatish Chandra</strong></a>, <a href="https://medium.com/u/cca293df0fd0"><strong>Aakarsh Nambiar</strong></a>,<strong> </strong><a href="https://medium.com/u/c79b3e8f732f"><strong>Aaditya Khetan</strong></a>, <strong>Saurav Singh</strong>, <a href="https://medium.com/u/98ce99995c9a"><strong>Mayank Solanki</strong></a>, and <a href="https://medium.com/u/7eb9236fe3b9"><strong>Sunil Rathee</strong></a> for their contributions and guidance throughout this project.</p><h3>References</h3><ul><li><a href="https://doi.org/10.48550/arXiv.1908.10084">Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks</a></li><li><a href="https://doi.org/10.48550/arXiv.2303.07678">Query2doc: Query Expansion with Large Language Models</a></li><li><a href="https://doi.org/10.48550/arXiv.2005.11401">Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b95b6a93ed60" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/search-that-gets-you-inside-instamarts-llm-powered-search-b95b6a93ed60">Search That Gets You: Inside Instamart’s LLM-Powered Search</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Solving Regional Consistency of Delivery ETA with a Deep Probabilistic Framework]]></title>
            <link>https://medium.com/swiggy-bytes/solving-regional-consistency-of-delivery-eta-with-a-deep-probabilistic-framework-8d82d6abdf41?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/8d82d6abdf41</guid>
            <category><![CDATA[food-delivery]]></category>
            <category><![CDATA[distributional-modelling]]></category>
            <category><![CDATA[delivery-ml-systems]]></category>
            <category><![CDATA[eta-prediction]]></category>
            <category><![CDATA[probabilistic-models]]></category>
            <dc:creator><![CDATA[Parag Bhandarkar]]></dc:creator>
            <pubDate>Thu, 08 Oct 2026 08:15:48 GMT</pubDate>
            <atom:updated>2026-10-08T08:15:47.179Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5qZMCoUG3yEf6aF5DquRtQ.png" /></figure><h3>1. Introduction</h3><p>In the on-demand food delivery ecosystem, an app’s Expected Time of Arrival (ETA) is the foundation of customer trust. Promise 25 minutes for a 40-minute delivery, and you leave a customer frustrated. Show 50 minutes for a 30-minute delivery, and that customer might abandon their cart entirely. Balancing under- and over-prediction is a delicate trade-off. To maintain operational efficiency and a competitive edge, the business requires granular, real-time control over this trade-off. The machine learning system powering our ETAs must be capable of supporting that level of precision.</p><p>Historically, our system relied on pointwise estimation trained primarily on custom Mean Absolute Error (MAE) loss function. While this setup successfully met global business targets, it struggled to maintain the business metric consistency across regions and segments. Furthermore, it lacked the mechanism needed to optimally manage the tradeoff between under and over prediction errors across segments as per evolving business needs.</p><p>To solve this, we built a deep probabilistic modelling system that predicts the entire distribution of delivery time for every order. Using distribution estimation, we gained precise control over regional business metrics and significantly boosted regional compliance consistency without compromising core prediction accuracy. In this post, we will explore why regional metric consistency matters, examine the challenges of our legacy point-estimation setup, and dive deep into how we engineered our deep probabilistic system and overcame its real-world implementation challenges.</p><h3>2. Background</h3><p>To evaluate the quality of our delivery ETA, we rely on two primary business metrics as below which must be maintained consistently across regions and segments:</p><ol><li><strong>Compliance</strong>: The percentage of delivered orders where <strong><em>Actual Time of Arrival (ATA) &lt; ( Estimated Time of Arrival (ETA) + Buffer Time)</em></strong>. High compliance protects customer trust by preventing delay-induced frustration.</li><li><strong>Difference of Means</strong>: The average prediction bias (<strong><em>mean of ETA — ATA</em></strong>), serving as our primary metric for absolute accuracy. A significantly positive difference indicates systematic over-prediction, inflating promises, triggering demand shaping, and driving customers to competitors.</li></ol><p>These metrics have a trade-off with each other. While ML models strive to improve this Pareto frontier overall, business operations require the capability to navigate this trade-off dynamically at a micro level (by region, time-slot, or customer cohort) because:</p><ol><li><strong>Customer Experience Consistency: </strong>A single national compliance average can mask severe regional disparities. Ensuring consistent compliance across every micro-market protects local customer trust and long-term retention.</li><li><strong>Better Competitiveness: </strong>Granular control enables the business to selectively tighten or relax ETA promises in specific regions/cohorts to counter aggressive local competitor moves without risking ETA promise breaches elsewhere.</li><li><strong>Managing Refund Costs:</strong> Severe delays trigger expensive customer refunds due to cancellations. By stabilizing consistency across different regions, the business can directly curb these financial leakages to stay strictly within regional budget constraints.</li></ol><h3>3. Architectural Choices</h3><p>Historically, we managed regional-level compliance using static city-level buffers applied on top of pointwise ETA predictions. While this approach was easy to understand and simple to deploy, its coarse granularity introduced severe inefficiencies. Applying a blanket time buffer across an entire city heavily penalizes regular, on-time orders, just to safeguard a small fraction of long-tail delayed orders, drastically degrading our global <em>Compliance</em> vs. <em>Difference of Means</em> trade-off. To achieve a finer control, we conceptualized several candidate solutions before choosing our final approach.</p><ol><li><strong>Multi-Task Learning with Specialized Heads</strong></li></ol><p>A natural evolution from static buffers is Multi-Task Learning, using a shared neural network backbone that branches into dedicated heads:</p><p><strong>City-Level Heads</strong>: Adding dedicated heads per city captures broad regional baselines (e.g., traffic speeds in Mumbai vs. Chandigarh). However, each head still outputs a single point estimate, forcing us right back to static, manual buffers to manage regional compliance, while loss gradients from one city still interfere with shared representations for others.</p><p><strong>Fixed Quantile Heads</strong> (e.g., p75, p80, p85 via <em>Pinball Loss</em>): While this predicts global percentiles, a global p80 estimate does not yield 80% compliance in every city due to local variance in delays. If a city requires a p83 estimate to hit its 80% compliance, fixed quantile heads cannot provide it without crude interpolation between p80 and p85.</p><p><strong>City x Quantile Heads</strong>: Combining both creates a combinatorial explosion (e.g., 100 cities x 5 quantiles = 500 heads). The resulting gradient competition across hundreds of losses severely corrupts shared backbone representations, while smaller cities suffer from sparse data.</p><p>The Bottom Line is Multi-task heads remain fundamentally rigid. Any shift in business targets or expansion into new cities requires structural code changes and an expensive, full model retraining cycle.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wNAXaX-OvushmFO9YviQ8w.png" /><figcaption>Framework of Multi-Task Learning</figcaption></figure><p><strong>2. Deep Probabilistic Modeling (Chosen Solution)</strong></p><p>Rather than predicting discrete quantiles, we transitioned to Deep Probabilistic Modeling, where the network predicts the parameters of a probability distribution for every order. Probabilistic modeling emerged as the winning paradigm for two fundamental reasons:</p><ol><li><strong>Loss Decoupling</strong>: The model’s training objective (which is <em>Negative Log-Likelihood</em>) is completely decoupled from downstream business metrics and devoid of manual weight tuning.</li><li><strong>Post-Hoc Optimization across use cases</strong>: Because the output is a continuous distribution, business objectives can be optimized from the single model independently after training. We can dynamically extract any percentile (p54, p77 or p89) at inference time for any city or cohort on the fly, achieving precise compliance targeting without modifying the underlying model.</li><li><strong>Variability Learning</strong>: Unlike point-wise models that output a single deterministic estimate, probabilistic models explicitly quantify and learn order-level uncertainty. By modeling spread alongside expected mean, the system dynamically adjusts predictions to reflect fluctuating real-world conditions like weather, traffic or network stress spikes.</li></ol><h3>4. Data Analysis</h3><p>Before jumping directly into the modelling, we performed extensive <em>Exploratory Data Analysis</em> (EDA) to understand the delivery time data better. Given delivery times are strictly positive and right skewed, potential distributions to fit were lognormal and gamma. We also added gaussian and exponential for baselining.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/946/1*gTn6HNMSpH7m47JEQCsd3g.png" /><figcaption>Distribution plot</figcaption></figure><p>The Lognormal distribution secured the best fit because food delivery is an environment of compounding delays between interdependent legs of order journey. The Gamma distribution took a close second, since it models delivery time as a sum of independent stage-wise waits — cooking, driving, hand-off, etc. In practice, though, these stages aren’t fully independent: a delay in cooking often cascades into a delayed pickup and a rushed, less predictable ride. Gamma’s independence assumption misses this compounding effect, which is exactly what Lognormal distribution captures better.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_v3r_2IypSDUPBzk-_G5XA.png" /><figcaption>Statistical test outcomes by distribution</figcaption></figure><p>Beyond single parametric distributions, we also benchmarked against Mixture Density Networks (MDNs) using Gaussian components. Surprisingly, a single parametric Lognormal distribution consistently outperformed Gaussian MDNs up to 10 mixture components, while requiring a fraction of the parameter complexity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CEx98An76lNYU8qlVsOxmg.png" /></figure><p>Finally, we validated distribution fitment across diverse business-relevant segments (city tiers, distance buckets, peak order hours, etc). Lognormal consistently delivered superior fitment metrics across all micro-cohorts, confirming it as the winner for our deep learning pipeline.</p><h3>5. Model Design</h3><p>We modified our existing point-estimation model by updating the output layer to predict the two parameters of the Lognormal distribution: <em>mu</em> (location) and <em>sigma</em> (scale).</p><p>To enforce <em>sigma</em> &gt; 0, the model predicts <em>log sigma</em>, which is exponentiated to obtain <em>sigma = exp(log(sigma))</em>. We chose exponentiation over ReLU because ReLU can floor at zero, causing division-by-zero errors, undefined log-likelihoods, and dead gradients. Exponentiating <em>log sigma</em> ensures a smooth, strictly positive with well-behaved gradients everywhere.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xaoXVfF5RVGJU9dZpM7fuw.png" /><figcaption>Deep Probabilistic Network Architecture</figcaption></figure><p>We replaced our legacy MAE-based custom loss with <strong>Negative Log-Likelihood (NLL)</strong>. For an observed delivery time <em>x &gt; 0</em> and predicted parameters (<em>mu, sigma</em>), the point-wise NLL loss function is expressed as:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DB1q7RLYBuUCWwijEMZs0w.png" /></figure><h3>6. Challenges</h3><p>Transitioning to probabilistic loss functions in production is rarely plug-and-play. During initial trials with vanilla Lognormal NLL loss, we encountered two immediate training hurdles:</p><ol><li><strong>Numerical Instability</strong> (<em>NaN</em> Predictions): Unconstrained <em>sigma</em> values quickly blew up exponentiation bounds, generating floating-point <em>NaN</em> losses.</li><li><strong>Variance Inflation</strong> (<em>sigma</em>-Inflation): Even after applying basic max clipping to prevent numerical crashes, the model consistently over-predicted sigma. Extremely high predicted variance flattened the distribution curves, rendering point-estimate predictions far less useful.</li></ol><p>To understand why the network favored high variance over accurate mean estimates, we analyzed the geometry of the 3D NLL loss surface.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*flTtBEsCzxZh_o764ZTx9Q.png" /><figcaption>Loss Curve of NLL and trajectory of params update</figcaption></figure><p>When starting from a high initial loss state, the optimizer sees two distinct trajectories to minimize NLL:</p><ul><li><strong>Path 1 (True Learning)</strong>: Adjust to minimize | <em>log x — mu</em> | (learning the true location parameter)</li><li><strong>Path 2 (The &quot;Escape Route&quot;)</strong>: Inflate <em>Sigma</em>, artificially spreading probability mass to lower the loss penalty without refining.</li></ul><p>Because gradient descent greedily follows the steepest directional slope, the optimizer takes <em>Escape Route</em>. In early training iterations—when <em>mu</em> is far from optimal, the partial derivative of <em>sigma</em> presents a significantly steeper gradient than the partial derivative of <em>mean</em>. The optimizer takes this <em>escape route</em>, reducing the loss by inflating <em>sigma</em> rather than performing the harder, more valuable work of updating <em>mu</em>.</p><p>We explored three candidate techniques to close this <em>escape route</em>:</p><ol><li><strong>Smarter Capping</strong>: Implemented EDA-backed thresholds on <em>Sigma</em> that zero out gradients when <em>sigma</em> enters unphysically high ranges. This blocks the escape route and moves the backpropagation towards true learning.</li><li><strong><em>Sigma </em>— Regularization</strong>: Added an explicit variance penalty term based on <em>sigma </em>to the NLL loss to penalize over-dispersion.</li><li><strong>Two-Stage Training</strong>: Cold-started the network using <em>MAE</em> loss to converge on <em>mu</em> first, followed by a second phase of <em>NLL</em> training to learn <em>sigma</em>.</li></ol><p>While all three approaches successfully eliminated <em>sigma</em>-inflation, we selected <strong>Smarter Cappings</strong> for production release. It delivered similar stability without adding extra hyperparameter or complicating the training pipeline with multi-stage execution.</p><h3>7. Offline Evaluation</h3><p>Evaluating a probabilistic model requires assessing two distinct dimensions: <strong><em>Calibration</em></strong> (are the predicted probabilities mathematically reliable?) and <strong><em>Accuracy</em></strong> (does the model outperform our point-estimate baseline on business metrics?).</p><ol><li><strong>Evaluating Calibration</strong></li></ol><p>To verify that predicted percentiles reflect real-world outcomes, we evaluated two complementary diagnostic tools across global and regional cohorts:</p><p><strong>Calibration Curve</strong> plots the predicted percentile on X-axis against the actual observed fraction of orders arriving within that percentile threshold on Y-axis. A perfectly calibrated model follows a 45-degree diagonal line.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/956/1*gqAqmLjat3MTRt8kfJryjA.png" /><figcaption>Calibration Curve</figcaption></figure><p><strong>Probability Integral Transform (PIT) Plot </strong>carries a similar underlying information, but it is uniquely sensitive to specific calibration failure modes — such as systemic over-dispersion (U-shaped), under-dispersion (inverted-U), or directional bias. Observing both plots ensured complete calibration confidence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/690/1*I9Dkq7dGKYZDWy8SrElcqw.png" /></figure><p><strong>2A. Benchmarking Accuracy</strong> Against a Point-Estimate Baseline</p><p>While <em>Continuous Ranked Probability Score (CRPS)</em> is the gold standard accuracy metric for distributional forecasts, our legacy point model outputs a single scalar — making direct CRPS comparison impossible. Furthermore, because the baseline was trained on a custom loss blending MAE with business heuristics, standard point metrics (MAE/MSE on mean or median predictions) yielded inconclusive comparisons.</p><p>To construct a like-for-like benchmark, we converted our continuous distribution into single-point predictions by using a segment-level greedy search. For each segment, we selected the exact distribution percentile that matched the baseline’s compliance level while optimizing the Difference of Means (and vice versa).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8TUv8m_y_8YBIoKJ7NXunA.png" /><figcaption>Metric Improvements via Probabilistic Modeling</figcaption></figure><p><strong>2B. Shift of Frontier for Compliance vs. Difference of Means Tradeoff</strong></p><p>To evaluate performance across all operating percentiles, we introduced a Compliance vs. Difference of Means Trade-off Curve. By sweeping across all input percentiles (p1, …, p99), we plotted the new model’s continuous trade-off frontier. For the legacy model, we generated a comparative baseline curve by adding constant positive/negative time buffers to its point predictions. As shown in figure, the probabilistic model’s trade-off curve strictly dominates the baseline across the entire frontier, delivering a lower Difference of Means at every target compliance level.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/814/1*1LFIHMvuTd2hq4hW4s2EpA.png" /><figcaption>Compliance vs Difference of means Tradeoff</figcaption></figure><p><strong>2C. The Core Business Goal: Achieving Regional Compliance Consistency</strong></p><p>With accuracy and calibration established, we turned to the primary objective of this initiative: enforcing consistent regional compliance. We designed a customized operational metric that measures the percentage of micro-segments (e.g city/time-slot cohorts) hitting their target compliance within a +/- 0.5% margin of error.</p><p>The probabilistic model drove a 13% relative increase in segments meeting compliance targets, eliminating regional ETA volatility and giving business operations precise, predictable control over local delivery promises.</p><h3>8. Productionization and Experimentation</h3><p>Deploying a probabilistic ML model into a real-time production system requires careful operational alignment and robust online validation before full rollout.</p><p><strong>Operational Percentile Mapping: </strong>We defined segments that reflect closely with physical operations and business needs and we applied our offline greedy quantile search on those micro-segment level. With this, we achieved fine-grained control to inspect local performance or adjust safety thresholds dynamically.</p><p><strong>Experimentation Setup: </strong>Validating ETA predictions via traditional user-level A/B testing is notoriously prone to marketplace spillover effects as it violates the Stable Unit Treatment Value Assumption (<em>SUTVA</em>). Because all customers within a geographic zone share the same finite fleet of delivery partners, showing altered ETAs to a treatment group reshapes order distribution, affecting delivery partner availability for the control group.</p><p>Standard user-level A/B tests fail in logistics marketplaces due to network interference. Because Treatment and Control users share the same courier pool, changes in Treatment ordering behavior distort logistics conditions for Control users.</p><p><strong>The Solution: Shadow Production Mode:</strong></p><p>We ran our probabilistic model in Shadow Mode, generating real-time predictions on live order streams without displaying them to users. Since average predicted ETA were held almost similar to production predictions, customer ordering behavior remained unbiased, allowing us to evaluate real-world prediction accuracy without marketplace spillover.</p><p><strong>Shadow Validation</strong><em>: </em>Live shadow metrics matched offline backtesting results, achieving target regional compliance consistency.</p><p><strong>Pan-India Deployment</strong>: The probabilistic pipeline was promoted to serve customer-facing ETA nationwide.</p><p><strong>Reverse Shadow Mode</strong>: The legacy point-wise model was shifted into shadow mode alongside the live probabilistic engine. This creates an automated, continuous counterfactual predictions — proving daily that the new model sustains superior regional compliance over the old approach.</p><h3>9. Key Conclusions and Future Outlook</h3><p>The transition from traditional point-estimate predictions to Deep Probabilistic Modeling marks a fundamental evolution in how we manage delivery time expectations at scale. By predicting full probability distributions rather than single scalar values, we decoupled downstream business objectives from model training, giving us an elegant, zero-retrain control mechanism over regional compliance.</p><p>Probabilistic modeling is now the default paradigm across our time prediction ML ecosystem. Following its success in end-to-end ETA estimation, we have migrated all other time-prediction models, from restaurant kitchen prep times to last-mile transit estimators onto probabilistic frameworks.</p><p>The ability to query full probability density curves in real time has opened up rich downstream possibilities for delivery logistics. In our upcoming blog posts, we will share the details about how we unlocked multitude business use cases using probabilistic framework.</p><h3>10. Credits:</h3><p>Special thanks to <em>Nishant Agrawal</em> for his guidance and <em>Sonakshi Gupta</em> and <em>Prince Raj</em> for their contribution in productionization.</p><h3>11. References:</h3><p><a href="https://www.uber.com/in/en/blog/enhancing-ubers-guidance-heatmap-with-deep-probabilistic-models/">Enhancing Uber’s Guidance Heatmap with Deep Probabilistic Models</a></p><p><a href="https://careersatdoordash.com/blog/improving-etas-with-multi-task-models-deep-learning-and-probabilistic-forecasts/">Improving ETAs with multi-task models, deep learning, and probabilistic forecasts — DoorDash</a></p><p><a href="https://careersatdoordash.com/blog/using-gamma-distribution-to-improve-long-tail-event-predictions/">Using Gamma Distribution to Improve Long-Tail Event Predictions</a></p><p><a href="https://eng.lyft.com/eta-estimated-time-of-arrival-reliability-at-lyft-d4ca2720bda8">ETA (Estimated Time of Arrival) Reliability at Lyft | by Rachita Naik</a></p><p><a href="https://careersatdoordash.com/blog/improving-eta-prediction-accuracy-for-long-tail-events/">Improving ETA Prediction Accuracy for Long-tail Events — DoorDash</a></p><p><a href="https://careersatdoordash.com/blog/deep-learning-for-smarter-eta-predictions/">Precision in Motion: Deep learning for smarter ETA predictions — DoorDash</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d82d6abdf41" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/solving-regional-consistency-of-delivery-eta-with-a-deep-probabilistic-framework-8d82d6abdf41">Solving Regional Consistency of Delivery ETA with a Deep Probabilistic Framework</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Decoding Customer Value: pLTV Models for India’s Quick Commerce Delivery Wars — part 1]]></title>
            <link>https://medium.com/swiggy-bytes/decoding-customer-value-pltv-models-for-indias-quick-commerce-delivery-wars-part-1-d01a3e6015ba?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/d01a3e6015ba</guid>
            <dc:creator><![CDATA[Jayshmi A]]></dc:creator>
            <pubDate>Fri, 17 Jul 2026 06:47:03 GMT</pubDate>
            <atom:updated>2026-07-26T13:35:25.137Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Author: </strong><a href="https://medium.com/u/2fa706c9716b"><strong>Jayshmi A</strong></a><strong><br>Special thanks </strong>to<strong> </strong><a href="https://medium.com/u/5380d1229081"><strong>soumyajyoti banerjee</strong></a><strong> , </strong><a href="https://medium.com/u/10c3c71bd5f1"><strong>Meghana Negi</strong></a>, <a href="https://medium.com/u/5b6ef08e7044"><strong>Shaik Masihullah</strong></a>, Akash Deep for their contributions throughout this project and<strong> </strong><a href="https://medium.com/u/7eb9236fe3b9"><strong>Sunil Rathee</strong></a><strong> </strong>for his guidance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H5EAzW0_4brbzi2dX_Titg.png" /><figcaption>The counterintuitive story of how 350+ carefully engineered features + a simple MLP matched industry leaders and what that says about where the real moat lives in ML.</figcaption></figure><h4>Introduction</h4><p>Predicted lifetime value is one of the most fundamental signals in e-commerce. It quietly powers the long term growth by influencing: how platforms prioritize acquisition, how ad systems optimize bids, how retention programs decide whom to invest in, how incentives are allocated, and even how risk and fraud systems reason about customer quality over time. Underneath many of these decisions is the same question: not just <strong><em>will</em></strong> <strong><em>this user act</em></strong>, but <strong><em>how valuable are they likely to become?</em></strong></p><p>Most growth systems are optimized for the next action: a click, a cart, a first order. But for a marketplace like Swiggy — operating across two distinct verticals, food delivery and quick commerce (Instamart) , that is often too narrow. Two users can look nearly identical in the first few days after signup and still end up contributing very different long-term values. The install to purchase conversion rate in e-commerce is 1%-3%. This number is even smaller in the initial week or first few days. The system had to make high-stakes decisions in a setting where most users generated weak/no early signals, and where the business outcome we actually cared about <em>‘Lifetime Value’</em> appeared much later and was concentrated in a relatively small share of users.</p><p>Our early attempts made this gap painfully visible. We started with an existing conversion-prediction model - the model produced predictions with a very high MAPE, meaning the predicted numbers were too noisy to put in a production system. This is a well-known challenge in the pLTV literature; point-estimate accuracy on heavily zero-inflated, long-tailed targets is notoriously difficult, and models trained for binary classification objectives (will this user convert?) don’t generalize to continuous value prediction (how much will this user be worth? - regression). The nuances of the Swiggy marketplace significantly amplify this challenge.</p><blockquote>The problem wasn’t producing a score; it was producing a score that could survive sparse early signals, distinguish high-value users from low-value ones early enough to change downstream decisions, and remain robust enough to plug into bidding systems.</blockquote><h3>The Constraints: Why This Problem Is Hard</h3><p><strong>The zero-inflation challenge.</strong> A large share of users place zero orders in their first 30 days. Any model that doesn’t explicitly handle this zero-mass will be systematically wrong, and a model that predicts “everyone is zero” will have a deceptively low MAE but zero business value. You can ace the loss function and still fail the business problem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/838/1*SoVlddVRkwbCZUnMulD8LA.png" /><figcaption>Fig 1. Illustration of a typical LTV distribution as published in Google’s <a href="https://arxiv.org/pdf/1912.07753">paper</a>. Notice the large spike indicating zero or low ordering users.</figcaption></figure><p><strong>Multi-vertical complexity.</strong> A user’s value isn’t one number. They might be a weekly Instamart shopper who never orders Food, or a daily Food orderer who occasionally adds Instamart. The model needs to predict both independently. These two verticals have fundamentally different purchase patterns, zero-rates, and spend distributions but are also correlated by the same user behaviours.</p><p><strong>Cold-start at the worst possible moment.</strong> We predict soon after the user registers, much before the first order, with zero transaction history. The only signals possibly available are acquisition metadata, device characteristics, and geo-context. This is the hardest possible moment to make a lifetime value prediction, and also the moment when it matters most (ad platforms need the signal at install time to optimize bidding).</p><p><strong>The Swiggy market reality: two populations, one prediction target.</strong> Unlike mature e-commerce markets where most acquired users follow a broadly predictable spending trajectory, the Swiggy market is structurally different. A large share of new users are trial-motivated, they arrive through cashback offers, first-order discounts, or coupon-driven campaigns. They may look identical to genuine repeat buyers at day 1: same cart behavior, same session patterns, but their long-term trajectories might diverge.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/994/1*Uwvrh1p90LkbYmz-avL62g.jpeg" /><figcaption>Fig 2. An Illustration of how LTV distribution might fit in an indian e-commerce setup.</figcaption></figure><p>This means the model isn’t just predicting a continuous value from noisy features. It’s fundamentally trying to <em>resolve two populations</em> that are observationally similar at prediction time but behave completely differently over the 30-day horizon. If you get this wrong, you either bid aggressively for coupon-burners who will never return, or miss the genuine high-value users because their early signals looked unremarkable.</p><p><strong>Data richness as a double-edged sword.</strong> Swiggy’s data infrastructure is unusually rich, we don’t just have order history. We have address verification tags, phonetic name matching, geo-hierarchy competition metrics, post-order complaint dispositions by category, SKU-level Instamart category affinity, premium payment instrument usage, fraud signals, and acquisition channel attribution. All available before a single order is placed. The question wasn’t whether there was a signal; it was whether we could extract it without drowning in noise.</p><h3>The Feature Engineering Philosophy: Everything We Know, Encoded as Numbers</h3><p>We started with an audit. For every signal available before a user’s first order, we asked: does this correlate with LTV? If it did, it went in. The result was 350+ features across 12 groups. Our guiding principle was: <em>everything we know, encoded as numbers.</em> Here’s a look into some signals:</p><p><strong>1. Acquisition Context</strong><br><strong>Key Signals:</strong> Channel (organic, paid Food/IM, alliances, media partners).<br>Strongest proxy for user intent — organic vs. paid-campaign installs tell fundamentally different stories about motivation.</p><p><strong>2. Device &amp; Fraud</strong><br><strong>Key Signals:</strong> Jailbreak/GPS spoof/clone app/screen-sharing flags; sessions per device in 24h; payment carts per session..<br>Fraud signals double as LTV signals — a GPS spoofer is also a low-LTV user whose geo features are unreliable.</p><p><strong>3. Geo-Hierarchy</strong><br><strong>Key Signals:</strong> 4 spatial resolutions (Geohash-8 → Geohash-7 → Geohash-6 → city (building to neighbourhood to block to city): order share, discount rate, new-user density, competitor presence.</p><p><strong>4. Post-Order Complaints</strong><br><strong>Key Signals:</strong> Issue rates (missing items, quality, wrong items) at customer, device, and neighborhood level; 7/30/60-day lookbacks.</p><p><strong>5. IM Category Affinity</strong><br><strong>Key Signals:</strong> Order flags, counts, bills, cancellations, complaints per SKU category (F&amp;V, Dairy, Cooking Essentials, Munchies, etc.) at customer + neighborhood level.<br>Category mix is a retention signal — a weekly fresh-produce buyer has fundamentally different LTV than an occasional snack buyer, even at identical 7-day spend.</p><p><strong>6. Last-X-Order Behavior</strong><br><strong>Key Signals:</strong> Per-position features: vertical (IM vs Food), outcome, delivery fee paid, tip, inter-order intervals, complaint type, bill amount.<br>Encodes behavioral trajectory without manual pattern definition i.e the shared trunk learns the sequences.</p><p><strong>7. Affluence &amp; Socioeconomic</strong><br><strong>Key Signals:</strong> Premium/gourmet restaurant orders; delivery fee paid without subscription; address tags (apartments, tech parks); premium payment instruments; corporate meal cards.</p><p><strong>8. Phonetic Name &amp; Address Matching</strong><br><strong>Key Signals:</strong> Same-sounding names in neighborhood: order volume, cancellation rate, complaint rate; similar-sounding address landmarks.<br>Catches household-level signals that individual-level features miss — shared buildings where address strings vary (“Flat 3B, XYZ Apts” vs “XYZ Apartments, 3rd Floor”).</p><p><strong>9. Cart Drop-Off &amp; Misc</strong><br><strong>Key Signals:</strong> COD abandonment ratio; lifetime order aggregates; temporal features (hour of day, day of week).<br>COD drop-off signals payment friction that directly affects completion rates and therefore LTV.</p><p><em>In total: 10+ feature groups, 350+ features, every signal we could extract before the first order.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q-hAEuIRYy6P5Yr702afJA.png" /><figcaption>Fig 3. Feature importance across multiple categories used in LTV prediction.</figcaption></figure><h4>Spotlights</h4><p><strong>Geo-Hierarchy : Four Resolutions, Four Different Questions — </strong>A user’s location is represented at four spatial scales simultaneously.</p><p>Geohash-8: new-user intensity at the apartment. Geohash-7: Instamart order share, discount rate, new-user density, 7-day transacting users. Geohash-6: total and completed orders at 7- and 30-day windows, competitor order share. City: new-user order volume, 7-day window.</p><p>The ratio of block-level to neighborhood-level orders tells the model how “concentrated” demand is around this specific user. A micro-cluster signal that city-level trends completely miss. A user in a building with high Instamart penetration surrounded by a neighborhood with low penetration is a different signal than either dimension alone.</p><p><strong>Post-Order Complaints : Three Levels, One Powerful Signal — </strong>Complaint data (missing items, quality issues, wrong items) aggregated at three levels:</p><p>Customer: What share of this user’s past orders had issues?<br>Device: What’s the issue rate on this specific device? Catches shared-device patterns if this phone has complaints across multiple Swiggy accounts, that’s a signal about the user behind it. <br>Neighborhood: What’s the local issue rate, broken down by Food vs. IM?</p><p>A user with a complaint history will likely churn regardless of individual spend as bad experiences drive abandonment. A neighborhood with high refund-to-bill ratios signals systematic delivery quality issues that depress LTV for everyone there.</p><h3>The Model Architecture:</h3><p>Given the richness of the features, we deliberately kept the model architecture simple.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jYqMuFcz6HBWAEiZWlHuTw.png" /><figcaption>Fig 4. Model Architecture</figcaption></figure><p>Three shared hidden layers, two task-specific heads. No attention, no embeddings, no mixture-of-experts, no transformers. Trained with a custom MAE loss, Adam optimizer.</p><h4>The 4-Head Insight: Auxiliary Tasks as Free Regularization</h4><p>As we iterated, we experimented with expanding to four heads, adding order-count predictions alongside LTV for each vertical. The intuition was that predicting <em>how many orders</em> a user would place (a frequency signal) might help the shared trunk learn richer representations than predicting <em>how much LTV</em> (a magnitude signal) alone. We also experimented narrowing the trunk as order-count heads would provide intermediate supervision. The results surprised us: the 4-head model achieved marginally better performance while using <strong>63% fewer parameters</strong> (135K vs 363K)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l1W4BO9Dcr3yZqrGuXjicQ.png" /><figcaption>Table 1. Comparision of 2-head (LTV only) vs 4-head (LTV + order frequency) models.</figcaption></figure><p><em>(Part 2 of this series will cover the evolution to probabilistic model architectures)</em></p><h3>Results</h3><h4>Why Not Just Report MAE?</h4><p>A natural first instinct is to by MAE, MAPE, R². The problem is that these metrics are deeply misleading on zero-inflated LTV targets.<br>&gt;50% of new users place no orders in a typical e-commerce market. A model that simply predicts zero for everyone achieves a better MAE than most non-trivial models while being completely useless for campaign targeting. High MAPE on non-zero actuals is almost guaranteed even for a good model, because LTV distributions have heavy tails where a single outlier user can swing a percentage error by orders of magnitude.</p><p><strong>Practically Meaningful Eval<br></strong><em>Can the model correctly rank users by their relative value?</em> Ad platforms don’t need the exact value but to say “this user is in the top 20% so bid higher for them.”<br>So we evaluate via <strong>quantile matching and diagonal coverage along with a spearman coefficient</strong>: sort users by predicted and true labels into 10 equal buckets; check how much of the population lands in the right bucket. A random model scores 10% diagonal. The goal is to push that as high as possible, especially at the extremes.</p><p><strong><em>We are able to achieve &gt;0.75 spearman in IM and Food respectively, with diagonal coverage of 70–80%.</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N-NCeP3BOBPQI4pzUlFUyg.png" /><figcaption>Fig 5. Illustration of a heatmap used to measure diagonal coverage of population for whom predicted LTV sits in the right decile.</figcaption></figure><h3>From Model Score to Campaign Decision</h3><p>The model’s role in production is to feed directly into Google’s tROAS bidding. The causal chain is: better rank ordering → correct bid differentiation → more budget allocated to acquirable high-value users → better cohort quality at the same spend.</p><h4>vs. Third-Party Platform</h4><p>The real test came when we ran A/B experiments pitting against an external pLTV bidding platform. The results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SIy_tXQ0vXtko7V8z9zYZg.png" /><figcaption>Table 2. Comparision of in-house pLTV model against a third-party vendor when used in a production system for ads bidding.</figcaption></figure><p>Our in-house model delivered better retention and higher gross order value per acquired user at zero incremental platform cost. The third-party platform acquired more new users per dollar spent (their campaign was more volume-efficient).</p><h3><em>What’s Next (Part 2 Preview)</em></h3><p><em>In Part 2, we’ll cover the model’s evolution from MAE-based point predictions to a probabilistic architecture. This moves the model from “what’s the expected value?” to “what’s the full probability distribution?” enabling the ad platform to make risk-aware bidding decisions in real time. We’ll also dive deeper into the trade-offs, the Kendall uncertainty weighting scheme for multi-task balancing and model metrics.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d01a3e6015ba" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/decoding-customer-value-pltv-models-for-indias-quick-commerce-delivery-wars-part-1-d01a3e6015ba">Decoding Customer Value: pLTV Models for India’s Quick Commerce Delivery Wars — part 1</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[DE Dost: Teaching Machines to Be a Friend on the Last Mile]]></title>
            <link>https://medium.com/swiggy-bytes/de-dost-teaching-machines-to-be-a-friend-on-the-last-mile-a4175e0f7066?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/a4175e0f7066</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[llm-evaluation]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm-applications]]></category>
            <dc:creator><![CDATA[Aishwary Yadav]]></dc:creator>
            <pubDate>Fri, 17 Jul 2026 06:26:37 GMT</pubDate>
            <atom:updated>2026-07-17T06:26:36.334Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lzWdWY4QvIMqCZdXm9VX5w.png" /><figcaption><em>Building Swiggy’s AI onboarding companion for delivery executives, where accuracy alone wasn’t enough and mentorship had to scale.</em></figcaption></figure><p>For a new Delivery Executive (DE) joining Swiggy, the experience is often overwhelming, throwing a world of SOPs, incentive structures, zone logistics, and app workflows at them all at once. This friction is evident in our metrics, but the drop-off isn’t due to unavailable information, as the DEs are exposed to this information in their onboarding and the information is present in sections of the Swiggy DE app (the dedicated mobile app for delivery partners, separate from the consumer-facing Swiggy app); it’s because the information fails to reach them in a way that <strong>builds confidence and delivers instant answers.</strong></p><blockquote>“The ones who survive the first two weeks? They almost always had an experienced DE nearby who could say — Ignore that for now. Focus on this. Here’s how I did it.”</blockquote><p>That’s not a search problem. That’s <strong>mentorship</strong>. And mentorship doesn’t scale. Until now.</p><p><strong>DE Dost</strong> is Swiggy’s answer: a GenAI-powered onboarding companion embedded inside the DE app—not a support bot, but a <strong>friend with infinite patience, consistent tone, and real-time personalized guidance</strong>.</p><h3>What is DE Dost?</h3><p>DE Dost lives inside the Swiggy DE app. New delivery executives can ask questions in any language (English, Hindi, Kannada, Marathi, Malayalam, Telugu, Tamil, Gujarati) and get real-time, personalized answers spanning delivery SOPs, earnings and payouts, app navigation, shift management, and support channels.</p><p>But what sets Dost apart from a typical support bot is a deliberate product choice: <strong>it’s positioned as a <em>companion</em>, not a helpdesk.</strong> The bot doesn’t just answer questions; it anticipates what a confused new DE needs to ask next. It adapts its language, escalates its level of detail when you ask the same thing twice, and maintains a conversational tone that feels like chatting with an experienced colleague rather than navigating a knowledge base.</p><p>This companion approach means the system must dynamically adjust its response based on the user’s immediate context and the complexity of their question.</p><h3>Not all questions need the same depth</h3><p>A DE asking <strong>“What is an incentive?”</strong> needs a very different answer than <strong>“Why did I miss my incentive yesterday?</strong>” The system must know the difference and handle each appropriately.</p><p>We designed Dost around a <strong>3-level personalization framework</strong> that progressively deepens. The level determines the pipeline, the data sources, and the latency budget.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5zk5W2kWm77S9VI4padN4Q.png" /></figure><blockquote>This <strong>framework</strong> enabled us to support multiple pipelines, diverse <strong>data sources</strong>, and varying <strong>latency budgets</strong>. The routing decision between <strong>L1/L2</strong> and <strong>L3</strong> is made before the first LLM call.</blockquote><h3>The Graph State: memory backbone of every conversation</h3><p>When a DE opens Dost, the system assembles their full context before a single LLM call is made. This is why two DEs asking the exact same question can get different answers.</p><p>The Knowledge Base is indexed at the <strong>Fleet × Payout</strong> level. This indexation helps us personalize starting in the cache, not the LLM.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mH-rrGAx5ilQdwjyQ8Nlrw.png" /><figcaption><strong>Personalization Before the First LLM Call</strong></figcaption></figure><h3>The Preprocessing Pipeline</h3><p>Before anything reaches the retrieval system, the raw query goes through a cleanup pipeline. This sounds like plumbing — but it’s actually where a lot of the intelligence lives.</p><p>Consider what DEs actually type. Conversations are messy. A DE might ask, <strong>&quot;When do I get paid?</strong>” followed by &quot;<strong>What if I miss it?</strong>” — the second query is completely unresolvable without knowing what “it” refers to. To a human colleague, it’s obvious from context. To a retrieval system, &quot;What if I miss it?” would return garbage results about missing orders, missed deliveries, anything but payouts. This is the <strong>anaphora problem</strong>, and it destroys <strong>retrieval quality</strong> in multi-turn conversations.</p><p>For tackling this problem, every raw query is cleaned in three steps:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l_BLUqwU56Ov3OW_oLWNdw.png" /><figcaption>The output of preprocessing is a <strong>normalized English query</strong> — complete, unambiguous, and ready for retrieval. Everything from this point on operates on this clean form, not the raw input.</figcaption></figure><h3>Why we used embedding similarity instead of an LLM classifier</h3><p><strong>The routing problem</strong>: distinguish general <strong>KB questions (L1/L2) </strong>from personal diagnostic questions (L3)—without burning latency and cost on an extra LLM call for every single query.</p><p>The naive solution is obvious: just ask the LLM to classify the query. “<strong><em>Is this a general question or a personal one?</em></strong>” Add a short system prompt, get a label, route accordingly. Takes maybe 800ms and $0.0001. Not a big deal per query—but multiply by millions of queries a month, and you’ve added a full second of latency to every conversation, plus non-trivial cost, for a task that doesn’t need LLM intelligence at all.</p><p>There’s also a subtler problem: LLM classifiers can<strong> <em>hallucinate routing decisions</em></strong>. An edge case query like<strong><em> &quot;Kya mera paisa aaya?&quot; </em></strong>is personal—it needs real DE data. But a poorly prompted classifier might see “money” and confidently route it to the general KB about payouts. The DE gets a generic answer when they deserve a personalized one. Trust broken.</p><p>The answer was <strong>semantic routing with a sentence-embedding encoder</strong>. Two routes are defined with seed utterances. At query time, similarity is computed against both, and the highest match wins deterministically, fast, with no hallucination risk.</p><p>The two routes are seeded very deliberately:</p><blockquote><strong>Route A </strong>gets <strong>~140 general onboarding</strong> questions<strong> </strong>spanning every topic in the KB, plus common greetings.</blockquote><blockquote><strong>Route B </strong>gets a focused set of <strong>L3 utterances </strong>specifically about incentives and minimum guarantee</blockquote><p>The two areas where DEs need real-time personal data. The contrast between routes is sharp enough that the encoder can pick confidently even on paraphrased or informal phrasing.</p><p>And because it’s a standalone embedding model — not part of the main LLM pipeline — it can be <strong>retrained independently</strong> if query patterns shift. No full model retraining, no prompt engineering. Just update the seed utterances and re-embed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LWhf913f18jZnycgbmTVqQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zkM43F59aTEU3t8vSq-fTw.png" /></figure><h3>Answer-centric retrieval: find answers, not text</h3><p>Traditional RAG retrieves text passages and asks the LLM to stitch them into an answer. The LLM does too much synthesis. We flipped the structure.</p><p>Imagine you’re looking for “<strong>how to book a shif</strong>t” in a large Swiggy operations manual. A <strong>text-chunked RAG system</strong> might return the paragraph that mentions shift booking in the context of zone eligibility, another chunk that mentions shift slots in the context of payment tiers, and a third about shift cancellation rules. None of them directly answers the question. The <strong>LLM</strong> has to read all three, figure out which parts are relevant, and stitch a coherent answer, which is exactly where hallucinations creep in. The model fills the gaps with confident-sounding guesses.</p><p>DE Dost’s knowledge base isn’t built that way. Instead of chunking documents into paragraphs, <strong>every piece of knowledge is authored as a QnA pair</strong>: a question, a direct answer, and a section tag. When a DE asks, <strong>&quot;How do I book a shift?</strong>”, the retriever finds the chunk that literally answers that question—and the LLM’s job becomes much easier: adapt this answer to the DE’s context and tone, not synthesize something from scratch.</p><p>This also makes knowledge base maintenance cleaner. When a policy changes, you update one <strong>Q&amp;A pair</strong>, not a paragraph buried in a long document.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aOSBdsRWESODBFkCrpvtlw.png" /></figure><h3>Hybrid Retrieval &amp; Reranking</h3><p>Here’s the honest problem with standard semantic search: embeddings are great at understanding meaning but terrible at exact words. Ask for &quot;MG,&quot; and the embedding model might happily return results about “milestones” or “<strong>minimum orders</strong>”—because they’re semantically close. But a DE who typed “MG” means one very specific thing: <strong>Minimum Guarantee</strong>, the floor earnings amount Swiggy promises. Missing that is not a retrieval failure—it&#39;s a trust failure.</p><p>So we fused two completely different signals together.</p><p><strong>Semantic scoring</strong> (cosine similarity) captures intent and meaning. It handles paraphrasing beautifully—&quot;How much will I earn?&quot; and &quot;What is my pay?&quot; point to the same answers even though they share no words.</p><p><strong>Lexical scoring</strong> captures exact vocabulary. It’s split into two sub-signals: <em>containment</em> measures what fraction of the query’s words appear in the chunk (precision-focused), and <em>Jaccard similarity</em> measures the overlap of unique terms between query and chunk (balance-focused). Together they give exact-keyword matching without requiring a full text search index.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dCOtT3-xK1egzfupC_hrzw.png" /></figure><p>The <strong>β parameter is the secret weapon</strong> here. Tune it higher when the domain has lots of precise jargon (like Swiggy operations) and tune it lower when queries are more conversational. It’s not a static hyperparameter—it can be adjusted as query patterns evolve, without retraining anything.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*obMt38I901d49CYvyKIpnw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hvlhemeiJ1v_l3R6qeOE2g.png" /></figure><h3>Responding in the language DEs actually speak</h3><p>DEs speak in a fluid mix — a Hindi sentence with the word “incentive” in English. A direct translation of “incentive structure” into Hindi sounds bureaucratic and alien. That’s not how DEs talk.</p><p>Dost detects language <strong>per turn</strong> — not a one-time setting. If a DE switches from Hindi to Kannada mid-conversation, Dost follows seamlessly. All internal processing stays in English. Only the final response gets translated back.</p><blockquote><strong>Domain-aware dynamic dictionaries:</strong> Swiggy-specific terms — “base pay”, “minimum guarantee”, “incentive”, “milestones”, “onboarding fees” — are <strong>tagged before translation</strong> so they pass through untranslated. Deep-links to in-app screens are preserved too. The DE hears their language, but the operational vocabulary stays exact.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1iTtg5AkYwk-d4jHs1mMqQ.png" /><figcaption>Actual DE dost answering DE queries</figcaption></figure><p>This translation layer supports 8 languages: English, Hindi, Kannada, Marathi, Malayalam, Telugu, Tamil, and Gujarati—all operating on a single English-internal pipeline.</p><h3>Teaching DEs what to ask next — without hallucinating</h3><p>New DEs don’t know what to ask next. A user asking about incentive rules may also need payout timelines, qualification logic, or overlapping incentive math—but doesn’t have the mental model to ask those questions yet.</p><p>That anticipatory layer is what follow-up suggestions are supposed to encode. Not UI convenience — actual mentorship logic baked into the product. When Dost answers,<strong> &quot;How do incentives work?</strong>”, it should already know that the natural follow-up is usually, &quot;Okay, but when do I actually get paid?” or &quot;What happens if I miss the milestone by one order?&quot; ”.</p><blockquote><strong>Follow-ups encode mentorship</strong>. But they’re dangerously easy to get wrong. Early versions generated plausible follow-ups the KB couldn’t answer. DE clicks → vague response → trust broken immediately.</blockquote><p>The damage from a <strong>bad follow-up is disproportionate</strong>. It’s not just one bad answer—it&#39;s the DE learning that Dost’s suggestions can’t be trusted. After that, they stop clicking them. The whole mentorship layer collapses. We treated follow-up quality as a hard correctness constraint, not a nice-to-have.</p><p><strong>Hard constraint enforced:</strong> every follow-up must be grounded in the KB. And computing this without an extra LLM call required an information-theoretic approach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oTyDwElTEAcl1E3EFH6-Yg.png" /></figure><blockquote>The <strong>decay is natural</strong>: if a DE keeps asking about incentives, incentive-related chunks get consumed turn by turn — the follow-up pool shrinks. When they switch to “shift booking”, the pool resets with fresh, relevant suggestions. Mentorship encoded as math.</blockquote><h3>From 15 seconds to 4.5 seconds</h3><p>In the early pilot, Dost’s end-to-end latency sat at 15 seconds. For a DE standing in the sun between deliveries, that’s an eternity. The target was sub-6 seconds.</p><p>The original architecture made <strong>5 separate micro LLM calls</strong>: intent classification, rephrasing, response generation, follow-up selection, and translation. Each added its own network and inference overhead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6ptXScDkIWZosnlBKfSxdA.png" /></figure><h3>Evaluating what matters in onboarding support</h3><p>A massive effort went into ensuring not just accuracy, but safety and contextual correctness at scale. Our evaluation framework was designed to replicate the complexity of real-world DE interactions, while our guardrails specifically addressed the critical challenges of latency and hallucination.</p><h3>LLM-as-a-Judge and Feedback Loop</h3><p>Responses are scored using an <strong>LLM-as-a-Judge</strong> setup, providing a quantitative measure for quality:</p><ul><li><strong>Response Correctness:</strong> Scored on a 0–5 similarity scale against expected gold-standard answers.</li><li><strong>Follow-up Relevance:</strong> Scored on a 0–3 relevance scale, with a hard constraint that all follow-ups must be <strong>grounded in the knowledge base.</strong></li></ul><p>Beyond the automated pipeline (which tracks macro accuracy trending at 90%+), we maintain a tight feedback loop:</p><ul><li><strong>Daily Review:</strong> We hold daily cadence calls with product stakeholders to review live bot responses.</li><li><strong>Micro Tracking:</strong> We drill down into failure categories, tracking specific issues like<strong> knowledge base gaps</strong>, bot misunderstandings, and language-related errors.</li><li><strong>Weekly Insights:</strong> Weekly reports highlight frequent DE concerns and poorly performing categories.</li></ul><p>This rigorous feedback loop fueled the improvements that we see today in DE Dost.</p><h3>The Comprehensive Evaluation Framework</h3><p>To rigorously validate every update, we developed a comprehensive test suite based on a cross-product matrix:</p><ul><li><strong>Breadth and Depth:</strong> The suite covers ~200 unique questions across 2 fleet types (shared, IM) and 2 payout types (daily, weekly), creating ~800 test cases per language.</li><li><strong>Multilingual Validation:</strong> These tests are run end-to-end across 9 languages.</li><li><strong>Adversarial Testing and Safety:</strong> We include contextual queries, paraphrased variants for semantic robustness, and adversarial inputs (such as abusive or off-topic questions) to validate the system’s safety guardrails and content filtering.</li><li><strong>End-to-End Logic Check:</strong> Each test case dynamically routes through the correct <strong>Knowledge Base</strong> path, validating the crucial semantic routing logic as well as the final response quality.</li></ul><h3>Key Challenges and Engineering Guardrails</h3><p>The evaluation process exposed several core challenges that required dedicated architectural guardrails:</p><p><strong>1. Latency Guardrail:</strong> The initial pilot latency of 15 seconds was unacceptable. The fix involved consolidating multiple micro-level LLM calls (for rephrasing, classification, response, follow-ups, and translation) into <strong>broad, structured prompts</strong> to achieve a target P50 latency of 4.5 seconds.</p><p><strong>2. Follow-up Hallucination Guardrail:</strong> The system struggled with generating non-answerable follow-ups. This was fixed by enforcing that every follow-up must be derived from the existing, unconsumed knowledge base chunks, a process managed by an information-theoretic approach using KL divergence.</p><p><strong>3. Retrieval Accuracy:</strong> Traditional text-centric RAG was not effective for operational support. The guardrail was a fundamental switch to a <strong>QnA-structured knowledge base</strong> (where each chunk is a self-contained answer) combined with a hybrid retrieval score (semantic + lexical matching) to handle Swiggy-specific terms like “MG” and &quot;COD.&quot;</p><h3>The future of onboarding isn’t manuals. It’s conversations that never get tiring.</h3><p>What DE Dost taught us is that building an AI companion isn’t primarily a model problem. The model is a commodity. The hard part is encoding <strong>tacit knowledge</strong> — the kind that lives in conversations between experienced and new DEs — into a system that can reproduce it at scale, in the right language, at the right moment, with the right tone.</p><p>Every technical decision in this system was ultimately a product decision in disguise. QnA-structured retrieval isn’t just an engineering optimization—it&#39;s a commitment to never giving a DE an incomplete answer. Semantic routing isn’t just latency management—it&#39;s the difference between a DE getting a generic payout explainer versus a diagnosis of why their specific incentive didn&#39;t get credited last Tuesday. Language adaptation isn’t localization—it&#39;s respect for how people actually talk at work.</p><blockquote><strong>A good LLM answers questions. A great one teaches users what to ask next.</strong></blockquote><p>DE Dost is live on the Swiggy DE app, supporting new delivery executives across India.</p><blockquote><strong>Author</strong>: <a href="https://medium.com/u/b606a27eba38">Rishabh Patil</a> and <a href="https://medium.com/u/1b80ff5da777">Aishwary Yadav</a></blockquote><blockquote><strong>Special Thanks</strong> to Farzan, <a href="https://medium.com/u/0d0c381fa186">Prateek Saxena</a>, Suryansh, <a href="https://medium.com/u/c2925e807cdc">Manav K</a> for their contribution and <a href="https://medium.com/u/10c3c71bd5f1">Meghana Negi</a>, Vaishnavi for their guidance throughout this project.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a4175e0f7066" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/de-dost-teaching-machines-to-be-a-friend-on-the-last-mile-a4175e0f7066">DE Dost: Teaching Machines to Be a Friend on the Last Mile</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Made All Swiggy Apps Operations AI-Native]]></title>
            <link>https://medium.com/swiggy-bytes/how-we-made-all-swiggy-apps-operations-ai-native-1343612ec228?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/1343612ec228</guid>
            <dc:creator><![CDATA[Raj Gohil]]></dc:creator>
            <pubDate>Fri, 17 Jul 2026 06:23:57 GMT</pubDate>
            <atom:updated>2026-07-17T06:23:56.571Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qUA5l62J2u06AaVLkGg1hw.png" /></figure><p>Engineering across all Swiggy Apps has always had a simple problem with a very large surface area: we ship fast, but every release, every production signal, and every operational review touches too many systems.</p><p>For a single release manager or on-call engineer, the day could start across Jira, Confluence, Bitrise, Play Console, Crashlytics, New Relic, Google Sheets, ImageKit dashboards, and Slack threads. None of these tools were wrong. Each had the data it was supposed to have. The problem was that the human had become the integration layer.</p><p>That does not scale.</p><p>Over the last few months, we have been rebuilding ways of working across all Swiggy Apps around a different idea: let engineers stay where the work is happening, and let agentic AI do the repetitive context gathering, cross-tool correlation, formatting, reminders, and first-level analysis.</p><p>The result is not one chatbot. It is an operating layer for mobile engineering. Release checklists, app vitals, New Relic observability, CDN and ingestion cost tracking, TRM follow-ups, rollout health, QA automation, and on-call analysis are now increasingly handled through AI skills, MCPs, scheduled jobs, and Slack-native agent workflows.</p><p>This is the story of what changed.</p><h3>The Problem: Operational Load Hidden In Plain Sight</h3><p>The Swiggy Apps ecosystem is not one app and one release train. It spans multiple business lines and shell apps across Android and iOS. Each team cares about stability, performance, release quality, app size, cost, experimentation safety, and user experience. Each of those has its own telemetry and review rhythm.</p><p>Before automation, a lot of high-leverage engineering time was being consumed by low-leverage work:</p><ul><li>Pulling the same vitals every morning.</li><li>Checking whether release tickets had missing QA owners or blockers.</li><li>Generating release notes and checklist summaries.</li><li>Looking up Crashlytics and Play Vitals before ramping rollout.</li><li>Mapping New Relic alerts to owning teams, widgets, banners, APIs, or configs.</li><li>Preparing TRM notes and chasing action item updates.</li><li>Explaining bandwidth or ingestion cost spikes after the bill had already moved.</li></ul><p>The team did not need another dashboard. We already had dashboards. What we needed was an assistant that could connect the dashboards, understand our workflows, and show up inside the channel where decisions were already being made.</p><h3>The Shift: From Dashboards To Agents</h3><p>We built around a few operating principles.</p><p>First, AI should work inside Slack, not as a separate destination. Engineers already coordinate releases, incidents, and TRMs in channels and threads. If the agent can read the same context, run the same checks, and post structured output there, adoption becomes natural.</p><p>Second, AI should be connected to real tools. A generic model cannot run release operations. Our workflows use domain skills and MCPs for Jira, Confluence, New Relic, Firebase, Play Vitals, Bitrise, Google Sheets, Appium, Playwright, Databricks, ImageKit, and internal mobile tooling.</p><p>Third, humans should stay in control of judgement-heavy decisions. The agent can recommend RAMP, HOLD, or ROLLBACK. It can summarize freshness of crashes, ANRs, adoption, and vitals. But rollout approval remains a human gate.</p><p>Fourth, automation must be reusable. A one-off prompt is useful once. A skill, cron, or MCP is useful every week.</p><p>That mindset changed our execution speed more than any individual tool did.</p><h3>1. Release Management From Slack</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M1-o4s6xSHBfhJkApXPFzw.png" /></figure><p>Release management was one of the clearest places to start because the workflow had a lot of repeatable structure.</p><p>We automated major RM tasks through AI agent skills for Android and iOS release readiness. A release manager can now ask for release status, bug bash readiness, blockers, unassigned tickets, missing QA owners, BL-wise grouping, release notes, or release checklist status directly from Slack.</p><p>The old flow required a release manager to open Jira, filter by fixVersion, manually group tickets, check status transitions, look at QA ownership, scan blockers, and then format an update for the team. The new flow is intent-driven:</p><blockquote>Run Android release readiness for this version.</blockquote><p>The agent pulls from Jira, groups the work, identifies gaps, and posts a structured update. It is not just a summary; it is a decision aid.</p><p>We also moved repetitive release hygiene into automation:</p><ul><li>Baseline profile updates no longer need manual PR verification in the common path.</li><li>Release checklists centralize APK size, memory leak reports, benchmark results, and other pre-release quality signals.</li><li>Release notes can be generated from the release scope.</li><li>Build creation can be triggered through the assistant.</li><li>TRM action items can be followed up through a skill and cron instead of individual manual pings.</li></ul><p>This changes the release manager’s role. The RM is no longer a person doing clerical integration across tools. The RM becomes the reviewer of an already-prepared operational picture.</p><h3>2. AI-Gated Rollouts</h3><p>The most visible release automation is rollout health.</p><p>For staged Play rollouts, the agent correlates Crashlytics, Play Vitals, and rollout adoption to recommend whether the release should ramp, hold, or roll back. This has already been used live in rollout discussions where health checks at early percentages informed the next ramp decision. One example is a 1 percent to 5 percent ramp on a release version after the assistant found no fresh fatal signal.</p><p>The important part is not that AI clicks the button. It does not. The important part is that the agent removes the manual pre-work before the decision:</p><ul><li>What is current adoption?</li><li>Are there fresh fatal crashes?</li><li>Are ANRs moving?</li><li>Are vitals within expected range?</li><li>Are there carry-over issues from earlier releases?</li><li>Does the signal justify a ramp, hold, or rollback?</li></ul><p>The final action still sits behind approval. That is the right balance. AI handles the repetitive analysis; humans retain accountability.</p><h3>3. Daily Observability Without Daily Pulls</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4JKQGhZGxchbHlVU8VGDSQ.png" /></figure><p>Observability used to be highly dependent on who remembered to check what.</p><p>Today, teams across all Swiggy Apps have moved toward recurring AI-driven observability crons that post daily updates into team channels. These cover app vitals such as crash rate, ANR rate, and startup time; UX zippiness metrics; top-of-funnel API RTT; New Relic ingestion; ImageKit CDN bandwidth; and scale-up health updates.</p><p>This sounds simple, but it changes behavior.</p><p>When vitals arrive every day in the channel, observability becomes ambient. The team does not need to wait for a TRM to rediscover a trend. If a startup metric, page load, API latency, or bandwidth number moves, the conversation can start immediately and with context.</p><p>We also invested in deeper signals. For example, warm and hot start debugging now has fragment-level information in New Relic, debug traces similar to cold start traces, and lifecycle timing breakdowns from create to resume to first draw. That means startup regressions can be traced to specific screens or fragments rather than discussed as a vague app-level number.</p><p>This is the difference between “startup is bad” and “this screen path is responsible, here is the owner, here is the metric, here is the next action.”</p><h3>4. Alert Triage With Context, Not Just Noise</h3><p>Alerts are cheap to create and expensive to interpret.</p><p>New Relic alerts used to trigger the same set of questions repeatedly: what changed, which widget or API is involved, who owns it, whether it is a real regression, and what the historical baseline says. The agentic workflow now does the first-level investigation.</p><p>For ImageKit 404 alerts, the assistant has mapped alerts down to the widget, banner ID, missing asset, and config owner. For frozen-frame alerts on collection pages, it has traced top collection IDs to Gandalf layouts, carousel widgets, banner IDs, and Lottie asset URLs. For network issues, the agent can facet signals by dimensions such as ASN and produce an investigation-ready summary.</p><p>Internally, we had already seen the same need in earlier tooling across all Swiggy Apps: New Relic alerts, crashes, and ANRs required manual context gathering across NR, code, logs, Jira, and Slack. The new MCP-backed flow reduces the alert-to-analysis loop from a manual hunt to a structured first response.</p><p>The goal is not to pretend every RCA is solved by AI. The goal is to make sure an engineer starts from the second page of debugging, not the first.</p><h3>5. Cost Tracking As An Engineering Signal</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b6ZQuLRj-ADOGBIJ8m_BIw.png" /></figure><p>Cost used to appear late in the cycle. A bandwidth or ingestion spike would show up in a bill, someone would ask why, and then engineers would reconstruct the story from dashboards and release timelines.</p><p>We changed that by treating cost as part of daily observability.</p><p>ImageKit bandwidth, storage, and video processing signals are now tracked in the same operating rhythm as vitals. New Relic ingestion is also tracked and compared so teams can catch volume changes earlier. Our in house Mobile Toolkit adds a self-serve NR cost optimisation suite with drop filters and cost tracking. The broader release platform now brings CI/CD, Play/App Store release tracking, release quality, and cost visibility into one place.</p><p>This matters because mobile cost problems often hide inside engineering details:</p><ul><li>A media transform change can increase CDN processing.</li><li>A logging or event schema change can increase New Relic ingestion.</li><li>A missing image transform can inflate bytes received.</li><li>A noisy 404 path can create both alert fatigue and bandwidth waste.</li></ul><p>When the daily report includes the why, not just the number, cost becomes actionable. The discussion moves from “the bill went up” to “this version, transform, event table, or asset path moved.”</p><p>That is a better operating model.</p><h3>6. The Long Tail: Skills For Repeated Work</h3><p>Once the team saw the pattern, the automation surface expanded quickly.</p><p>We now have or are actively using skills and MCPs for:</p><ul><li>New Relic query validation and alert investigation.</li><li>Play Vitals analysis.</li><li>Firebase and Crashlytics reports.</li><li>Weekly TRM generation.</li><li>Bitrise build and CI/CD stats.</li><li>Appium and Playwright test generation and execution.</li><li>Perfetto-based performance debugging.</li><li>Google Sheets and Docs automation.</li><li>Figma user journey mapping.</li><li>Engineering code review checks.</li><li>Specialized Swiggy mobile agents for screen exploration, DI wiring, analytics, network layer, and screen-specific code tasks.</li></ul><p>The important cultural change is that engineers are no longer waiting for a central platform team to automate every workflow. The people closest to the pain are building skills for their own repeated work, and those skills are being shared across all Swiggy Apps.</p><p>That is how automation becomes contagious.</p><h3>What Changed Operationally</h3><p>The most obvious improvement is speed. Tasks that earlier required opening several tools now finish in a Slack thread. Some internal examples show New Relic alert analysis in under a minute, UXZ or P0 metric summaries in under two minutes, and custom New Relic metric crons in minutes instead of a manual setup cycle.</p><p>But speed is only one part of the story.</p><p>The bigger change is reduction in cognitive load. Engineers no longer need to remember every dashboard, query, ownership map, release checklist field, or TRM follow-up format. The agent brings the checklist to them.</p><p>We also improved consistency. A manually written release update depends on who is writing it and how much time they have. An automated readiness check follows the same structure every time. Daily vitals reports create the same operating rhythm every morning. TRM action items no longer depend on someone remembering to chase every owner.</p><p>And finally, we improved accountability. When observability, release readiness, rollout health, and cost signals are posted in shared channels, gaps become visible early. The team can act before an issue turns into a production surprise.</p><h3>What We Learned</h3><p>The first lesson is that the best AI workflows are boring in the right way. They do not need to feel futuristic. They need to save X minutes every day, reduce one avoidable miss every release, and remove one manual chase every TRM.</p><p>The second lesson is that AI needs domain tools. A model without access to Jira, New Relic, Play Vitals, Crashlytics, Bitrise, Confluence, and internal release systems is only a writer. A model connected through skills and MCPs becomes an operator.</p><p>The third lesson is that approvals matter. For release rollouts, the safest pattern is recommendation plus human gate. The agent prepares the operational view; the owner approves the action.</p><p>The fourth lesson is that visibility creates adoption. When an automation posts a useful daily report in a channel, everyone sees the value. When a release manager asks a question in Slack and gets a structured answer in seconds, the next RM tries it too.</p><p>The fifth lesson is that small automations compound. A release checklist skill, a vitals cron, a New Relic alert analyzer, a TRM follow-up bot, and a CDN cost report may look small individually. Together, they change how a team operates.</p><h3>What Is Next</h3><p>We are still early.</p><p>The next phase is to expand AI-gated rollouts to more surfaces across all Swiggy Apps, bring more approvers and guardrails into the approval flow, extend daily observability coverage across shell apps, add per-session network data consumption tracking, and build stronger business-impact overlays for performance and reliability signals.</p><p>We also want every repeated operational workflow to have the same question attached to it:</p><blockquote>Why is a human still doing this manually?</blockquote><p>Not because humans are unimportant. The opposite. Human judgement is too valuable to spend on copying metrics, chasing owners, formatting reports, and switching tabs.</p><p>Our goal is simple: make the Swiggy Apps operations fast, visible, consistent, and AI-native. Let agents handle the mechanical load. Let engineers focus on judgement, product quality, architecture, and execution.</p><p>That is how we improve speed without losing control. That is how we reduce operational load without reducing ownership. And that is how the Swiggy Apps ecosystem is turning AI from a productivity experiment into a real operating model.</p><h3>Acknowledgements</h3><p>This shift wouldn’t have happened without a lot of people quietly doing the hard work. <a href="https://medium.com/u/adb4aa10275a">Pratik Sahu</a> ideated and built the agentic AI-driven flows that made this real, with <a href="https://medium.com/u/723d70adf627">Sambuddha Dhar</a> guiding him. <a href="https://medium.com/u/400467f4ffab">Tushar Tayal</a> put his trust in the team to experiment and take it live — and that trust is what let this move from an idea to something running in production.</p><p>And the entire Apps team — the engineers, QA partners, release managers, on-call owners, and platform contributors who kept questioning repetitive work, turned operational pain into reusable automations, and kept building more skills and workflows around it. Their ownership across releases, observability, cost, and quality is what turned AI from an experiment into how Consumer Apps actually works now.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1343612ec228" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/how-we-made-all-swiggy-apps-operations-ai-native-1343612ec228">How We Made All Swiggy Apps Operations AI-Native</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Swiggy Saved 3.7 MB by Solving a Vector Drawable Mystery]]></title>
            <link>https://medium.com/swiggy-bytes/how-swiggy-saved-3-7-mb-by-solving-a-vector-drawable-mystery-d8561877dbca?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/d8561877dbca</guid>
            <dc:creator><![CDATA[Shivam Gupta]]></dc:creator>
            <pubDate>Fri, 10 Jul 2026 05:14:55 GMT</pubDate>
            <atom:updated>2026-07-10T05:17:07.010Z</atom:updated>
            <content:encoded><![CDATA[<h3>🎭 The Mystery Begins</h3><p>Picture this: You’re a responsible Android developer. You’ve done everything right. You’ve replaced those bulky PNGs with lighter, elegant, scalable SVGs. You’ve patted yourself on the back. You’ve probably even tweeted about it.</p><p>Then one day, you open the APK Analyzer and see a familiar face staring back at you — a PNG you never invited to the party.</p><h3>The Crime Scene</h3><p>We recently discovered multiple instances where our APK analyzer reported mysterious size bumps. Upon investigation, we found PNG files that we absolutely, definitely, 100% did not add to our codebase.</p><p>Alongside our pristine SVG files, there were now multiple high-resolution PNG versions — one for each screen density (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*T3Yb931KU6qAHGITGhFIHQ.jpeg" /></figure><p>Lets try to understand this better with one of our SVGs</p><h3>The Case of random.png</h3><p>We discovered a PNG, lets call it random.png had snuck into our APK like that one person who wasn’t invited to the party but somehow ends up in all the photos.</p><p>Plot twist: We never added random.png. We added random.xml.</p><p><em>So how did it end up in the APK?</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b8MTBMVaITgs26jNOXbITA.png" /></figure><h3>🔍 The Root Cause (A.K.A. “It’s Not a Bug, It’s a Feature™”)</h3><p>The automatic generation of these PNG assets comes from how the Android Asset Packaging Tool (AAPT2) handles vector drawables during the build process.</p><p>Here’s what the Android documentation says:</p><blockquote>Android 5.0 (API level 21) and higher provides vector drawable support. If your app has a minimum API level that’s lower, Vector Asset Studio adds the vector drawable file to your project; also, at build time, Gradle creates PNG raster images at various resolutions.</blockquote><p>Alright, that sounds reasonable… and with minSdkVersion 23, we should be safe. Right?</p><p>Upon deeper investigation into the VectorDrawable documentation (you know, the documentation nobody reads until something goes horribly wrong), we discovered the truth:</p><p>While basic vector drawable support begins at API 21, certain tags and attributes — like <em>android:fillType</em> and <strong><em>gradients</em></strong> in <em>android:fillColor</em> — were only introduced in API level 24.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ACS9lInc9IzMdNFDq5SN8A.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OazBk22Ne0B2NUzIUDeJMQ.png" /></figure><p>If these newer attributes are present in your vector XML, AAPT goes:</p><blockquote>&gt; “Oh, you’re using fancy features? Let me just generate PNGs for you.</blockquote><p>Let’s look at our random.xml:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b8064c9ca4e4104275657de2f06e5fb0/href">https://medium.com/media/b8064c9ca4e4104275657de2f06e5fb0/href</a></iframe><p>See that gradient in android:fillColor? That feature was introduced in API 24. Our minSdkVersion is 23.</p><p>Since gradients were used heavily throughout the apps, all those assets automatically qualified for raster generation</p><h3>Impact</h3><p>While vector drawable XML files are typically lightweight, often just a few kilobytes, but they can significantly bloat the APK when they trigger automatic rasterization. The problem lies in the fact that rasterization process is opaque and we don’t know in advance how large the generated PNGs will be, and their size can vary significantly based on the complexity of the vector</p><p>For example, raster_prone.xml is only <strong>~1KB as a vector asset</strong>. However, when rasterized by the build system, it results in a PNG file of approximately <strong>~50KB.</strong></p><p>As XML,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M5AZxoHoZFcv-Qb1EUnZaw.png" /></figure><p>In Bundle,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xQ0KPmwSY-_xw6u9e-MMHQ.png" /></figure><p>For few assets, it generated PNGs worth ~200KB</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2b6H6shPGx_OQVEb5TSf3g.png" /></figure><h3>The Plot Twist</h3><p><em>AAPT2 generates rasterized PNG assets even for unused vector resources.</em></p><p>That’s right. You could have an SVG sitting in your drawable folder, referenced by absolutely nothing, forgotten by everyone, and AAPT will generate PNG versions of it even if you have enabled shrinkResources.</p><h3>Solution?</h3><p>The solve is to use VectorDrawableCompat by referencing vector assets via <strong>app:srcCompat</strong>, particularly for SVGs that include attributes not supported below API 24 and are prone to triggering rasterization.</p><h4>Why VectorDrawableCompat?</h4><p><strong>VectorDrawableCompat</strong> is part of the AndroidX support library and renders vector drawables at runtime on devices running API levels below 24 without relying on pre-generated PNGs.</p><p>Also, we need to explicitly inform Gradle and AAPT that our project intends to use the support library for vector drawables to ensure that Gradle avoids generating density-specific PNGs and relies on the support library to render vector assets at runtime.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f1ad8b01a59cb5bfb81c743d67fcee1d/href">https://medium.com/media/f1ad8b01a59cb5bfb81c743d67fcee1d/href</a></iframe><h3>🧹 What We Actually Did</h3><p><strong>Phase 1</strong>: Removed unused assets — especially vector files that were triggering rasterization. With these gone, they no longer made it into the build, preventing AAPT2 from generating those unnecessary PNG variants and reducing APK size.</p><p>Result — Download size reduced by &gt; <strong>1 MB</strong>.</p><p><strong>Phase 2</strong>: minSDK bump to 24</p><p>Result — Download size reduced by <strong>2.7MB</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RfBU4MzFW0XoJtP4h1SSqg.png" /></figure><p>We observed a similar improvement in our shell apps (Toing-<strong>2.1MB</strong>, Instamart — <strong>2.3MB</strong>) also.</p><h3>References</h3><ul><li>Vector Asset Studio — PNG Generation <a href="https://developer.android.com/studio/write/vector-asset-studio#pngoption">https://developer.android.com/studio/write/vector-asset-studio#pngoption</a></li><li>VectorDrawable API Reference<br><a href="https://developer.android.com/reference/kotlin/android/graphics/drawable/VectorDrawable">https://developer.android.com/reference/kotlin/android/graphics/drawable/VectorDrawable</a></li></ul><h4>About the Author:</h4><p><em>An Android developer who has spent too many hours in APK Analyzer.</em></p><h4>Acknowledgements</h4><p><em>Big thanks to </em><strong><em>Sambuddha Dhar, Raj Gohil, and Tushar Tayal</em></strong><em> for always having my back. This milestone wouldn’t have happened without you all! 🙌</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d8561877dbca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/how-swiggy-saved-3-7-mb-by-solving-a-vector-drawable-mystery-d8561877dbca">How Swiggy Saved 3.7 MB by Solving a Vector Drawable Mystery</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Swiggy Migrated from protobuf-java to Square Wire]]></title>
            <link>https://medium.com/swiggy-bytes/how-swiggy-migrated-from-protobuf-java-to-square-wire-e97d435c86d3?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/e97d435c86d3</guid>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[square-wire]]></category>
            <category><![CDATA[protobuf]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Balvinder Gambhir]]></dc:creator>
            <pubDate>Fri, 10 Jul 2026 05:14:44 GMT</pubDate>
            <atom:updated>2026-07-10T05:17:10.314Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dYQRNWaafakkF6uEP7CJAA.png" /></figure><p>At Swiggy, we use Protocol Buffers everywhere. Nearly every message exchanged between our backend services and our mobile apps is a protobuf. On Android, the default and most widely adopted way to work with those schemas was protobuf-java, Google’s official Java runtime, with Java classes generated by protoc.</p><p>It worked. But when we started profiling our app size, we found that protobuf-java and the code generated by protoc collectively were consuming <strong>6.735 MB</strong> of our overall app size. That is a significant chunk, and it became the reason we started planning a migration away from protobuf-java.</p><p>App size is not just a vanity metric for us. Every additional MB affects install conversion, update reliability, and the size budgets of multiple Android apps built on the same platform stack. Since protobuf sits in a shared layer, any improvement here would not be limited to one screen or one feature. It had the potential to reduce size across <strong>Swiggy</strong>, <strong>Instamart</strong>, and <strong>Toing</strong>.</p><h3>Why Not Just Switch to protobuf-javalite?</h3><p>The obvious answer to a bloated protobuf runtime is protobuf-javalite — Google&#39;s trimmed-down version built specifically for Android. Smaller runtime, less generated code, same wire format.</p><p>We could not use it. The Swiggy app uses google.protobuf.Any extensively across its schema — it is how our APIs handle dynamic, polymorphic payloads. protobuf-javalite strips well-known types, including Any, entirely from the runtime. Switching to lite would have required rearchitecting a significant portion of our serialization layer.</p><p>We needed a library that was smaller than protobuf-java on Android but still supported everything protobuf-java supports, including Any. That led us to Square Wire.</p><h3>What Is Square Wire?</h3><p><a href="https://square.github.io/wire/">Wire</a> is an open-source protobuf implementation maintained by Square. It is byte-for-byte wire-format compatible with protobuf-java — same .proto schemas, same encoding. What changes is the code generator and the runtime library.</p><p>Instead of Java Builder-pattern classes, Wire generates lean, immutable Kotlin objects. The runtime is significantly smaller than protobuf-java. And unlike protobuf-javalite, it supports google.protobuf.Any and the rest of the well-known types.</p><p>Wire also supports JSON serialization out of the box, which is a useful addition — though worth noting that Wire’s JSON parsing speed is significantly slower than its binary parsing. It works, but if you have a high-throughput JSON path, benchmark it before relying on it.</p><h3>The Generated Code: Before and After</h3><p>Take this simple proto:</p><pre>syntax = &quot;proto3&quot;;<br>message CartItem {<br>    string item_id = 1;<br>    string name = 2;<br>    int32 quantity = 3;<br>}</pre><p><strong>What protobuf-java generates (Java):</strong></p><pre>public final class CartItem extends com.google.protobuf.GeneratedMessageV3 {<br>private String itemId_ = &quot;&quot;;<br>private String name_ = &quot;&quot;;<br>private int quantity_ = 0;<br>public String getItemId() { return itemId_; }<br>public String getName()   { return name_; }<br>public int getQuantity()  { return quantity_; }<br>public static Builder newBuilder() { ... }<br>public static final class Builder extends GeneratedMessageV3.Builder&lt;Builder&gt; {<br>    public Builder setItemId(String value) { ... return this; }<br>    public Builder setName(String value)   { ... return this; }<br>    public Builder setQuantity(int value)  { ... return this; }<br>    public CartItem build() { ... }<br>    }<br>}</pre><pre>// To create, modify, encode, and decode:<br>CartItem item = CartItem.newBuilder()<br>    .setItemId(&quot;item_001&quot;)<br>    .setName(&quot;Masala Dosa&quot;)<br>    .setQuantity(2)<br>    .build();</pre><pre>// Modify (must go through builder again)<br>CartItem updated = item.toBuilder()<br>    .setQuantity(3)<br>    .build();</pre><pre>// Encode<br>byte[] bytes = item.toByteArray();</pre><pre>// Decode<br>CartItem item = CartItem.parseFrom(bytes);</pre><p><strong>What Wire generates (Kotlin):</strong></p><pre>class CartItem(<br>    @field:WireField(tag = 1, adapter = &quot;com.squareup.wire.ProtoAdapter#STRING&quot;)<br>    val item_id: String = &quot;&quot;,<br><br>    @field:WireField(tag = 2, adapter = &quot;com.squareup.wire.ProtoAdapter#STRING&quot;)<br>    val name: String = &quot;&quot;,<br><br>    @field:WireField(tag = 3, adapter = &quot;com.squareup.wire.ProtoAdapter#INT32&quot;)<br>    val quantity: Int = 0,<br><br>    unknownFields: ByteString = ByteString.EMPTY<br>) : Message&lt;CartItem, CartItem.Builder&gt;(ADAPTER, unknownFields) {<br><br>    fun copy(<br>        item_id: String = this.item_id,<br>        name: String = this.name,<br>        quantity: Int = this.quantity<br>    ): CartItem = CartItem(item_id, name, quantity)<br><br>    companion object {<br>        @JvmField<br>        val ADAPTER: ProtoAdapter&lt;CartItem&gt; = ...<br>    }<br>}</pre><p>To create, modify, encode, and decode:</p><pre>// Create<br>val item = CartItem(<br>    item_id = &quot;item_001&quot;,<br>    name = &quot;Masala Dosa&quot;,<br>    quantity = 2<br>)</pre><pre>// Modify<br>val updated = item.copy(quantity = 3)</pre><pre>// Encode<br>val bytes: ByteArray = CartItem.ADAPTER.encode(item)</pre><pre>// Decode<br>val item: CartItem = CartItem.ADAPTER.decode(bytes)</pre><p>The difference in generated code is visible immediately. Wire produces a straightforward Kotlin class with public properties. No builder boilerplate, no getter methods, no intermediate allocation for construction or modification.</p><h3>What the Migration Actually Looks Like</h3><h3>Field Names</h3><p>protobuf-java generates camelCase Java getters. A field called user_id in your .proto becomes getUserId() in Java. Wire keeps the original snake_case name as a Kotlin property, so getUserId() becomes user_id.</p><p>Every field access in the codebase needs to be updated. A regex or IDE structural search handles most of it, but budget real time for this — it is the most labour-intensive part of the migration.</p><h3>Building and Copying Messages</h3><p>With protobuf-java, every message is constructed through newBuilder(). Wire generates a plain Kotlin constructor instead, so you pass fields as named arguments directly. Modifying a message is a copy() call rather than a round-trip through a builder. The resulting code is noticeably cleaner.</p><h3>Encoding and Decoding</h3><p>With protobuf-java, messages serialise themselves: message.toByteArray(), MyMessage.parseFrom(bytes). Wire moves this to a static ADAPTER on each generated class: MyMessage.ADAPTER.encode(message), MyMessage.ADAPTER.decode(bytes). Every serialisation call site in the codebase needs updating.</p><h3>The Packed Encoding Gotcha</h3><p>This is the issue to watch out for most carefully, because it produces no compile errors and no runtime exceptions — just a silent wire format mismatch.</p><p>The proto3 spec says that repeated scalar fields — integers, enums, and similar — should be packed by default. protobuf-java follows this automatically. Wire does not enable packed encoding for repeated enum fields unless you explicitly annotate the field in the .proto schema with [packed = true].</p><p>We ran into this when sending requests that had a proto as the request body. The server was rejecting or misreading the payload — the repeated enum fields were being encoded unpacked by Wire on the client, while the server expected packed encoding as per the proto3 spec. No crash, no obvious error, just the server not seeing the data it expected.</p><p>The fix was straightforward: add [packed = true] to the relevant repeated fields in the .proto file. But the issue itself is subtle enough that you will not find it without specifically testing your request payloads end-to-end after the migration.</p><h3>ProGuard Rules</h3><p>Getting ProGuard right with Wire took us several rounds of trial and error. The challenge in our case is that we use both binary protobuf and JSON — Wire’s JSON serialization goes through a different code path than binary, and R8 will aggressively strip things that look unreachable from static analysis but are actually needed at runtime.</p><p>After going back and forth, these are the rules that give us the leanest app size without breaking anything on the Wire side:</p><pre># Wire runtime<br>-keep class com.squareup.wire.** { *; }<br>-keepclassmembers class * extends com.squareup.wire.Message {<br>    public static com.squareup.wire.ProtoAdapter ADAPTER;<br>    @com.squareup.wire.WireField &lt;fields&gt;;<br>    &lt;init&gt;(...);<br>    public *** get*();<br>    public *** is*();<br>}<br>-keepnames class * extends com.squareup.wire.WireEnum<br>-keepclassmembers class * extends com.squareup.wire.WireEnum {<br>    public static com.squareup.wire.ProtoAdapter ADAPTER;<br>    public static ** fromValue(int);<br>    ** INSTANCE;<br>    &lt;init&gt;(...);<br>}<br># EnumJsonFormatter checks simpleName == &quot;Companion&quot; to skip companion objects.<br># Without this, R8 renames the companion and the check breaks, producing corrupt JSON.<br>-if class * extends com.squareup.wire.WireEnum<br>-keepnames class &lt;1&gt;$Companion</pre><p>A few things worth explaining:</p><p><strong>ADAPTER</strong> is accessed reflectively at runtime — not through a direct static reference in your own code, but by Wire internally. If R8 removes or renames it, serialization silently breaks.</p><p><strong>@WireField annotations</strong> are how Wire knows which fields to include when encoding a message and how to map bytes back to fields when decoding. Strip them and your messages come out empty.</p><p><strong>fromValue(int)</strong> is called during enum deserialization to resolve an integer back to the right enum constant. R8 will remove it if you do not keep it, causing a crash at decode time.</p><p><strong>$Companion keepnames</strong> is the one that caught us off guard. Wire&#39;s EnumJsonFormatter checks whether a class&#39;s simpleName is &quot;Companion&quot; to skip it when iterating enum members. R8 renames companion classes when they are not otherwise referenced by name, which causes the formatter to fail and crash. The conditional keep rule targets only companions of WireEnum subclasses, so you are not keeping more than you need.</p><h3>The Code Generation Pipeline</h3><p>At Swiggy, we do not use the standard Wire Gradle plugin directly. Our proto schemas live in a monorepo organised into packages, and we wanted to publish one AAR per package to our internal Maven repository on AWS CodeArtifact.</p><p>To support that, we built a custom generation pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kHKD2h-KQgfAdy46MdGMMg.png" /></figure><p>The build script scans all proto packages, normalises the file structure (Wire requires that a file’s filesystem path matches its package declaration), runs the Wire compiler once across the entire schema, maps the generated Kotlin files back to their source packages, builds an AAR per package, and publishes to CodeArtifact.</p><p>Running the Wire compiler once for everything — rather than per package — is important. Wire needs to resolve all cross-package type references at compile time, so isolating packages causes unresolved type errors. A single pass over the full schema avoids this.</p><p>We also run incremental builds: only packages with changed files, and their transitive dependents, are rebuilt on each CI run.</p><h3>The Result</h3><p>Switching from protobuf-java to Wire reduced the size of the Swiggy app by <strong>3.52 MB</strong>. The savings came from replacing the full protobuf-java runtime and the heavier protoc-generated classes with Wire’s leaner runtime and generated Kotlin models.</p><p>The impact was not limited to the Swiggy app. <strong>Instamart</strong> and <strong>Toing</strong> also depend on the same protobuf-generated artifacts, so the migration gave us measurable size reductions across those apps as well. That made this more than a single-app optimization; it became a platform-level cleanup across our Android apps.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fuoz8V%2F2%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fuoz8V%2F2%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fuoz8V%2Fplain-s.png%3Fv%3D2&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="227" frameborder="0" scrolling="no"><a href="https://medium.com/media/b412f7d28717fda1ad260ba2e6ec0ff9/href">https://medium.com/media/b412f7d28717fda1ad260ba2e6ec0ff9/href</a></iframe><p>If you are on protobuf-java and looking to reduce your Android app size without giving up features like google.protobuf.Any, Wire is the right move.</p><h4><strong>Acknowledgements</strong></h4><p>I want to thank <a href="https://medium.com/u/723d70adf627">Sambuddha Dhar</a> <a href="https://medium.com/u/1bf9bdb89775">Raj Gohil</a> <a href="https://medium.com/u/400467f4ffab">Tushar Tayal</a> for your continuous support throughout this migration. This migration wouldn’t have been possible without you.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e97d435c86d3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/how-swiggy-migrated-from-protobuf-java-to-square-wire-e97d435c86d3">How Swiggy Migrated from protobuf-java to Square Wire</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Scaling Android CI: From 44 → 10 minutes — A Deep Dive into Build Time Optimization and Best…]]></title>
            <link>https://medium.com/swiggy-bytes/scaling-android-ci-from-44-10-minutes-a-deep-dive-into-build-time-optimization-and-best-ff93aec82f05?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/ff93aec82f05</guid>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[cicd]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[cache]]></category>
            <dc:creator><![CDATA[Pratik Sahu]]></dc:creator>
            <pubDate>Fri, 10 Jul 2026 05:14:38 GMT</pubDate>
            <atom:updated>2026-07-10T05:17:03.983Z</atom:updated>
            <content:encoded><![CDATA[<h3>Scaling Android CI: From 44 → 10 minutes — A Deep Dive into Build Time Optimization and Best Practices</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_nqahu2HQn3iwfPFiimGLQ.png" /></figure><p>Android monorepos get expensive and slow as they scale. We re-architected CI/CD caching, trimmed the Gradle graph, tuned the JVM, modernized the toolchain, and finally moved to Apple Silicon — plus specific configs and numbers you can replicate.</p><h3>Context and Baseline</h3><p>Our <strong>CI/CD</strong> pipeline started conventional: git clone → fetch npm and Android dependencies → execute <strong>Gradle</strong> build → notify; the build phase alone <strong>averaged ~35 minutes</strong> and repeatedly re-downloaded dependencies with no cache restore while configuration re-ran due to environment-driven invalidations. Each CI minute cost 2 credits.</p><p>After targeted optimizations, we cut total time to P90 = 11 minutes and P50 = 10 minutes, and reduced average credits per build from 55 to 43 (~20%).</p><p>At ~460 builds/month, we projected ~37% fewer credits and ~37% less wall-clock time, avoiding an imminent $22k/year tier-upgrade by staying on the current plan.</p><h3>What Was Holding Us Back</h3><ul><li><strong>Archive cache irrelevance</strong>: as many modules changed, CICD cache decayed fast and reuse dropped.</li><li><strong>Configuration invalidations</strong> from system properties and env inputs caused re-configuration even when tasks didn’t change.</li><li><strong>Dependency resolution overhead</strong>: Gradle probed multiple repositories for each coordinate, adding latency at our scale.</li><li><strong>Variant bloat</strong>: CI compiled non-required variants, inflating the task DAG by thousands of tasks.</li></ul><p><strong>Machine constraints</strong> created OOMs and noisy variability that masked true performance.</p><h3>Strategy Overview</h3><p>We focused on four compounding pillars:</p><ul><li><strong>Cache architecture</strong> with branch-aware keys, release/develop fallbacks, and a <strong>Gradle Remote Cache</strong> for task-level reuse.</li><li><strong>Build graph control</strong> to shrink the DAG: reduce inter-module coupling and enforce only required variants.</li><li><strong>Dependency resolution</strong> hardening via <strong>repository content filtering</strong> to avoid redundant lookups.</li><li><strong>Runtime tuning</strong>: right-size <strong>JVM heap/Metaspace</strong> for CI and disable work that helps locally but wastes time on ephemeral agents (e.g., configuration cache).</li></ul><h3>Optimization 1: Deterministic Caching with Branch-Scoped Keys</h3><p>We made caching <strong>branch-aware</strong> and OS/arch-aware, and cached more than artifacts: Android SDK components, project dependencies, transformed artifacts, <strong>NPM</strong> cache, and <strong>NDK</strong>.</p><p>Restore priority was: current branch → previous release → develop.</p><pre># Gradle restore (priority order)<br>{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv &quot;SWIGGY_CACHE_VARIANT&quot; }}-{{ getenv &quot;GIT_BRANCH&quot; }}<br>{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv &quot;SWIGGY_CACHE_VARIANT&quot; }}-release<br>{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv &quot;SWIGGY_CACHE_VARIANT&quot; }}-develop<br># Gradle save<br>{{ .OS }}-{{ .Arch }}-gradle-cache-v3-{{ getenv &quot;SWIGGY_CACHE_VARIANT&quot; }}-{{ getenv &quot;GIT_BRANCH&quot; }}<br># npm restore/save<br>{{ .OS }}-{{ .Arch }}-npm-cache-{{ checksum &quot;**/package-lock.json&quot; }}</pre><p>We normalized cache variants (release, releaseDebuggable) and gated cache saves to the default branch to reduce fragmentation and cost overhead:</p><pre># Decide cache variant and whether to save cache<br>cache_variant=&quot;prod&quot;<br>if [ &quot;$SWIGGY_APK_APP_VARIANT&quot; = &quot;release&quot; ] || [ &quot;$SWIGGY_APK_APP_VARIANT&quot; = &quot;benchmark&quot; ]; then<br>  cache_variant=&quot;release&quot;<br>elif [ &quot;$SWIGGY_APK_APP_VARIANT&quot; = &quot;releaseDebuggable&quot; ]; then<br>  cache_variant=&quot;releaseDebuggable&quot;<br>fi<br>envman add --key SWIGGY_CACHE_VARIANT --value &quot;$cache_variant&quot;<br><br>save_cache=false<br>default_branch=develop<br>if [ &quot;$GIT_BRANCH&quot; == &quot;$default_branch&quot; ]; then<br>  save_cache=true<br>fi<br>envman add --key PROJECT_DEFAULT_BRANCH --value &quot;$default_branch&quot;<br>envman add --key SWIGGY_SAVE_CACHE --value &quot;$save_cache&quot;</pre><p>Early lesson: continuously saving branch-level cache added overhead that often outweighed benefits on our credit-per-minute plan; using develop/release caches (refreshed automatically on back-merges via GitHub Actions) gave better ROI.</p><h4>Pricing plan tradeoff</h4><p>Under the credit-per-minute plan (2 credits/min), the 1–1.5 minutes (sometimes ~3 minutes) to save cache increased direct cost, so we avoided branch-level cache saves. Once we moved to a per-build plan, time overhead wasn’t billed linearly, and we started saving branch cache post-build.</p><h4>Post-Apple Silicon refresh</h4><p>After moving to Apple Silicon, we set the base branch cache as the primary restore, then save a branch-level cache after the first run so subsequent, same-branch builds consistently hit <strong>single-digit minutes</strong> when changes are localized.</p><h3>Optimization 2: Gradle Remote Cache (Module-Level Reuse)</h3><p>CICD archive cache staled quickly as more modules changed, so we piloted a <strong>Gradle Remote Cache</strong> on a small <strong>EC2</strong> (Linux, 8GB RAM, 256GB SSD) serving GET/PUT under /cache.</p><p>Results from the POC:</p><ul><li>Incremental builds: cache hit is now P50 = 44% and P90 = 56% (latest),</li></ul><p>This raised incremental hit rates and stabilized day-to-day developer builds.</p><h3>Optimization 3: Build Only What Matters (Variants + DAG)</h3><p>We enforced building only required release variants in CI, <strong>cutting total Gradle tasks by ~26% (3399 → 2745)</strong>.</p><p>We also reduced inter-module coupling across 100+ modules to widen parallelism and shorten the critical path — an ongoing effort with tangible impact.</p><h3>Optimization 4: Repository Content Filtering</h3><p>On clean builds, Gradle’s multi-repo probing added up. We introduced <a href="https://docs.gradle.org/current/userguide/filtering_repository_content.html"><strong>repository content filtering</strong></a> so Gradle fetched coordinates from the correct repo without trial-and-error, reducing resolver latency for our large graph.</p><h3>Optimization 5: Right-Size the JVM for CI</h3><p>We ran controlled trials and converged on <strong>Heap = 16GB</strong> and <strong>Metaspace = 12GB</strong> for 32GB runners, balancing GC frequency and pause time while avoiding OOMs.</p><p>Representatively, under the develop/release cache plan, end-to-end times routinely landed around ~20–22 minutes when cache hit was healthy.</p><p>We also disabled <strong>configuration cache</strong> in CI after build scans showed ~30 seconds spent saving a cache that’s not reused in ephemeral CI — useful locally, wasteful in pipelines.</p><h3>Optimization 6: Machines That Don’t Fight You</h3><p>We moved from Ubuntu 22.04 L to XL to eliminate OOMs; Bitrise’s SOC update (Google → AMD) improved times, and M1 Max trials reached ~15 minutes with availability caveats at the time.</p><p>We’ve since moved critical workflows to Apple Silicon (M4 Pro, 54GB), which tightened variance and removed memory headroom issues seen on earlier x86 profiles.</p><h3>Toolchain Modernization</h3><p>We bumped <strong>Kotlin</strong>, <strong>AGP 8.10.1</strong>, and the <strong>Gradle wrapper 8.11.1</strong>. Combined with the refreshed caching approach on Apple Silicon, this pushed subsequent, small/localized builds into single-digit minutes.</p><h3>Results</h3><p><strong>Baseline vs improvements:</strong></p><ul><li><strong>Before optimizations on x86</strong>: P90 ≈ 44m, P50 ≈ 35m.</li><li><strong>After pipeline optimizations on x86</strong>: P90 ≈ 26m, P50 ≈ 23m; average credits per build 55 → 43 (~20% lower); ~37% monthly reductions in both credits and minutes; avoided a $22k/year tier-upgrade.</li><li><strong>After Apple Silicon</strong> (M4 Pro, 54GB) and cache/toolchain refresh: P90 = 11m, P50 = 10m.</li></ul><p><strong>Additional visible effects:</strong></p><ul><li><strong>“Time to start build”</strong> dropped from ~4–5 minutes to 30–90 seconds depending on cache source.</li><li>In a comparable live window after rollout, we saved ~5800 credits (~45%).</li></ul><h3>What We’d Do Again (Best Practices You Can Lift)</h3><ul><li><strong>Use branch-aware, tiered cache keys</strong> and avoid saving branch caches on every run under per-minute billing; refresh authoritative develop/release caches automatically on back-merges.</li><li><strong>Adopt a Gradle Remote Cache</strong> for large modular repos to stabilize incremental builds and raise hit rates.</li><li><strong>Trim the DAG ruthlessly</strong>: enforce required variants only; stub or disable non-essential modules in CI to cut tasks.</li><li><strong>Constrain dependency lookups</strong> with repository content filtering to reduce resolver overhead.</li><li><strong>Tune the JVM to the runner</strong> and validate with CI trials; target predictable GC over edge-of-OOM settings.</li></ul><p><strong>Right-size machines</strong> and prefer Apple Silicon for Kotlin/AGP-heavy builds where available to compress build times.</p><h3>Acknowledgements</h3><p>I am Pratik Sahu from the Android Mobile team at Swiggy and I’d like to thank <a href="https://medium.com/u/723d70adf627">Sambuddha Dhar</a>, <a href="https://medium.com/u/1bf9bdb89775">Raj Gohil</a> and <a href="https://medium.com/u/400467f4ffab">Tushar Tayal</a>. This milestone wouldn’t have been possible without their constant help and support</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff93aec82f05" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/scaling-android-ci-from-44-10-minutes-a-deep-dive-into-build-time-optimization-and-best-ff93aec82f05">Scaling Android CI: From 44 → 10 minutes — A Deep Dive into Build Time Optimization and Best…</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Nostalgia, Coupons, and a State Machine: The Swiggy Mother’s Day Story]]></title>
            <link>https://medium.com/swiggy-bytes/nostalgia-coupons-and-a-state-machine-the-swiggy-mothers-day-story-8e9a4df15a8f?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/8e9a4df15a8f</guid>
            <category><![CDATA[swiggy]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[swiggy-engineering]]></category>
            <category><![CDATA[rive]]></category>
            <dc:creator><![CDATA[Taruun Malik]]></dc:creator>
            <pubDate>Thu, 09 Jul 2026 05:53:58 GMT</pubDate>
            <atom:updated>2026-07-09T05:53:56.773Z</atom:updated>
            <content:encoded><![CDATA[<p>You did something wrong as a kid. You knew you weren’t supposed to. And somewhere in the house, your mom just found out.</p><p>Your heart is going faster than it ever has. You’re calculating exit routes. And then you hear it: the slap of a chappal being picked up. You’ve accepted your fate. You’re bracing for impact. And then it lands. BOOM.</p><p>Every Indian kid has lived this moment. That cocktail of dread and love that only moms can brew. This Mother’s Day, Swiggy wanted to bottle that feeling. Not as a greeting card, not as a discount banner, but as something you could actually play with.</p><h3>What We Were Actually Building</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/384/0*WBiSU_UUfahKo77e.gif" /><figcaption>User experience — Powered by rive and engineering</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QvB8d4TgqLftJGDaqfzSNA.png" /><figcaption>Dynamically bound Ledger &amp; controlled rive states and gratification on successful scan</figcaption></figure><p>We built a game around mom’s love and nostalgia. Land on the page, click a photo of the iconic mom weapons from your childhood — the jhadu, the chappal, the wooden spoon, the belt — scan them, and unlock coupons. The more you find, the more you unlock. Share the fun with friends and get cashback straight to your account. Simple concept, deeply satisfying to play, especially when you’re the one holding the chappal for once.</p><p>Simple to play. Not so simple to build.</p><p>The moment you land, a fully animated mom is already running behind her kid, ground moving, clouds drifting, the whole scene alive. Every weapon you scan triggers a unique state. Every coupon unlocked reflects a real value pulled from the backend. The share page at the end isn’t a static image either; it assembles itself dynamically based on the amount you unlocked.</p><p>Everything is live. The coupon count, the reward values, how much of the final reveal to show, all driven by real-time backend data. Nothing is static, nothing is pre-baked.</p><p>When your product manager first walks you through a brief like this, you start thinking: this is going to be a very long sprint. The kind of project that sounds deceptively simple until you start pulling the thread.</p><p>Rive is why it isn’t.</p><h3>What Is <a href="https://rive.app/">Rive</a> exactly?</h3><p>Before we get into how we used it, a quick primer for anyone who hasn’t encountered it yet.</p><p>Rive is a tool for building interactive animations that run in real-time — not pre-rendered videos, not GIFs, not something that just plays on loop and calls it a day. Animations that actually listen. To user input, to backend data, to whatever state your product is in at that exact moment.</p><p>The thing that makes Rive genuinely different is the state machine baked into its core. Most animation tools give you a timeline. You choreograph what happens, export it, and it plays back exactly as designed. Rive gives you logic. You can define states, you can define what triggers a transition between them, and the animation responds accordingly — at runtime, in real-time, based on what’s actually happening in your product.</p><p>Rive is really two things working together:</p><ul><li><strong>Rive Editor</strong>: is the designer’s territory. The interface will feel familiar if you’ve spent time in Figma, with a bit of After Effects mixed in. But the job here isn’t prototyping, it’s building animations that are production-ready. Designers own the motion logic: when something appears, what triggers it, how it responds to state changes. By the time a Rive file reaches engineering, it isn’t a reference to be recreated. It’s the real thing.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*_d-kftfs71UXbHPS" /></figure><ul><li><strong>Rive Runtimes</strong>: is where engineering picks it up. Lightweight libraries for whatever platform you’re building on — web, iOS, Android, Flutter. The integration is genuinely minimal. You load the file, connect your data to the right properties, and Rive handles the rest.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*3z7ku4d-lxPilngd" /></figure><p>What this creates in practice is something that’s surprisingly rare on a product team: design and engineering moving independently without blocking each other. Design owns the visual logic and states. Engineering owns the integration. The file is the handoff, and it’s a two-way street.</p><h3>Why Rive, and Why Not Lottie?</h3><p>We’ve worked with Lottie before. It’s a great tool for what it does. You export an animation and it plays back on screen. Clean, reliable, predictable.</p><p>Lottie is linear by design. It plays a timeline, and that’s the entirety of what it does. The second your animation needs to reflect state, respond mid-interaction, or branch on a condition, you’re no longer animating; you’re manually managing playback as a substitute for logic. Lottie works beautifully when the world is static. When the world isn’t, it doesn’t bend with you.</p><p>Rive is built differently. It has a state machine at its core. Animations aren’t just timelines, they’re logic. And it supports real-time data binding natively through something called ViewModel properties: named fields inside the animation that your code can write to directly.</p><pre>// autoBind: true is the magic flag that connects your ViewModel to Rive<br>const { rive, RiveComponent } = useRive({<br>  src: riveSrc,<br>  stateMachines: &quot;State Machine 1&quot;,<br>  autoplay: true,<br>  autoBind: true,<br>});<br><br>const viewModel = useViewModel(rive, { name: &quot;ViewModel1&quot; });<br>const viewModelInstance = useViewModelInstance(viewModel, { rive });<br>// From here, it&#39;s just: get the property, set the value. Rive does the rest.<br>const totalWinText = useViewModelInstanceString(&quot;winSubtext&quot;, viewModelInstance);<br>totalWinText.setValue(data.totalWinText);</pre><blockquote><em>Pass it a value. Rive handles the rest. That simplicity is deceptive; there’s a lot of sophistication underneath it.</em></blockquote><p>Beyond the state machine, Rive also handled cross-platform consistency and dynamic asset swapping out of the box — two things that would have added significant complexity had we rolled our own solution.</p><h3>What It Actually Changed for Our Team?</h3><p>Here’s the shift that still surprises us most: design doesn’t need to come back to engineering for every visual tweak.</p><p>Normally, if something looks slightly off, a label in the wrong place, a transition that feels too fast, an element that needs to hide under a specific condition, that requires dev work. Back-and-forth. Context switching. Waiting.</p><p>With Rive, design and engineering each have a clear lane, and neither is blocked waiting on the other. The visual logic lives inside the Rive file. Engineering focuses on passing the right data into the right property names. Both sides move independently.</p><p>And when we say visual logic, we mean a lot more than tweaking colors or timing. Design can control when the coupon value appears above a brick on the landing page, how much of the mother’s glare gets revealed as you progress, even when to pause the first time user experience (FTUE) animation if another gratification was happening on screen. All of this sitting cleanly inside the Rive file, zero engineering involvement.</p><p>The back-and-forth that typically eats a campaign timeline mostly disappears. It is, genuinely, plug and play.</p><p><em>That’s not us being generous. That’s what it is.</em></p><h3>Getting Here Wasn’t Instant</h3><p>The first time our team used it, on a Holi Blast campaign before Mother’s Day, we learned that the hard way.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/0*NtmShKPsesuSCvKu.gif" /></figure><blockquote><em>Holi Blast Rive implementation — our first attempt at wiring motion to logic</em></blockquote><p>The shift is that Rive asks your motion designer to think like an engineer.<em> </em>State machines, conditions, logic branches — these aren’t concepts that come naturally from a timeline-based animation background. On our Holi Blast campaign, design wasn’t just choreographing motion anymore; the team was building logic. Mapping out when a label should appear, what state triggers it, what happens when the data arrives out of order. That’s a real transition, and it takes time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*N-Dz7ODnIe4YKLrH.png" /><figcaption>A look under the hood at state machine handling in Rive</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/926/0*KUEUxw69JxijvT8m.png" /><figcaption>Conditional checks on how Rive verifies user interaction</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/596/0*eT6bnVpb6PhszPWG.png" /><figcaption>Different viewModel properties: Heart of a state machine</figcaption></figure><p>On the engineering side, the integration itself is lightweight, but you need to understand how ViewModels and state machines connect before things start making sense.</p><p>Early on, we hit a wall: assets loading, data flowing, code clean, but nothing showing on screen. After hours of going back and forth, we found it. The ViewModel wasn’t linked to the artboard inside the Rive file itself. One missed connection, completely invisible from the code. We linked it, loaded the new file, hit refresh, and everything appeared. Thirty seconds to fix. Hours to find.</p><p>That kind of problem only gets solved when both teams understand the tool together. Which is probably the most important thing we took from that first project. The Holi Blast pain was, in hindsight, exactly the investment the Mother’s Day experience needed.</p><h3>One Technical Thing Worth Knowing</h3><p>There was one specific gotcha worth passing on. Certain triggers, specifically label show/hide behaviors, weren’t updating in real-time the way we expected when wired to numerical inputs.</p><p><strong>Before:</strong></p><pre>const revlabel4 = useViewModelInstanceNumber(&quot;revealLabel4&quot;, viewModelInstance);<br>const earned = landingPageData.progress?.cashback?.earned;<br><br>revlabel4?.setValue(earned); // raw number — label never responded</pre><p>The label bound to revealLabel4 wasn&#39;t responding live. The value was updating correctly on the React side, but the state machine transition conditioned on that boolean wasn&#39;t picking it up in real-time — because earned was a number, and we were implicitly coercing it at the point of setValue rather than binding a proper boolean input from the start.</p><p><strong>After:</strong></p><pre>const revlabel4 = useViewModelInstanceBoolean(&quot;revealLabel4&quot;, viewModelInstance);<br>const targetscore4 = useViewModelInstanceString(&quot;label4Text&quot;, viewModelInstance);<br><br>useEffect(() =&gt; {<br>    if (!targetscore4 || isEmpty(landingPageData)) return;<br>    const earned = landingPageData.progress?.cashback?.earned;<br>    if (isEmpty(earned)) return;<br><br>    revlabel4?.setValue(earned !== 0); // boolean — transition fires correctly<br>    targetscore4.setValue(format(&quot;₹{0}&quot;, earned));<br>}, [targetscore4, landingPageData, revlabel4]);</pre><p>The fix was straightforward once we saw it. The state machine transitions inside Rive are typed against whatever the designer wired in the editor. Pass the wrong type and the transition silently never fires.</p><p>Small in isolation, but it’s the kind of edge case that bites you at 11pm before a campaign launch. Map your trigger types carefully before you wire everything up.</p><h3>What We’d Have Built Instead (And Why I’m Glad We Didn’t)</h3><p>The running mom animation alone — character in motion, parallax background, clouds, ground, would have been a significant custom engineering effort without Rive. Sprite sheets, canvas rendering, frame management. A few weeks, realistically, not days.</p><p>Stack the dynamic share page reveal, the real-time coupon states, and the personalized reward image on top of that, and you’re looking at a completely different project scope. A different timeline. A different sprint.</p><p>With Rive, design authored all of it. We connected the data. The experience shipped feeling polished, reactive, and considered in ways that the actual code behind it wouldn’t suggest. A lot of what would typically live in engineering — the transitions, the state logic, the micro-interactions — lived inside the Rive file instead. That’s the best compliment you can give a design tool.</p><h3>If Your Team Is Thinking About Rive</h3><p>Here are the tips we’d share with teams starting out:</p><p><strong>Designers:</strong> Go through the Rive 101 tutorials on YouTube before opening your production file. Rive will ask you to think about logic, not just how something looks, but when it appears, what triggers it, what state it’s in. That’s a real shift. The tutorials prepare you for it.</p><p><strong>Developers:</strong> Read the Rive runtime docs for your platform. The integration is genuinely lightweight, but understanding how ViewModels and state machines connect will save you from staring at a blank screen wondering why nothing is rendering.</p><p><strong>Both:</strong> Spend time on the Rive Marketplace before you start. It shows you the ceiling of what’s possible and often sparks ideas you wouldn’t have had otherwise.</p><h3>Would We Do It Again?</h3><p>Yes. And we say that having felt the learning curve.</p><p>The first integration is always the hardest. But what’s on the other side is worth getting to: a design team that owns the motion logic, an engineering team that just passes data, and an experience that updates in real-time without either side blocking the other.</p><p>The day we shipped, the devs were only sending numbers and strings from the backend. Everything else, the animation, the logic, the feel, was Rive. And somewhere in the world, a kid is picking up a virtual chappal and feels that old familiar dread.</p><p>For every mom who ever made us run — this one was for you. Happy Mother’s Day.</p><p><strong>To the team that made it possible</strong>❤️🙏<strong>:</strong></p><p><strong><em>Engineering</em></strong><em>: Dalchand Sharma, Taruun R Malik, Rooheen Garg, Rahul Dhawani<br></em><strong><em>Design</em></strong><em>: Srushti Haware, Ezaz Ansari, Spardha, Vishnu Madhavan and the rest of the team</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8e9a4df15a8f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/nostalgia-coupons-and-a-state-machine-the-swiggy-mothers-day-story-8e9a4df15a8f">Nostalgia, Coupons, and a State Machine: The Swiggy Mother’s Day Story</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Production-Scale Visual AI for Claims and Returns at Swiggy Instamart]]></title>
            <link>https://medium.com/swiggy-bytes/building-production-scale-visual-ai-for-claims-and-returns-at-swiggy-instamart-a0918a0abf28?source=rss----9556560f659---4</link>
            <guid isPermaLink="false">https://medium.com/p/a0918a0abf28</guid>
            <dc:creator><![CDATA[Harshit Juneja]]></dc:creator>
            <pubDate>Tue, 16 Jun 2026 03:53:42 GMT</pubDate>
            <atom:updated>2026-06-16T03:53:41.241Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y2l5aEuAGMn_GHtjvu5VSA.png" /></figure><h3>Introduction</h3><p>If you have ever filed a complaint on an e-commerce app — uploaded a photo of a damaged packet, reported a wrong item, or flagged an expired product — you were probably expecting a quick, fair resolution. You were not thinking about what happens on the other side. But at scale, every wrong call on the other side either costs the business money or costs the customer their trust, and both compound fast.</p><p>At Swiggy Instamart, that single action triggers a chain of decisions that touches customer trust, financial exposure and fraud risk — all at once. We process more than <strong>40,000 such claims every day</strong> across five complaint categories: wrong items, expired items, bad quality, damaged items, and packaging issues.</p><p>Each claim follows the same pattern: a customer reports an issue, uploads a photo, and waits. <em>Sometimes patiently. Usually not</em>. Before automation, every claim that our rule-based checks and fraud detection models could not resolve was routed to a human agent. The agent would visually compare the customer’s image against the catalog, read the customer’s description, cross-reference order details, and make a judgment call — approve the claim, or deny it.</p><p>This worked, but it did not scale: agent costs grew linearly with claim volume, resolution times stretched during peak hours, and inconsistency between agents meant identical claims could get opposite outcomes. There was also a second problem downstream. For claims that required returns, delivery executives (DEs) were expected to verify whether the returned item actually matched the customer’s complaint, often while rushing between deliveries. In practice, most returns were accepted without meaningful verification — leading to inventory leakage.</p><p>This is the story of how we built two <a href="https://en.wikipedia.org/wiki/Vision-language_model">Vision Language Model (VLM)</a> powered systems that solved both sides of this problem — while being deliberate about where we allowed the model to reason and where we kept the system deterministic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i_neVG_Az3_vBIsGF8pB4g.png" /><figcaption>*IVM: Image Validation Model</figcaption></figure><h3>The Operational Challenges Behind Claims and Returns</h3><h4>On the Claim Side</h4><p>The existing system had a clear gap. Rule-based checks (fraud signals, internal policies, compliance filters) covered a minority of claims. But for the rest — especially those involving visual evidence , the only path was a human agent.</p><p><strong>That created three recurring failure modes:</strong></p><ul><li><strong>Inconsistency</strong> — Two agents looking at the same blurry photo of a dented can would reach different conclusions.</li><li><strong>Latency</strong> — Customers waited minutes for decisions on a platform designed for instant delivery.</li><li><strong>Scale</strong> — At the scale we operate, even marginal improvements in bot resolution rates translate to meaningful cost savings.</li></ul><p>The opportunity was clear: If a model could reliably identify the obviously valid cases, we could provide instant resolution without agent intervention — and only route genuinely ambiguous claims to humans.</p><h4>On the Return Side</h4><p>The problems on the return side were equally structural but different in nature:</p><ul><li><strong>Operational friction</strong> — DEs are optimised for speed and delivery, not quality assurance. Asking them to cross-reference and verify returned items adds complexity to their expectations.</li><li><strong>Incentive misalignment</strong> — DEs are primarily measured and rewarded on how quickly they complete pickups and deliveries. Verifying returned items requires additional time and effort, but the workflow does not explicitly incentivise that behaviour. As a result, speed naturally takes precedence over thorough inspection.</li><li><strong>Fraud exposure</strong> — Without verification, customers could return incorrect or used items, and DEs could accept items without accountability.</li></ul><p>Approximately <strong>10–15% of returned orders were incorrect or in lesser quantity</strong>. That is real inventory leakage at scale.</p><p>The old RQC process relied on the DEs making a subjective decision — which, given the constraints above, was not working. We needed to move the verification decision away from the DEs entirely, into a system that could verify visually without requiring training or judgment from the person on the ground.</p><h3>Scope: Two Systems, One Loop</h3><p>We built two systems that work in tandem across the claim lifecycle:</p><p><strong>Image Validation Model (IVM)</strong> runs when a customer files a claim. It analyses the uploaded images against catalog images (with context of other signals too, like the comments which the customer has written) and returns a structured verdict: valid, invalid, or ambiguous. That verdict feeds into a business rules engine alongside other signals (fraud scores, confidence levels etc.) to make the final approve, reject, or escalate decision.</p><p><strong>Reverse Quality Check (RQC)</strong> fires after a claim has been approved and a return pickup is triggered. When the DE arrives at the customer’s doorstep and photographs the item, RQC compares those photos against the customer’s original claim images. If they match, the pickup proceeds. If they do not, the DE receives a specific nudge message explaining what to fix and can retry.</p><h4>High-Level Architecture</h4><p>The diagram below shows how both systems fit into the overall claim lifecycle. IVM and RQC share a common design philosophy: the model handles visual understanding, while deterministic code handles all logic, routing, and business decisions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0Nvb42ynHz0KSX_x7T0gYA.png" /></figure><h3>IVM: The Image Validation Model</h3><h4>Why Visual Evidence Must Come First</h4><p>The central design principle for IVM was simple but critical: <strong>the customer’s uploaded image is the primary evidence. Everything else — comments, product name, category — is context, never proof.</strong></p><p>This principle emerged from studying how the old system failed. Earlier versions of IVM would sometimes approve claims based on a compelling written narrative alone, without sufficient visual evidence to support it. A customer claiming “the yogurt tasted sour” with a photo of a perfectly sealed container tells us nothing visually. Taste is not verifiable from an image. A customer claiming “item was damaged” with a dark, blurry photo tells us the evidence is insufficient, not that the claim is false.</p><p>The model needed to be more disciplined than a human in this specific dimension — <strong><em>it should never validate what it cannot see</em></strong>.</p><h4>Why One Prompt Cannot Fit All Five Categories</h4><p>Early on, we tried a single generic prompt; something on the lines of “look at this image and decide if the claim is valid.” It performed poorly across the board.</p><p>The reason became clear quickly. What constitutes valid evidence for a wrong item claim (visual identity mismatch between two products) is fundamentally different from what constitutes valid evidence for an expired item claim (a date printed on a label). These are different visual reasoning tasks that require different evaluation criteria, different failure modes to watch for, and different output structures.</p><p>So we decomposed IVM into a <strong>LangGraph StateGraph</strong> — a directed workflow that routes each claim to a specialised handler based on its category. The routing is deterministic; only the visual assessment within each handler uses the LLM.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NP0mfHFlQGp6aHS5lJ58Kw.png" /></figure><p>Every claim flows through four stages:</p><ol><li>Ingesting and pre-processing the request</li><li>Downloading customer and catalog images</li><li>Routing the claim to the appropriate handling mechanism</li><li>Finally aggregating the outputs into a standard decision format.</li></ol><h4>What Each Category Handler Does Differently</h4><p><strong>Bad Quality, Damaged and Packaging</strong> — These three share a common architecture: compare the customer image against the catalog image to assess physical condition. The core principle is that visual evidence always takes precedence over customer narrative. Non-visual claims (smell, taste, “device not powering on”) are classified as out of scope.</p><p><strong>Expired Items</strong> — Focused on extracting manufacturing and expiry dates from product labels. The hardest vision task in the system — labels are rotated, printed in small fonts, in regional languages, and often partially obscured.</p><p><strong>Wrong Items</strong> — Compares visual identity between what was ordered (catalog image) and what arrived (customer image). Special handling for categories where catalog images may not be representative of the product, and explicit rules against marking claims valid for quantity, ripeness, or quality issues that are not actually wrong-item scenarios.</p><h4>The Expiry Challenge: Why Two Extraction Paths Are Better Than One</h4><p>Extracting dates from product labels turned out to be unreliable with a single approach. VLMs are impressive at reading text in images, but they occasionally hallucinate dates or misread characters in small, low-contrast label text. This matters because for expiry claims, the entire decision hinges on correctly reading a printed date.<em> </em>A single wrong character changes “2026” into “2028” and transforms an expired product into a valid one.</p><p>To maximise both precision and recall, we run <strong>two extraction paths in parallel</strong> for every expiry claim:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yUbZgj9lo6qw6jRo3pq6yA.png" /></figure><p><strong>Path 1 (VLM Direct):</strong> GPT-4o receives the customer image and attempts to read manufacturing and expiry dates directly from the label.</p><p><strong>Path 2 (OCR + LLM):</strong> AWS Textract runs OCR on the image, producing a raw text string. A second GPT-4o call then parses this text with a specialised prompt that handles misspelled labels, computes expiry dates, also helps in disambiguating when multiple dates are present.</p><p><strong>Merging Logic:</strong> OCR-extracted dates take priority over VLM-extracted dates, because OCR is empirically more reliable for reading small printed text, which is also confirmed by our internal audits of the extracted dates.</p><h4>Multi-Image Aggregation: Where Deterministic Code Meets LLM Output</h4><p>Customers often upload multiple photos for a single claim. Rather than asking the LLM to reason across all images simultaneously (which increases hallucination risk and prompt complexity), we process each image independently through the model and then apply <strong>deterministic aggregation rules in code</strong>:</p><ul><li>If <em>any</em> image shows valid evidence, the claim is treated as valid.</li><li><strong>Image relevance guardrail:</strong> if the aggregated outcome is “valid” but <em>none </em>of the images were deemed relevant to the claimed issue, it overrides to invalid.</li><li><strong>Cross-category detection:</strong> if the model consistently classifies all images as belonging to a <em>different</em> category (e.g., filed as “bad quality” but every image shows a wrong item), the claim is flagged as miscategorised.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uVNuRvcPN2Fuurob8rAwZQ.png" /></figure><p>This was a deliberate architectural choice. The LLM does what it is uniquely good at — visual understanding and nuanced judgment. Code does what code is good at — aggregation, boundary conditions, and consistency. The aggregation logic is testable, debuggable, and perfectly deterministic. Putting it inside an LLM call would make it probabilistic and harder to audit. <strong>In short, we use the LLM as the artist and the code as the accountant — because letting the artist handle the taxes never ends well.</strong></p><h4>Model Selection: The Experiment That Shaped Our Architecture</h4><p>IVM started with GPT-4o across all categories. As we accumulated production data, we benchmarked Gemini 2.5 Flash to evaluate whether we could reduce cost and latency without sacrificing accuracy.</p><p>The results were clear for visual categories: Gemini matched or exceeded GPT-4o at significantly lower cost and latency. The table below shows production error rates across all visual categories:-</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OVHr79kQYDCKIDVa6UQGpg.png" /></figure><p>Gemini consistently delivered lower error rates across both FP and FN, while also being cheaper and faster per call.</p><p>For Expired Items, the story was different: GPT-4o proved more reliable on expiry-date extraction, where reading small printed text accurately mattered more than speed. A model that confidently reads ‘MFD: 2022’ as ‘MFD: 2026’ is not helping anyone, least of all the customer who just ate that yogurt</p><p>The<strong> current routing </strong>is<strong>:</strong></p><ul><li><strong>Gemini 2.5 Flash</strong> → Bad Quality, Damaged, Packaging, Wrong Items</li><li><strong>GPT-4o</strong> → Expired Items</li></ul><p>All of this matters only if the system can respond fast enough in real-world conditions. Customers expect near-instant resolutions, and DEs cannot stand outside a doorstep waiting for a slow model response.</p><h4>Deployment</h4><p>IVM runs as a real-time serving endpoint. The moment a customer submits a claim, it handles fetching images, running the model on each one in parallel, aggregating the results, and returning a decision, all within seconds. Full tracing (through <a href="https://mlflow.org/docs/latest/genai/tracing/">MLFlow</a>) gives us visibility into every step of that process: which category handler ran, what the model saw in each image, how much it cost, and where it failed when something went wrong. That observability is what makes continuous improvement possible at this scale. We have also set up alerting jobs that trigger when the error rate exceeds a predefined threshold within a specified time period. This is essential for aggregate-level monitoring.</p><h3>RQC: Reverse Quality Check</h3><h4>The Verification Problem</h4><p>After a claim is approved and a return is initiated, the DE arrives at the customer’s doorstep to pick up the item. The DE photographs the product. And now the system has to answer one question:</p><blockquote><em>Is the item the </em>DE<em> is picking up the same physical product the customer showed in their claim images?</em></blockquote><p>This is not a trivial image comparison. The customer’s photo was taken indoors, from one angle. The DE’s photo is taken outside — often in harsh sunlight or a dim stairwell — from whatever angle their back pain and delivery schedule allow. The same blue towel looks navy in one photo and sky blue in another. The same sealed packet looks flawless in customer lighting and dented under the DE’s flash. <strong>A lot of deliveries basically turn into that viral “</strong><a href="https://en.wikipedia.org/wiki/The_dress"><strong>is the dress blue or gold</strong></a><strong>” debate from 2015.</strong></p><p>The system needs to distinguish between:</p><ul><li><strong>Genuine mismatches</strong> — the customer is trying to return a different (or older or cheaper) item</li><li><strong>Apparent mismatches caused by environment</strong> — same item, different lighting, angle, or packaging state</li><li><strong>Genuinely unverifiable scenarios</strong> — DE photographed a sealed opaque bag, or the image is too blurry to verify</li></ul><h4>How a Return Verification Flows Through RQC</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t0e9A4cEogZ7orqPkX6ipg.png" /></figure><p>RQC runs as an <strong>AWS Lambda</strong> function, triggered the moment the DE uploads all the photos:</p><p>1. We download both image sets (customer images and DE images), and construct a single prompt with all images.</p><p>2. The VLM evaluates and returns a structured response: Match, Not Match, or Ambiguous — along with a confidence score and reasoning.</p><p>3. The result is returned to the DE’s app in real time.</p><p>4. If Not Match or Ambiguous: the DE receives a contextual nudge message and can retry with a new photo.</p><h4>Model Selection</h4><p>We benchmarked three models for RQC on an evaluation set with human-verified labels. Gemini 2.5 Flash led on accuracy while also being the cheapest and fastest option. GPT-4o came close on accuracy, but at 13x the cost and significantly higher latency, while Claude Sonnet 4 trailed on both accuracy and cost efficiency.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qnexBr0hkJbOu4iYY6fiXA.png" /></figure><p><strong>Gemini 2.5 Flash</strong> won on the accuracy-cost-speed trifecta. It handled multi-angle and partially visible product images well by relying on colour, shape, and text cues together.</p><p><em>The caveat</em>: it occasionally overanalyses subtle visual noise — lighting reflections or glossy textures can tip it into a wrong call.</p><p><strong>GPT-4o</strong> was reliable and consistent but too slow at P90 (30.6s) for real-time use, and 13x costlier per claim. It also follows stricter content policy filters — refusing to process certain categories (e.g., tobacco items) — which creates coverage blind spots.</p><p><strong>Claude Sonnet 4</strong> was the fastest overall, but its visual reasoning fell well short — frequently misidentifying objects entirely (e.g., a badminton racket as a stick), making it unsuitable for precision product matching. Our confusion matrices confirmed this: Claude produced nearly 3x more false “Not Match” calls than Gemini on the same evaluation set.</p><p>The choice was clear: Gemini 2.5 Flash went to production.</p><h4>The Prompt: 11 Core Principles</h4><p>Unlike IVM (which compares against a <em>canonical</em> catalog image), RQC compares two <em>real-world</em> photos — both imperfect, both taken in uncontrolled environments. The prompt encodes 11 principles that emerged directly from <strong>production cases</strong>. A few key ones:</p><ul><li><strong>Focus on core invariants</strong> — Branding, logos, shape, and fundamental product design. Not colour (which shifts with lighting) or minor text (often unreadable in phone photos).</li><li><strong>Ignore low-confidence features</strong> — Colour differences from lighting, partial text, condensation, and minor packaging creases must not trigger a “Not Match.”</li><li><strong>Packaging state invariance</strong> — Product inside packaging in one photo and outside in another is still a Match if the core item is identifiable.</li></ul><p>Each of these principles exists because of a specific failure pattern we observed in early production. Lighting differences alone accounted for the majority of initial false “Not Match” decisions.</p><h4>The DE Nudge System</h4><p>When RQC cannot confirm a match, the DE does not just see a generic failure. They receive a <strong>specific and actionable instruction</strong> telling them exactly what went wrong and how to fix it:</p><blockquote><em>“Photo is dark or blurry. Go to a bright place and focus the image.”</em></blockquote><blockquote><em>“Item is inside packaging. Take it out and click again.”</em></blockquote><blockquote><em>“Item is inside delivery bag. Take it out and click again.”</em></blockquote><blockquote><em>“Photo angle not match. Click from the same angle as customer’s photo.”</em></blockquote><blockquote><em>“Item is different. Click photo of the same item customer reported.”</em></blockquote><p>After receiving a nudge, the DE can <strong>retry</strong> — take a new photo following the guidance and submit again. This loop continues up to 2 attempts before escalating to manual review.</p><p>The key insight: around 60% of initial “Not Match” or “Ambiguous” results aren’t caused by actual product mismatches — they’re caused by bad photos. Giving the DE specific feedback and another chance dramatically improves the final match rate, which directly affects the linked customer experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9-_0ENxDEKlQIVXy757aaw.png" /></figure><h4>Automated Prompt Optimisation</h4><p>The 11-principle prompt was not written once and frozen. As we accumulated labeled production data (human-verified Match/Not Match decisions), we adopted an <a href="https://dspy.ai/learn/optimization/optimizers/"><strong>Automated Prompt Optimisation</strong></a> pipeline to systematically improve RQC accuracy.</p><p>The approach: we define a metric which we want to improve upon (False Not Match % — cases where the system incorrectly flags a legitimate return as a mismatch), compile a labeled eval set from production, and let the optimiser iterate on prompt instructions. It refines principles against the eval set until the metric improves.</p><p>This moved our <strong>False Not Match rate from 23% down to 17%</strong> — a meaningful reduction, because every false “Not Match” means a DE is incorrectly told the item does not match, triggering unnecessary retries and friction in the return flow, at the same time impacting the customer experience too.</p><p>The key insight here is that, at a certain scale, manually iterating on prompts hits diminishing returns. You stop being able to intuit which phrasing change will move the needle. Automated optimisation treats prompt engineering as a search problem (<em>because letting gradient descent do the heavy lifting is much easier than pretending you can hallucinate the perfect adjective.)</em></p><h3>Operating at Swiggy’s Scale</h3><p>Building the model is only one part of the job. Operating it reliably at 40K+ decisions per day is equally important.</p><p><strong>Observability</strong> is what makes continuous improvement possible at this scale. Every IVM and RQC decision is fully traced: which category handler ran, what each image returned, confidence scores, cost per call, and end-to-end latency. When a wrong decision surfaces through a customer complaint or an audit, the trace gives us a complete reconstruction of exactly what the model saw and why it decided what it did. Without that, debugging at this volume is guesswork.</p><p><strong>Cost</strong> is a first-class concern. At 40K+ decisions a day, even small per-call differences compound significantly. The Gemini/GPT-4o routing split in IVM and Gemini selection for RQC were not purely accuracy decisions; each was an accuracy-cost-latency tradeoff validated against real production data, not a benchmark.</p><p><strong>Resilience</strong> is non-negotiable for RQC specifically. A DE standing at a customer’s doorstep cannot wait for a slow or failed model response. If the system is unavailable, the claim escalates to manual review rather than blocking the DE in place. The failure path is designed to be invisible on the ground.</p><p>Finally, neither system was deployed and frozen. Production data continuously feeds back into prompt refinement, eval sets, and model benchmarking. RQC launched with a False Not Match rate of 60% — the model was overcautious, flagging legitimate returns as mismatches. Iterative prompt work brought that to 23%, and automated optimisation brought it further to 17%. That trajectory is only possible if the infrastructure to capture, label, and act on production failures is built from week one.</p><h3>Results</h3><ul><li><strong>Customer Satisfaction score: </strong>up by<strong> 6</strong>%</li><li><strong>Image Validation tickets: </strong>weekly ticket volume for Image Validation has decreased by <strong>20,000</strong>. This significant reduction directly improves our time-to-resolution and elevates the overall customer experience.</li><li><strong>RQC Accuracy</strong>: 98.5%</li><li><strong>RQC Tickets solved without human intervention</strong>: &gt;5000 per day</li><li><strong>RQC Business Impact:</strong> The reverse QC system has significantly reduced fraud-related losses and inventory leakage by ensuring returned items are verified before being accepted.</li></ul><p>The system has been processing 40K+ claims daily in production for over 6 months, with continuous prompt and model improvements driving incremental accuracy gains.</p><h3>Conclusion</h3><p>The biggest shift was not that we added a VLM to the claim flow. It was that we changed the architecture from a binary “send to agent or auto-resolve by rule” into a visual evidence system with a clear division of responsibility. Different claim types get fundamentally different evaluation criteria. The model’s output is one input among many into a financial decision, not the decision itself. DEs receive actionable feedback and can retry rather than hitting a dead end. And the model stays in its lane — vision stays probabilistic, while logic, routing, and financial decisions remain deterministic.</p><p>The old system relied on humans to perform visual verification at scale. The new system relies on AI for visual reasoning, while reserving humans for genuinely ambiguous cases. The key architectural decision was knowing where not to use the model.</p><blockquote><strong>Author: </strong><a href="https://medium.com/u/5e106e29b6da"><strong>Harshit Juneja</strong></a></blockquote><blockquote><strong>Contributors: </strong><a href="https://medium.com/u/abe69fbfb436"><strong>Yash Khandelwal</strong></a>, <a href="https://medium.com/u/1b80ff5da777"><strong>Aishwary Yadav</strong></a><strong> and </strong><a href="https://medium.com/u/106433079ce3"><strong>Jayanth</strong></a></blockquote><blockquote><strong>Special Thanks to: </strong><a href="https://medium.com/u/0d0c381fa186"><strong>Prateek Saxena</strong></a>,<strong> </strong><a href="https://medium.com/u/10c3c71bd5f1"><strong>Meghana Negi</strong></a>, <a href="https://medium.com/u/3d8518cd344d">Rachna Singh</a> <strong>and </strong><a href="https://medium.com/u/e3905126c73d"><strong>Shaurya Sindhu</strong></a><strong> </strong>for their guidance throughout this project.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a0918a0abf28" width="1" height="1" alt=""><hr><p><a href="https://medium.com/swiggy-bytes/building-production-scale-visual-ai-for-claims-and-returns-at-swiggy-instamart-a0918a0abf28">Building Production-Scale Visual AI for Claims and Returns at Swiggy Instamart</a> was originally published in <a href="https://medium.com/swiggy-bytes">Swiggy Bytes — Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>