<?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[Expedia Group Tech Blog - Medium]]></title>
        <description><![CDATA[Stories from the Expedia Group tech teams - Medium]]></description>
        <link>https://medium.com/expedia-group-tech?source=rss----38998a53046f---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Expedia Group Tech Blog - Medium</title>
            <link>https://medium.com/expedia-group-tech?source=rss----38998a53046f---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 10 Nov 2026 14:50:33 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/expedia-group-tech" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[How I Taught an AI to Read My Logs]]></title>
            <link>https://medium.com/expedia-group-tech/how-i-taught-an-ai-to-read-my-logs-9c04f1cfa750?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/9c04f1cfa750</guid>
            <category><![CDATA[spark]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[apache-iceberg]]></category>
            <category><![CDATA[fine-tuning]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Vincent DANIEL]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 12:01:02 GMT</pubDate>
            <atom:updated>2026-11-10T12:01:02.199Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/data">Expedia Group Technology - Data</a></h4><h4><em>AI-assisted data engineering at Expedia Group — and less squinting at Spark execution plans</em></h4><figure><img alt="A bridge in Queensland, Australia at night" src="https://cdn-images-1.medium.com/max/1024/0*_5DV4iBoB3hVucxE" /><figcaption>Photo by Queensland, Australia on Unsplash</figcaption></figure><p>Anyone who has actually opened a Spark execution plan to figure out why a job is slow or expensive knows what the pain looks like. It’s not one screen — it’s the Spark UI’s stage DAG, then the SQL tab’s physical plan with nested Exchange, BroadcastHashJoin, and SortMergeJoin operators stacked five levels deep, then a separate executors tab to check whether any of that actually correlates with memory pressure, then cross-referencing timestamps against a job&#39;s config history to rule out &quot;did someone change something last week.&quot; Reading a plan correctly means holding all of that in your head at once, and most of it is stored in different tabs that don&#39;t talk to each other. Doing this well takes real practice, and doing it under time pressure — a pipeline failed overnight, someone needs an answer before standup — is exactly when people take the fastest wrong guess instead of the right slow one.</p><p>This is the part of the job that AI turned out to be genuinely good at, and it’s the part of the last two years worth focusing on.</p><h3>A brief origin point: Iceberg V3</h3><p>The first place this paid off concretely was migrating tables to Apache Iceberg’s V3 format after it shipped in mid-2025. The work itself was conversational — reading a table’s delete-file accumulation, reading the actual scan plan against that table, deciding whether a merge-on-read migration to deletion vectors was worth it, then checking the resulting plan again to confirm it worked. Daily compute cost on a couple of pipelines that got this treatment dropped by more than 90%. That’s a good result, but the more important thing it proved was narrower: that reading a real execution plan and matching it against real table metadata, rather than trusting what the DDL or the job config claimed, was something an AI could do reliably enough to act on. (The deletion-vector mechanics themselves are covered in more depth separately: <a href="https://medium.com/@vincent_daniel/deletion-vectors-and-puffin-files-merge-in-the-new-v3-iceberg-format-565188036d0c">Deletion Vectors and Puffin Files: MERGE in the New V3 Iceberg Format</a>.) That one-off, table-by-table exercise is what eventually got packaged into a repeatable skill — but the skill’s value was never really about Iceberg. It was about not having to read the plan by hand anymore.</p><h3>Where this actually shows up: reading the execution plan instead of guessing from configuration</h3><p>Two skills now do this reading for us — one after a job has finished, one while it’s still running — and both are built around the same premise: the plan and the execution trace tell you what happened; the config and the code only tell you what someone intended.</p><p><strong>The cost-analysis skill reads the plan after the fact, on every expensive job.</strong> For the two or three most costly jobs in a pipeline, it pulls the actual stage summary (shuffle and spill volumes, task skew), the actual executor summary (peak memory against what was allocated), and the literal SQL execution plan the engine produced — the same tabs a human would have to cross-reference by hand, done automatically and correlated in seconds instead of the better part of an hour. That turns into diagnoses that are hard to make correctly by eye:</p><ul><li><strong>Skipped stages vs. wasted stages.</strong> A stage the plan skips is a cache hit doing its job, not idle compute — easy to misread as “this stage never ran” if you’re scanning the DAG quickly, and the skill is built to recognize the difference rather than flag it as an anomaly. It’s the same judgment call discussed separately in <a href="https://medium.com/@vincent_daniel/optimizing-cte-on-apache-spark-when-to-cache-and-when-not-to-0fe764766a63">Optimizing CTEs on Apache Spark: When to Cache and When Not To</a>: a repeatedly-rescanned CTE is real cost worth fixing, but reflexively caching everything isn’t free either, and telling the two apart from a plan requires actually tracing which stage re-executes the same subtree.</li><li><strong>Near-capacity vs. already spilling.</strong> Peak executor memory at 99% of a container with zero spill and the same peak with active spill look nearly identical on a summary chart — one is a warning for next week, the other is money being spent right now. Distinguishing them means reading the actual spill metric per stage, not the aggregate memory graph.</li><li><strong>Full-scan </strong><strong>MERGE INTO detection.</strong> This is the plan-reading catch I&#39;d put ahead of the others: the skill checks whether a merge statement&#39;s target-side condition is filtered the same way its source is. A plan with an unfiltered target ON clause against a windowed source looks completely unremarkable at a glance — it&#39;s the kind of thing a human reviewer skims past — but it forces a full-table scan every run, and where a WHEN NOT MATCHED BY SOURCE clause is present, it can delete rows outside the intended window. This was caught as active, ongoing data loss on more than one pipeline during a routine cost sweep, because the skill compared the plan&#39;s actual scan boundaries against what the source windowing implied instead of trusting that the code did what its comments said. More on why this specific plan shape is so costly on Iceberg tables specifically: <a href="https://medium.com/@vincent_daniel/boost-iceberg-performance-and-cut-compute-costs-with-well-scoped-merge-statements-e0a8f702c321">Boost Iceberg Performance and Cut Compute Costs with Well-Scoped MERGE Statements</a>.</li><li><strong>Driver-side vs. executor-side memory failures.</strong> Recurring out-of-memory kills are the textbook trigger for “give it more executor memory” — a reasonable guess from the error message alone. Reading the actual heap trace changes the diagnosis: if executor heap usage at the moment of failure was near zero, the failure is on the driver side (in one case, file-transfer buffering), and more executor memory would have done nothing.</li><li><strong>JVM heap pressure vs. off-heap memory pressure.</strong> A separate pipeline kept failing with the same OOM error after someone manually doubled executor memory — because in both configurations, JVM heap usage topped out at only 11–23% of what was allocated. The memory being exhausted was Python process memory outside the JVM, invisible to anyone reading the Spark memory config and assuming that’s where the ceiling was.</li><li><strong>Stale tables, caught through metadata rather than assumption.</strong> Not every finding comes from a Spark plan — some come from asking the catalog itself a question a human would rarely think to ask on a routine pass: when did this table actually last get written to? Through a Trino MCP connection, the skill queries a table’s Iceberg snapshot history directly — no ad hoc SQL session, no remembering the right metadata table name — and checks the most recent commit timestamp against the analysis window. A table with no write in the last 30 days but ongoing shared maintenance cost is pure waste hiding in plain sight, invisible unless something actually checks the snapshot log instead of assuming a table is “probably fine” because nobody’s complained. The same Trino MCP path is used to reconcile a repo’s DDL against what genuinely exists in the live catalog — turning up tables that were dropped in one environment but never removed from source, or tables the catalog thinks are current but haven’t been touched in months. Once a table is confirmed genuinely stale or corrupted, the lowcode deployment skill takes over: registering it cleanly as a trash table with the right ownership tags before dropping it, or, for a table stuck in a corrupted metadata state, running the unregister-and-re-register recovery sequence — again pulling the current state through the same metadata queries rather than guessing at what’s safe to touch.</li></ul><p>Reading a plan well enough to catch any one of these by eye is a real, learned skill — the kind senior engineers build over years of exactly this kind of incident. Having it done automatically, correctly, and consistently on every top-cost job is what turned troubleshooting from “hope the person on call has seen this exact failure before” into a repeatable ten-minute pass. The limits get documented rather than papered over: a pre-signed dashboard URL that’s already been used needs a fresh request, and a job older than roughly 30 days has no server-side trace left to read at all — a hard platform limit, not something worth retrying around.</p><p><strong>The lowcode deployment skill reads the plan while the job is still running — and, increasingly, before it’s even committed.</strong> Rather than deploying straight to production and waiting for a downstream alert to say something went wrong, it can run the same job as a live dry run first: executing the actual statement against a non-production table or branch (the same Write-Audit-Publish pattern behind Iceberg’s branch-based validation — see <a href="https://medium.com/expedia-group-tech/chill-your-data-with-iceberg-write-audit-publish-746c9eb3db48">Chill Your Data With Iceberg: Write-Audit-Publish</a>), watching the real plan and stage progress it produces, and confirming the change behaves as expected before anything touches production data. For a risky operation — an Iceberg format upgrade, a DDL change with ownership implications — that dry run is what turns “we think this is safe” into “we watched it run safely.” Once a deployment does go to production, the skill keeps watching: it polls the running job’s stage progress every ten seconds or so for the duration of the deployment, the same live equivalent of the plan a human would otherwise only get to inspect after the fact. That’s the difference between discovering a failure an hour later and starting a fresh debugging session, versus watching it fail in the same session it was deployed in and fixing it before moving on. The same live view — from the dry run or the real deployment — is what lets it pick the right recovery procedure — a corrupted table metadata state, a write that committed conflicting deletion vectors, a stale table confirmed through the catalog checks above — from the actual job trace rather than guessing from an error message.</p><h3>What actually changed</h3><p>Not the model getting smarter — the willingness to go read the plan instead of guessing from the config, every single time, on every job that matters, instead of only when someone finally has an hour free to dig in properly. That’s the same instinct that started with one engineer squinting at a scan plan to decide whether an Iceberg table was worth migrating, now running automatically across a whole portfolio of pipelines — querying the catalog directly through Trino MCP, dry-running risky changes before they touch production, reading real execution traces, and fixing what it finds rather than just reporting it. The dollar savings and the incident catches are both downstream of that one habit, not the point of it.</p><p><em>Related reading:</em></p><ul><li><a href="https://medium.com/@vincent_daniel/deletion-vectors-and-puffin-files-merge-in-the-new-v3-iceberg-format-565188036d0c"><em>Deletion Vectors and Puffin Files: MERGE in the New V3 Iceberg Format</em></a></li><li><a href="https://medium.com/@vincent_daniel/optimizing-cte-on-apache-spark-when-to-cache-and-when-not-to-0fe764766a63"><em>Optimizing CTEs on Apache Spark: When to Cache and When Not To</em></a></li><li><a href="https://medium.com/@vincent_daniel/boost-iceberg-performance-and-cut-compute-costs-with-well-scoped-merge-statements-e0a8f702c321"><em>Boost Iceberg Performance and Cut Compute Costs with Well-Scoped MERGE Statements</em></a></li><li><a href="https://medium.com/expedia-group-tech/chill-your-data-with-iceberg-write-audit-publish-746c9eb3db48"><em>Chill Your Data With Iceberg: Write-Audit-Publish</em></a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9c04f1cfa750" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/how-i-taught-an-ai-to-read-my-logs-9c04f1cfa750">How I Taught an AI to Read My Logs</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[Beyond the Prompt: Engineering the Loop for Software Change at Scale]]></title>
            <link>https://medium.com/expedia-group-tech/beyond-the-prompt-engineering-the-loop-for-software-change-at-scale-9ac26e3b2706?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/9ac26e3b2706</guid>
            <category><![CDATA[developer-productivity]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Karthik Bedre]]></dc:creator>
            <pubDate>Tue, 27 Oct 2026 11:01:01 GMT</pubDate>
            <atom:updated>2026-10-27T11:01:01.661Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/engineering">Expedia Group Technology — Engineering</a></h4><h4>How Expedia Group compressed an estimated two years of iOS modularization into about three months while shipping weekly</h4><figure><img alt="Two people walk across a bridge" src="https://cdn-images-1.medium.com/max/1024/0*t84NxU5lNvwxXh8P" /><figcaption>Photo by Queensland, Australia on Unsplash</figcaption></figure><p>The first wave of coding assistants trained us to think in prompts: describe a task, generate code, review the result.</p><p>Reusable skills made that interaction more repeatable. They packaged instructions, conventions, and tools so an agent could perform a familiar task more consistently.</p><p>The hard part of large-scale engineering sits between code changes: hidden dependencies, incomplete context, build failures, test regressions, architectural decisions, ownership boundaries, and a codebase that keeps moving while the work is underway.</p><p>Generating a change is only the first step. The system still has to carry progress across hours of execution, repeated failures, changing state, and human decision points.</p><p>For that, we need a <strong>Loop</strong>.</p><blockquote>A prompt provides direction. A skill packages reusable capability. A sub-agent provides bounded execution and context isolation. The <strong>Loop</strong> is the operating model.</blockquote><p>At <a href="https://www.expediagroup.com/">Expedia Group</a>™, that distinction became the foundation for a major modularization program inside our Shared iOS Components Stack. Work initially estimated at roughly two years was completed in about three months. We did it without a code freeze, and our Monday release cadence stayed intact.</p><p>In one complex-domain migration, the loop ran autonomously for more than eight hours before requesting its first human architectural decision.</p><p>We got there by treating coding agents as one component of a deliberately engineered system.</p><p>Three engineering north stars shaped the program:</p><ul><li><strong>Prepare workspaces for the agentic era:</strong> create smaller, faster, isolated workspaces with clear ownership and dependency boundaries that can compile independently</li><li><strong>Shorten every feedback loop:</strong> let developers and CI build and test the smallest meaningful piece</li><li><strong>Improve developer flow:</strong> turn faster build cycles into less coding time, less waiting, and faster delivery</li></ul><h3>The goal: transform the architecture without stopping delivery</h3><p>An important distinction: our iOS repository is a monorepo, but it is not a monolith. It uses a layered, modular, white-label architecture designed for delivery at scale. Its shared foundation supports more than ten Expedia Group brands, with many domain teams contributing concurrently.</p><p>This program focused on one of its largest and most interconnected surfaces: the Shared iOS Components Stack. Teams build domain-specific experience components and logic here, then compose them into screens and end-to-end flows across brands. The stack is large, heavily reused, and always changing.</p><p>In this context, a domain is a group of packages that exposes multiple related components and experiences.</p><p>The target for each domain was explicit:</p><ul><li>Build, run, and test in a focused domain component workspace app</li><li>Depend only on what the domain requires</li><li>Expose clean protocol-based seams to its consumers</li><li>Remain composable across existing screens, flows, and brands</li></ul><p>Before the migration, domain folders lived behind one Shared Components Stack package boundary. Direct cross-domain imports were easy, but focused compilation, testing, and automated reasoning were harder. The stack had a focused all-component app for component validation, but it still compiled every domain.</p><p>The target state gave each domain an implementation package and a small API package for protocols, state, and dependency-injection entry points. Application packages could consume both. Domains could depend on one another through API packages, breaking compile-time cycles while keeping implementations decoupled.</p><p>Each domain team could instead compile and run a focused domain component workspace app containing only its components and required dependencies. That made component validation much faster locally and in CI.</p><figure><img alt="Diagram comparing before and after states: before shows Lodging, Flights, and Trips domains sharing one compilation boundary inside the Shared Components Stack with direct cross-domain access; after shows each domain paired with its own API package (protocols, state, DI), consumed only through that API seam, with a focused domain workspace app and the legacy workspace still compiling throughout." src="https://cdn-images-1.medium.com/max/1024/1*0K2atlbvCeukZr_N7_Dx6A.png" /><figcaption>One domain at a time, with the legacy workspace kept green</figcaption></figure><p>The migration contract mattered as much as the end state. There could be no code freeze, Monday releases had to continue, and product work could not stop.</p><p>This involved much more than moving files. Each migration had to expose implicit dependencies, restructure implementation code, update generated-code infrastructure, and prove compatibility at progressively wider boundaries.</p><p>Before this approach, a medium-to-complex domain could require approximately four months of incremental work. Faster typing by an engineer or an agent addressed only a small part of the problem. The end-to-end flow of work needed a redesign.</p><h3>Find the loop before designing the automation</h3><p>“Modularize this domain” is a goal, not an executable plan.</p><p>We began by studying successful migrations and identifying the sequence experienced engineers repeated. Beneath the domain-specific details was a stable control loop:</p><p><strong>Analyze → Decide → Act → Verify → Memory → Route → Repeat</strong></p><ul><li><strong>Analyze:</strong> collect facts about the domain, its size, complexity, dependencies, consumers, tests, and generated code</li><li><strong>Decide:</strong> select the next transformation using known patterns or fresh reasoning</li><li><strong>Act:</strong> change package structure, seams, sources, dependencies, and generated-code configuration</li><li><strong>Verify:</strong> compile and test at progressively wider boundaries</li><li><strong>Memory:</strong> record failure signatures and successful resolutions</li><li><strong>Route:</strong> continue automatically, invoke reasoning, or request a human architectural decision</li><li><strong>Repeat:</strong> use the new verified state as the starting point for the next cycle</li></ul><figure><img alt="Circular diagram of a seven-stage loop — Analyze, Decide, Act, Verify, Memory, Route, Repeat — arranged around a central “state machine” that controls transitions between stages." src="https://cdn-images-1.medium.com/max/1024/1*kSyP2UN4eKnvScWabllsUw.png" /><figcaption>The loop provides motion. The state machine provides control.</figcaption></figure><p>This loop became the design center for the system. Scripts, agent skills, orchestration, builds, tests, memory, and human review all existed to serve one or more stages of it.</p><p>We stopped asking how to write a better modularization prompt. Instead, we asked how to build a system that could keep modularizing safely after the initial prompt had disappeared from view.</p><h3>Design the system around the loop</h3><p>At repository scale, sending every operation through a model would have been slow, expensive, and unnecessarily variable. We separated deterministic work from judgment and connected the two through a purpose-built Model Context Protocol (MCP) server.</p><figure><img alt="Flowchart showing a modularization request flowing through an Orchestrator Entry Skill into goal and guardrails, an MCP domain analyzer producing a structured plan, and a Main Agent that applies loop semantics (Analyze, Decide, Act, Verify, Memory, Route, Repeat) to route execution to the Modularization MCP, Main Agent reasoning, or a human architect, before build-and-test evidence determines whether the loop continues or exits with a verified pull request." src="https://cdn-images-1.medium.com/max/1024/1*veScM-ZeQZQB7QQOFs2VcA.png" /><figcaption>C1: the Main Agent defines the loop semantics; the MCP manages state and deterministic execution</figcaption></figure><h4>Set the modularization goal before the loop starts</h4><p>For each domain, the Orchestrator Entry Skill set a bounded goal:</p><ul><li>The target domain and desired package and API seams</li><li>The infrastructure transformations the workflow could perform</li><li>The ordered build and test boundaries required for completion</li><li>The cross-domain decisions that required human architectural direction</li></ul><p>The Main Agent could choose how to reach the next verified state, but it could not redefine the target architecture or bypass a verification gate.</p><h4>Analyze with deterministic tooling</h4><p>Through the Modularization MCP, deterministic scripts inventoried the repository, mapped dependencies in both directions, measured complexity, and ranked migration order. The analyzer worked at the domain or portfolio level and wrote a structured findings document. No model reasoning was needed, and the agent did not have to rediscover the repository on every turn.</p><h4>Execute through the Modularization MCP</h4><p>The Orchestrator Entry Skill accepted the modularization request, established guardrails, and configured the Main Agent. The Main Agent defined the loop’s semantics and made decisions from the structured plan and MCP state, including when to continue or exit.</p><p>Guided by the analyzer-driven plan, the Modularization MCP checked and persisted state, managed tasks and completeness, created package infrastructure and protocol seams, moved sources, updated dependencies and code generation, and drove compilation and testing. After each build, it queried Xcode’s index for precise symbols and references instead of using broad text searches.</p><p>The index gave the Main Agent smaller, more accurate context when reasoning was required. The skill made the workflow discoverable, the MCP made it durable and token-aware, and the Main Agent’s loop provided autonomy.</p><h4>Verify against executable truth</h4><p>Build and test evidence, rather than the Main Agent’s judgment alone, determined completion.</p><p>Completion came from a hierarchy of builds and tests:</p><ol><li>Build the focused domain component workspace app with only that domain and its dependencies.</li><li>Run the domain’s tests until they pass.</li><li>Compile and test the legacy workspace against the domain’s new public seams.</li><li>Wire the modularized domain into the main application.</li><li>Build and test the shippable application.</li><li>Prepare the complete change for review as one coherent pull request.</li></ol><p>Each green boundary expanded the part of the system we knew was safe. Each failure returned control to the loop.</p><p>Builds and tests provided feedback throughout the workflow. Their results supplied the evidence the Main Agent used to choose the next state.</p><figure><img alt="Flowchart showing the primary path: analyze and plan a domain, apply the modularization change, then build and test the current boundary; if the boundary isn’t green it routes to the failure router, otherwise it checks whether all boundaries are green before producing a verified pull request." src="https://cdn-images-1.medium.com/max/1024/1*rbw4lwlEvZIxVAvIyX9WIw.png" /><figcaption>C2A: the primary path advances only when executable evidence is green</figcaption></figure><h4>Use memory before reasoning again</h4><p>When a build failed, the Main Agent classified the error while the MCP checked persistent memory for a matching pattern. If an earlier domain had produced the same failure, the MCP applied the recorded resolution and compiled again.</p><p>Only unfamiliar failures required fresh reasoning. The Main Agent examined the dependency and ranked possible resolutions by architectural fit and blast radius. It selected the best option and returned the workflow to verification.</p><p>Memory made later migrations cheaper. Each verified fix became available to later runs, which reduced cost and uncertainty as the work moved across domains.</p><figure><img alt="Flowchart showing failure handling: a boundary-not-green signal triggers a check of workflow memory, which routes to a known fix with no new reasoning, Main Agent reasoning for a new failure, or human direction for an architectural fork; the selected resolution is applied and the workflow returns to build and test." src="https://cdn-images-1.medium.com/max/1024/1*VqGQF1xob3g_Xat2YeDT9g.png" /><figcaption>C2B: every failure route returns to build and test with state preserved</figcaption></figure><h4>Treat token cost as a system constraint</h4><p>Efficiency was part of the architecture. We reduced unnecessary model work in five ways:</p><ul><li>The MCP handled deterministic analysis, infrastructure changes, compilation, and testing.</li><li>Structured findings and Xcode index lookups supplied precise facts and symbols without broad repository searches.</li><li>Focused domain workspaces bounded both source context and build feedback.</li><li>Memory reused verified fixes before requesting new reasoning.</li><li>Main Agent routing reserved deeper reasoning and human attention for genuinely new or high-impact decisions.</li></ul><p>We did not optimize for the fewest possible model calls. We optimized for the least expensive safe path to the next verified state.</p><h3>Put humans at decision boundaries, not inside every step</h3><p>Human involvement remained essential, but its placement changed.</p><p>Engineers no longer needed to supervise routine file movement, known build errors, or repeated compile-and-test cycles. The Main Agent routed work to a person only when it discovered a consequential architectural fork. One example was a deep cross-domain dependency with several credible resolutions and a large potential blast radius.</p><p>At that point, the workflow paused with evidence: the dependency path, the affected domains, the failure, and ranked options. An engineer provided architectural direction, and the Main Agent resumed from the state preserved by the MCP.</p><p>One especially large and complex domain ran through the workflow for more than eight hours before requiring its first human decision.</p><blockquote>Useful autonomy keeps work moving safely within clear decision boundaries. It is measured by how far the system can progress between decisions that genuinely require human judgment.</blockquote><h3>The outcome: months became days</h3><p>The results moved together across program delivery, domain delivery, build feedback, CI, and developer flow.</p><figure><img alt="Table of six before-and-after metrics: program timeline from about 2 years to about 3 months, domain delivery from about 4 months to under 3 days, focused domain build from about 15 to 2 minutes, CI build and test from 45 to 11 minutes, P75 cycle time from about 5 to 2 days, and P75 coding time from about 4 days to about 1 day." src="https://cdn-images-1.medium.com/max/1024/1*xIJUX5gyVMhQaZbkPE7rQQ.png" /><figcaption>The program improved delivery speed without pausing weekly releases</figcaption></figure><p>A medium-to-complex domain moved from about four months to under three days. Focused domain builds fell from about 15 to 2 minutes, while CI build and test time fell from 45 to 11 minutes. P75 cycle time moved from about 5 to 2 days, and P75 coding time from about 4 to 1 day.</p><p>The architecture changed as well:</p><ul><li>Domains became independently executable and testable.</li><li>Dependency boundaries became enforced by builds rather than described only in documents.</li><li>The continuous integration system could perform more selective work.</li><li>Smaller build and test surfaces improved reliability and developer feedback.</li><li>Product development and weekly releases continued throughout the transformation.</li></ul><p>The migration finished faster, and it left behind a more modular architecture.</p><h3>Why the loop scales where prompts do not</h3><p>Prompts are temporary. They depend on the context available at one moment.</p><p>Skills last longer and usually describe how to perform a task. State management, verification, recovery, memory, cost controls, and escalation boundaries sit outside them.</p><p>A production engineering loop brings those pieces together.</p><p>It scales because it has:</p><ul><li><strong>State:</strong> it knows what has completed, what failed, and what must happen next</li><li><strong>Evidence:</strong> builds, tests, and dependency analysis determine progress</li><li><strong>Memory:</strong> successful resolutions become reusable operational knowledge</li><li><strong>Economics:</strong> deterministic programs perform deterministic work, reserving model reasoning for ambiguity</li><li><strong>Boundaries:</strong> rules define what may proceed automatically and what requires human judgment</li><li><strong>Recovery:</strong> failure is an expected input to the next iteration, not the end of the run</li></ul><p>This changes the model’s role. The Main Agent does not simply receive a task and generate a change. It defines and follows explicit loop semantics, while the MCP supplies durable state, deterministic execution, and evidence. Reasoning is applied only where it has the highest value.</p><h3>Lessons for organizations operating at scale</h3><p>Six principles from this work apply beyond modularization or mobile development:</p><ol><li><strong>Start with the human success pattern.</strong> Before automating, understand how experts recognize completion, including the evidence, exceptions, and escalation points.</li><li><strong>Make success executable.</strong> Compilation, tests, dependency rules, and generated artifacts give the loop a machine-verifiable definition of done.</li><li><strong>Separate certainty from ambiguity.</strong> Put inventory, graph traversal, validation, and pattern matching in deterministic tools. Reserve model reasoning for multiple plausible solutions.</li><li><strong>Design memory as part of the product.</strong> Chat history is not operational memory. Persist failure signatures, resolutions, context, and verification outcomes in a form the next run can query.</li><li><strong>Engineer the escalation path.</strong> Trigger human involvement when a decision requires authority. Supply the evidence and preserve the state needed to resume.</li><li><strong>Optimize the whole system.</strong> Model speed is only one variable. At organizational scale, optimize elapsed time, token consumption, human attention, recovery cost, release safety, and architectural quality together.</li></ol><h3>The next era is about stateful loops</h3><p>Prompts provide direction. Skills package capability. Sub-agents provide bounded execution. Autonomy becomes reliable when the Main Agent defines explicit loop semantics and the MCP preserves the state needed to run the loop.</p><p>The loop provides motion. The state machine provides control. The MCP tracks progress and preserves memory. The Main Agent uses that evidence to repeat, advance, route, or escalate.</p><p>Once that architecture is explicit, a coding agent can serve as both controller and reasoning component inside a larger engineering system. The system can run for hours, improve across projects, and make progress while engineers retain control of architectural decisions.</p><p>The question for engineering organizations is no longer, “What prompt should we write?”</p><p>It is, “What stateful loop should we engineer?”</p><p><a href="https://careers.expediagroup.com/life/">Learn about life at Expedia Group</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ac26e3b2706" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/beyond-the-prompt-engineering-the-loop-for-software-change-at-scale-9ac26e3b2706">Beyond the Prompt: Engineering the Loop for Software Change at Scale</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[The Quiet Tax on Machine Learning Teams]]></title>
            <link>https://medium.com/expedia-group-tech/the-quiet-tax-on-machine-learning-teams-43d71bd539c7?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/43d71bd539c7</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[ml-engineering]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[mlops]]></category>
            <dc:creator><![CDATA[Gianpi Colonna]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 15:38:37 GMT</pubDate>
            <atom:updated>2026-10-20T15:38:36.097Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/data">Expedia Group Technology — Data</a></h4><h4><strong>Building a command-line interface to standardize machine learning model delivery at Expedia Group</strong></h4><figure><img alt="Montserrat’s rock spires near Barcelona at sunset." src="https://cdn-images-1.medium.com/max/1024/1*ScbZRH1fx7fb0vXySfsyhg.jpeg" /><figcaption>Photo by Gianpi Colonna — Montserrat, Barcelona</figcaption></figure><p>At <a href="https://www.expediagroup.com/">Expedia Group</a>™, machine learning (ML) teams design, build, and deploy models that use optimization techniques and artificial intelligence to help travelers find the best option for every trip.</p><p>In practice, a <strong>significant share of time goes to everything <em>around</em> models</strong>: continuous integration and continuous delivery (CI/CD) pipelines, automated retraining, inference setup, access permissions, environment targeting, and cluster and resource sizing on Databricks and Kubernetes. These tasks can’t be skipped, and every moment a model waits on them is time that model isn’t helping travelers. This phenomenon isn’t specific to Expedia Group; it shows up in any growing organization that runs several ML teams.</p><p>Inside these ML teams, ML scientists, on the one hand, want to iterate on optimization techniques, features, architectures, and training strategies. ML engineers, on the other hand, want to work on novel engineering initiatives, challenging problems, and partner closely with the scientists they support to serve ML models at scale. This organisation and split, although easy to define, is not perfect. Often, teams spend their time running repetitive, ad-hoc deployment work one model at a time. Both groups <strong>end up doing the same setup work again and again, on every new model</strong>.</p><p>This repetitive setup work is called the <strong><em>quiet tax, </em></strong>and eventually, it becomes a structural cost. Without standardisation, the engineering effort required to deploy and maintain N models grows roughly linearly with N. In other words, every new model brings its own workflow setup, environment alignment, debugging cycle, among other efforts and tasks.</p><figure><img alt="Illustrative, not measured, chart comparing standardized and bespoke deployment methods in engineering effort versus production model count. Standardized deployment effort rises at first, then levels off and gradually declines. In contrast, bespoke deployment effort increases linearly, crossing it at about 20 models and growing steadily by 60. The widening difference is labeled &quot;the quiet tax&quot;." src="https://cdn-images-1.medium.com/max/1024/1*IcJ6X9-D7qKfTmTMP1nC3w.png" /><figcaption>A figure comparing the engineering effort and time taken for ML models with and without standardised setups. When no standard is in place, teams pay the <strong>quiet tax, </strong>where effort and time grow linearly with the number of ML models. When standards are in place, teams spend sublinear effort and time after a certain number of ML models. This figure is for illustration purposes only and does not constitute measured data.</figcaption></figure><p>This article describes a problem space many ML teams will recognise, the trade-offs in solving it, and one pattern that works well: <strong>a command-line interface (CLI) that standardises the setup work that is the same for every model, while leaving the model itself, i.e., code, dependencies, and framework, fully in the hands of the scientist.</strong></p><h3>A familiar but taxed workflow</h3><p>To make the problem concrete, it helps to walk through a workflow that will be familiar to most ML practitioners. The exact shape varies by team, by model, by personal preference and seniority; however, the general pattern is broadly familiar across the industry.</p><p>An ML scientist typically starts in a notebook environment, working interactively against a cluster configured ad hoc for their use case. They load data, prototype features, train models, and iterate until they have something worth promoting. Up to this point, the experience is fast and interactive, which is exactly what notebook development is good at.</p><p>The friction begins on the way to production. The working notebook has to be migrated into a version-controlled repository, restructured into importable code, and wired to run outside the interactive cluster. Cluster configurations drift between environments, so settings that worked in development do not always work in staging or production. The list of things that need to be aligned is long and rarely documented in one place. For instance, identity and access management (IAM) roles, service accounts, secrets, and table permissions.</p><p>On top of that, the ML scientist has to set up a workflow orchestrator for training and inference, and decide how it will be scheduled. For models that require hyperparameter optimisation, there is the additional problem of running the search in parallel across multiple executors. This, for example, is something most platforms support, but that teams rarely set up because the implementation effort is not worth it for a single model. None of these tasks are conceptually hard. However, their context must be reconstructed from memory, institutional knowledge, or another model’s repository, every time.</p><p>The outcome is predictable: a gap of days to weeks between <em>“the model works in the notebook”</em> and <em>“the model is running reliably in production”</em>, with most of that gap spent on setup work rather than on the model. And that gap is paid twice. Once in engineering time, and again in deferred impact: a finished model that is not yet in production is delivering nothing.</p><h3>Three common classes of failure</h3><p>Without a standardised setup, the same categories of problem recur across models, more often quietly, but sometimes loudly. Any one of these is manageable. When all three happen at once, across, for example, thirty models, it is a tax.</p><p><strong>Silent failures.</strong> The dangerous ones. For instance, version mismatches in ML frameworks or dependencies could silently corrupt predictions. Another example: Spark configuration drifting between development and production, altering data processing behaviour in ways that passed initial testing.</p><p><strong>Visible failures.</strong> The time-consuming ones. These failures are loud, which makes them tractable; however, the cumulative cost of debugging them, model by model, is significant. For instance, runtime errors from forgotten version pins; authentication failures from misconfigured IAM roles or service accounts; or incompatible package combinations.</p><p><strong>Operational inefficiencies.</strong> The slow-burn ones. These cause friction for both new and experienced ML teams, where the friction can slow down processes for weeks at times. For instance, configuration drift between models without a single source of truth; knowledge silos where certain engineers understand specific deployment patterns; or manual approval bottlenecks for routine tasks.</p><h3>The tension to resolve: standardisation against flexibility</h3><p>The naive solution of “everyone uses the same template” constrains legitimate scientific variation. ML scientists work in different ML frameworks, including TensorFlow, PyTorch, and custom in-house frameworks. Business logic also varies significantly across use cases. Compute profiles differ from one model to the next. A rigid template collapses that variation into a lowest common denominator, which is not a victory. A more useful question is: <strong>which components should be standardised, and which should remain flexible?</strong></p><p><strong>The components that should remain flexible are the ones where variation is the source of value: </strong>the model code, framework choice, dependencies, business logic, compute requirements. <strong>The components that should be standardised are the ones where variation is only a source of drift: </strong>CI/CD pipelines, development quality standards, testing frameworks, deployment configurations, and governance integration. Put differently, the differentiator is the model, not the deployment pipeline.</p><h3>A command-line interface, not a platform</h3><p>A pattern that works well at this scale is a CLI that scaffolds and manages production-ready ML project structures. The syntax of the CLI is deliberately uniform: every command follows the shape &lt;resource&gt; &lt;operation&gt;, where the operation is one of create, update, validate, or delete. A scientist starting a new model runs two commands to get a working setup generated for them: one to bootstrap the repository, one to add a model to it.</p><h4>Drawing the line: project versus model</h4><p>The CLI is built around a deliberate split between the <strong>business domain a repository serves</strong> (the <em>project</em>) and the <strong>individual ML model inside it</strong> (the <em>model</em>).</p><p>A <strong>project</strong> is a repository scoped to a business domain. It owns the shared infrastructure, CI/CD, and environment configuration. It is expected to hold multiple models over its lifetime.</p><p>A <strong>model</strong> lives inside a project. It owns its own config, dependencies, notebooks, and orchestration workflows, and inherits the project’s shared infrastructure.</p><p>This split matters for two reasons. First, it prevents repo sprawl; i.e., adding a new model to an existing domain is strictly preferred over creating a new project. Second, it simplifies the organization of business domains, i.e., a project is the single place where all the models and configurations of a business domain are located.</p><p>The rule that follows: a new project is justified only when a new model belongs to a genuinely different business domain than anything in any existing project.</p><h4><strong>Resources</strong></h4><p>Everything the CLI manages is modelled as a <em>resource</em>. Some resources are scoped to a model, some to the project, and most are created by default; a few are optional and only added when a team opts in. A representative catalogue covers:</p><ul><li><strong>Project and model containers: </strong>the top-level scoping primitives.</li><li><strong>Training and inference entry points: </strong>generated notebook templates, source modules for testable code, and bindings to a workflow orchestrator.</li><li><strong>Workflow files: </strong>auto-generated orchestration definitions for each model, regenerated whenever config or resources change.</li><li><strong>Model registry integration: </strong>uploading and loading model artefacts from a model store.</li><li><strong>Experiment tracking integration: </strong>built-in hooks into a tracking server.</li><li><strong>Table writer utilities: </strong>a standardised path for persisting outputs with consistent schemas and governance defaults.</li><li><strong>Notebook bootstrap utilities: </strong>so notebooks behave consistently across clusters and environments.</li><li><strong>Config schema and interface contracts:</strong> the loaders and contracts that make model code pluggable.</li><li><strong>Validation step:</strong> automated acceptance checks (metric thresholds, schema checks, distribution drift) between training and inference that fail the workflow when the model regresses.</li><li><strong>Human-in-the-loop gate:</strong> a configurable gate baked into training and inference that pauses execution before persistence, notifies a reviewer with the run artefacts, and only saves the result on approval.</li><li><strong>Hyperparameter search:</strong> a standardised parallel search harness wired to the experiment tracker.</li></ul><p>Each resource is documented individually, and the list grows over time. Crucially, every new resource inherits the same lifecycle: <strong>create</strong>, <strong>update</strong>, <strong>validate</strong>, <strong>delete. </strong>ML scientists do not have to learn a new set of commands every time the platform gains a capability.</p><figure><img alt="Ownership matrix: ML Scientist and CLI columns; Value and Drift rows. Under Value, the scientist owns differentiation (model code/architecture, framework, hyperparameters, features, business logic, dependencies, evaluation); CLI over-reach is intentionally empty. Under Drift, manual setup, permissions, workflows, registry uploads, and parallel HPO move to CLI standardisation (CI/CD, workflow/auth, runtime/environment config, registry/tracking, governance, validation, HITL, HPO)." src="https://cdn-images-1.medium.com/max/1024/1*4U2QyM8XmgexZ_OfSGm9sQ.png" /><figcaption>A figure showing the ownership of responsibilities between ML scientists and a CLI tool. A CLI can standardise several processes without jeopardizing scientific variability and discoverability. Moreover, standardising those processes helps ML Scientists to focus on addressing more complex problems.</figcaption></figure><h4>Two non-obvious design choices</h4><p><strong>Shared infrastructure code is copied into each project, not hidden behind a package.</strong> The alternative, i.e., publishing shared code as an internal dependency, would make updates trivial. The heavier path is the right one at small-to-medium scale because <em>visibility</em> matters more than <em>update ergonomics</em>. When an ML scientist hits an error in shared code, they can read it, add print statements, and debug it themselves. Stack traces point to local files, which shortens the feedback loop and reduces the support burden on engineers. Past a certain scale, the trade-off inverts and a published package becomes the right answer; until then, visibility wins.</p><p><strong>The CLI retains ownership of infrastructure files after scaffolding.</strong> Running <strong>update</strong> pulls the latest templates for managed files while respecting local changes to user-owned files. This means <strong>the CLI is not a one-shot code generator; it is the durable owner of the undifferentiated parts of every project</strong>, with a clear boundary around the parts the team owns.</p><p>The result is that standardisation applies where it should, and flexibility survives where it should.</p><figure><img alt="CLI update behavior: user-owned files are untouched; scaffolded-only files are created once, then user-owned; CLI-managed files regenerate on each update. Model-scoped: source code and model config are scaffolded-only; wrappers and workflow files are CLI-managed; tests, custom modules, and notebooks are user-owned. Project-scoped: dependency manifest is scaffolded-only; CI/CD, cluster/runtime config, shared infrastructure, and governance/tagging defaults are CLI-managed." src="https://cdn-images-1.medium.com/max/1024/1*6vLNv6j6wIs7CJ-opZpsDQ.png" /><figcaption>A figure showing how the CLI updates different kinds of files in a project. Some files are always user-owned, so the CLI never changes them, e.g., shared modules. Some files are CLI-scaffolded, then user-owned, e.g., source code for training or inference. Lastly, the remaining files are CLI-managed, meaning the CLI creates or updates their contents via its commands, e.g., CI/CD pipelines.</figcaption></figure><h3>What comes next</h3><p>The resource model is explicitly open-ended. Several capability classes sit naturally on the roadmap of any tool of this shape:</p><ul><li><strong>Real-time model deployments: </strong>Some models need to run as microservices behind an HTTP or gRPC endpoint rather than as scheduled batch workloads. Built-in support for serving frameworks (KServe and similar) is a natural extension for teams moving in that direction.</li><li><strong>Multi-model orchestration: </strong>Production-grade real-time ML pipelines rarely runs a single model in isolation. They typically run an orchestrator that routes traffic across multiple models, run experiments, and call feature stores. The CLI can add project-scoped resources to manage orchestrator configuration alongside the models it serves.</li><li><strong>Hooks for downstream monitoring: </strong>The CLI itself does not need to manage monitoring dashboards, but there are upstream prerequisites worth standardising: consistent training-baseline tagging, run-id propagation, schema fingerprints, among others. Monitoring tools can consume them without bespoke per-model wiring. Getting these hooks right makes monitoring setup a configuration task rather than an integration project.</li><li><strong>Feature cataloguing:</strong> A standard format for declaring the features a model consumes, alongside lineage and ownership metadata, would make cross-model reuse practical rather than aspirational.</li><li><strong>Automated profiling and bottleneck identification:</strong> Model training and inference code regularly hides performance problems that only surface in production, slow data loaders, unnecessary collects, inefficient joins, memory blow-ups. Standardised profiling hooks that run alongside training and inference, surface the hot paths, and flag likely bottlenecks would turn performance work into a routine output of every run rather than an ad-hoc investigation triggered by an incident.</li></ul><p>The pattern across all of these is the same one that drives the tool in the first place: identify a capability that every team would benefit from, standardise the setup, and leave the modelling to the scientist.</p><h3>What changes when this works</h3><p>The change is best described in terms of the people the tool serves.</p><p><strong>For the ML scientist: initialisation in minutes, not days.</strong> A scientist starts a new model by running two commands, gets a production-ready project, and is iterating on the model itself within minutes. Production setup is no longer a multi-day project that competes with modelling time; it is a small step that happens once and is then taken for granted.</p><p><strong>For the new joiner: one structure to learn, not many.</strong> Every project the team owns shares the same skeleton. A new team member who learns one project effectively understands all of them. Onboarding stops being a survey of historical decisions across many repositories and becomes the much shorter task of learning a single, current standard.</p><p><strong>For everyone: silent drift goes away.</strong> Because the code running during development is generated from the same source of truth as the code running in deployment, configuration drift between environments stops being a recurring class of bug. The “silent failure” category — where dev and prod look the same to the eye but behave differently in subtle ways — shrinks materially.</p><h4>The compound advantage</h4><p>The first-order benefits are what sell the tool. The second-order effects are what make it pay back over years.</p><ul><li><strong>Maintenance compounds:</strong> A bug discovered in shared infrastructure is fixed <em>once</em>, in the CLI, and propagates to every project on the next update. Without the CLI, the same fix is repeated by hand across N repositories — and missed in some of them.</li><li><strong>Improvements compound:</strong> A new capability becomes available to <em>every</em> model the moment it ships in the CLI. The fixed cost of building the capability is amortised across the entire portfolio.</li><li><strong>Reasoning compounds: </strong>A reviewer, an on-call engineer, or a new joiner who looks at <em>one</em> project recognises the patterns in <em>all</em> of them. Operational reasoning is no longer a per-project skill; it is a team-wide one.</li><li><strong>Scaling compounds: </strong>Adding a model no longer adds proportional engineering load.</li></ul><p>The hypothesis worth betting on is that these second-order effects matter more than the first. If the tool works as intended, every new model joins a standardised baseline instead of starting from a blank page; <strong>every CLI update propagates across the portfolio at once; and every engineering improvement lands on every model rather than just one</strong>. That is the outcome the investment is for, and the reason standardisation compounds over time.</p><h3>Final thoughts</h3><p>The broader principle is worth extracting. <strong>A template is a one-shot starting point; a CLI is a durable tool that continues to own the infrastructure it generates.</strong> Every fix, every new capability, every updated default propagates across the portfolio on the next update. <strong>Maintenance and improvements compound.</strong></p><p>The approach assumes a bounded number of projects, on the order of ten, not hundreds. At larger scale, the shared-code visibility argument inverts, and the right answer becomes a published package. Knowing that the architecture can evolve in that direction is part of the case for starting with a CLI.</p><p>For any team carrying the quiet tax described above, the useful exercise is simple: look at your ML delivery pipeline and separate what is genuinely differentiated from what is undifferentiated plumbing. The differentiated work is why your team exists. The plumbing is the tax, and it does not have to be paid model by model.</p><h3>Acknowledgements</h3><p>Thanks to the Expedia Group engineers and scientists who provided their feedback, code, reviews, and design input to the CLI. Special thanks to our manager, Marco Rodrigues, for backing the initiative, and to <a href="https://medium.com/@FernandoBPerezM">Fernando B. Pérez Maurera</a>, who will lead the project going forward.</p><p><em>Edited by Anantharaman Janakiraman from Expedia Group Technology editorial team</em></p><p><a href="https://careers.expediagroup.com/life/"><em>Learn about life at Expedia Group</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43d71bd539c7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/the-quiet-tax-on-machine-learning-teams-43d71bd539c7">The Quiet Tax on Machine Learning Teams</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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 the Operating Model for Agentic AI at Expedia Group]]></title>
            <link>https://medium.com/expedia-group-tech/building-the-operating-model-for-agentic-ai-at-expedia-group-f7555fcce506?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/f7555fcce506</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[innovation]]></category>
            <category><![CDATA[ai-governance]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[ai-agent]]></category>
            <dc:creator><![CDATA[Xavier Amatriain]]></dc:creator>
            <pubDate>Tue, 22 Sep 2026 11:01:01 GMT</pubDate>
            <atom:updated>2026-09-22T11:01:01.460Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/innovation">Expedia Group Technology — Innovation</a></h4><h4>How Expedia Group is getting organizationally and operationally ready for agents</h4><figure><img alt="A view of Machu Picchu" src="https://cdn-images-1.medium.com/max/1024/0*f2_6Eu3UmexBLGsh" /><figcaption>Photo by Willian Justen de Vasconcellos on Unsplash</figcaption></figure><p>At <a href="https://www.expediagroup.com/">Expedia Group</a>™, agents are showing up across the business: traveler-facing trip planners, partner tools, internal copilots, log readers, and a growing number of workflows that make teams say, “We tried this, and it was surprisingly useful.”</p><p>That energy is exactly what AI should unlock. But without shared foundations, it can also create fragmented technology choices, partial governance stories, and promising experiments that never become durable capabilities.</p><p>The question is not how to stop teams from building agents. It is how to let teams explore locally while building toward shared standards, reusable patterns, clear accountability, and a coherent experience for travelers and employees.</p><p>At Expedia Group, the answer is a connected operating model: communication and culture, platform and tooling, governance, and enforcement and recommendations. Together, these layers help us move quickly without becoming fragile.</p><h3>What “agentic development” means at Expedia Group</h3><p>“Agentic” is a term that can quickly become too broad to be useful. We should be precise.</p><p>Deterministic workflows, AI-assisted workflows, and agents all have a place in our technology landscape. A deterministic workflow follows explicit, repeatable rules. An AI-assisted workflow may call a model within an otherwise scripted process. Both can be valuable, reliable, and often the right solution for a defined problem.</p><p>But neither should automatically be called agentic.</p><p>At Expedia Group, an agent is an AI-powered system that works toward a goal. It can use context and memory, draw on skills, reason across multiple steps, and use tools to gather information or make progress. Some tasks will be long-running: they require the system to maintain relevant context, adapt to changing conditions, and work through a sequence of decisions rather than execute a fixed path.</p><p>This definition is not about giving systems unlimited autonomy. It is about deliberately designing the capabilities that make an agent useful: a clear goal, bounded memory, well-defined skills, orchestration, secure tool access, evaluation, and observability. The more capable the system, the more intentional its design needs to be.</p><p>The practical principle is simple: choose the least complex approach that can deliver the outcome — but use true agentic systems when a goal requires reasoning, context, tool use, and adaptive multi-step work.</p><h3>From specialized agents to a whole-trip experience</h3><p>We do not believe the future is one monolithic agent that attempts to do everything. The future of travel AI is a composed, multi-agent system.</p><p>Specialized agents can be excellent at discrete jobs: understanding traveler intent, helping with discovery, comparing properties, building an itinerary, answering questions about booking, or supporting a traveler when plans change. Each should deliver value on its own. Over time, orchestrating those specialized agents can create a continuous, end-to-end whole-trip experience.</p><p>This is both an ambitious product vision and a practical engineering approach. Narrowly focused sub-agents are easier to test through robust evaluations, easier to secure, and easier to improve. Their focus helps teams understand failure modes, set task-specific quality bars, and ensure agents do not go off the rails. That focus does not make them deterministic. They still reason, use context, and adapt; they simply have a clear purpose and well-designed boundaries.</p><p>The orchestration layer then becomes a product and systems-design challenge. How do specialized agents share the right context and memory? How do they hand off cleanly? How do they follow common principles for tone, transparency, privacy, and traveler control? The traveler should not have to understand the architecture. They should simply experience Expedia Group as a trusted companion that understands their trip and remains helpful when plans change.</p><h3>Evals are the new PRD</h3><p>For agentic systems, evaluations cannot be a final compliance check after the product is designed. They are how we encode what the product is meant to do: the same role a Product Requirements Document (PRD) has traditionally played.</p><p>That means shifting evals left. Before building, teams should define the traveler or business outcome, the behaviors they expect, the failure modes they will not accept, the security and privacy requirements, and the risk-based release criteria. Those expectations belong in the product design itself.</p><p>This is the core idea behind “evals are the new PRD.” A strong evaluation suite turns expectations into something we can test, debate, monitor, and improve. It creates a shared language for quality and makes a faster feedback loop possible: production signals should feed back into the evaluation suite, which helps teams identify, fix, and prevent issues.</p><p>This is how we build velocity rather than merely speed. Teams need room to experiment, but they also need the shared mechanisms to learn quickly, reuse what works, and converge with confidence.</p><p>For consequential traveler actions, the quality bar should be appropriately high. Agents can inform, compare, recommend, and help a traveler decide. The traveler should retain the final click for decisions such as completing a booking. That is a trust principle as much as a product one.</p><p>Delivering that standard consistently cannot depend on individual teams solving for quality, safety, and scale on their own. It requires an <strong>operating model</strong> that turns these principles into shared capabilities and everyday development practices.</p><h3>The operating model</h3><p><strong>1. Communication and culture: creating shared visibility before standardizing</strong></p><p>The first layer of readiness is not a platform or a policy. It is the way people share what they are doing.</p><p>Agentic work at Expedia Group is intentionally distributed. Different organizations face different problems, so experiments should not all look the same. The important pattern is what happens next: teams fan out to learn locally, then fan back in by turning useful discoveries into shared guidance and assets.</p><p>That connective tissue includes:</p><ul><li>AI Champion groups embedded in teams to support adoption, repository readiness, and local sharing of tools and agents.</li><li>Cross-team guilds, including the Expedia Group Agentic Leads Guild, where senior practitioners compare patterns, surface failure modes, and develop standards that can travel across organizations.</li><li>Engagement forums — AI Slack channels, office hours, Show and Tell sessions, and events such as the Expedia Group Intelligence Conference — where teams share both the business story and the implementation story.</li><li>Organization and company hubs that bring together tools, patterns, and channels so teams can find existing work before creating another version from scratch.</li></ul><p>The goal is more “we adapted another team’s pattern” and fewer surprises where multiple groups solve the same problem in incompatible ways. Internal AI adoption is not separate from the traveler vision; it is how we build the speed, creativity, and operating muscle to deliver it.</p><p><strong>2. Platform and tooling: making the right path reusable</strong></p><p>Teams should not have to rebuild the foundations for every use case. Expedia Group is building a common GenAI and agentic fabric that provides governed, observable access to models; shared evaluation and experiment tooling; tool and Model Context Protocol (MCP) governance; and integration with logging, alerting, and incident-management systems.</p><p>The platform must also support model optionality. The model landscape is changing too quickly for any one model, vendor, architecture, or cost curve to remain optimal for every task. Optionality is not abstraction for its own sake. It is the ability to evaluate, route, and change models based on quality, latency, cost, risk, and the needs of the experience.</p><p>The same is true of the assets that make travel AI valuable: trusted supply, rich and current content, marketplace signals, first-party traveler understanding, and accountable servicing. Generic AI can generate generic travel answers. Expedia Group’s opportunity is to build agents that are more accurate, current, useful, and grounded in the real traveler experience.</p><p>Platform investment should turn local experimentation into shared capability. It is how the work of one team can make every subsequent team faster.</p><p><strong>3. Governance: making safe development easier to follow</strong></p><p>Governance starts with Responsible AI and is reinforced by security standards for agents and tools. Every AI initiative that goes live, including agents, should be registered and risk rated.</p><p>Governance needs to correlate to risk. A lightweight internal helper and a traveler-facing trip-planning agent do not carry the same consequences, so they should not face the same path. Lower-risk use cases should move quickly with a lighter process; higher-risk traveler or partner capabilities should receive more scrutiny.</p><p>Shared standards should make expectations clear around:</p><ul><li>Ownership and accountability.</li><li>Approved access paths to models and data.</li><li>Agent identity and permissions.</li><li>Logging, traceability, and observability.</li><li>How systems behave when they cross a defined boundary.</li></ul><p>The goal is not to bolt requirements on at the end. It is to design them into the tools and workflows teams already use, so safe development is the easiest path — not a separate document or a late-stage obstacle.</p><p><strong>4. Enforcement and recommendations: turning principles into practical adoption</strong></p><p>Our ML/AI Model Principles set the common standard for how we build across Expedia Group. They apply equally to classical machine learning, generative AI, and agentic systems: pursue measurable business impact and long-term marketplace health; earn complexity against strong baselines; evaluate both offline and online; build on shared foundations; make systems traceable; govern proportionally to risk; and monitor continuously in production. These principles have also been shared publicly because they are not abstract aspirations — they reflect how we intend to build reliable AI at scale.</p><p>Agent Release Tollgates are the primary mechanism for turning those principles into action for agent development. They are not a final checklist at the end of a project. They are progressive, risk-tiered checkpoints that complement the Software Development Life Cycle (SDLC) and shift agent-specific practices — evaluation, operational readiness, security, cost effectiveness, and observability — left into the development process. The goal is to anticipate issues early, so teams can move faster once they are on the road to production.</p><p>We are also automating this process as part of the Agentic SDLC. Tollgates can already be created and tracked as work items through Jira automation, and the broader direction is to automate their execution with reusable checks, evaluation assets, metrics, and dashboards rather than ask each team to build its own bespoke pipeline. That is essential if these standards are to scale across agents, skills, tools, and multi-agent systems.</p><p>The paved road includes:</p><ul><li>Agent Release Tollgates that connect Responsible AI risk classification to the requirements that are recommended or required, and make readiness visible throughout development rather than only at launch.</li><li>Standard implementation patterns in agent-development handbooks and guidance, including how to structure tools, avoid burying business logic in prompt spaghetti, and keep behavior observable.</li><li>AI-ready repositories developed with Developer Experience and AI Champions, so documentation, configuration, and automated checks live in the repository rather than in someone’s inbox.</li><li>Shared observability patterns that make agent behavior inspectable through standard tracing, logging, and dashboards rather than bespoke tooling that only one person can operate.</li></ul><p>The paved road should be opinionated, but not restrictive. It does not close every other path; it makes the common path smoother, better supported, and easier to operate. In practice, it asks focused questions throughout development: Are we evaluating the behavior that matters? Is anyone watching this? Do we understand cost and latency? If it misbehaves, who owns the response?</p><h3>Measuring impact, not novelty</h3><p>Agentic approaches are already being applied to Expedia Group’s operations. For example, Developer Experience (DevEx) is investing in reusable agents that reduce recurring “keep the lights on” work across many repositories — not just a single product team.</p><p>But a plausible response is not enough. Agents should be evaluated on whether they complete tasks correctly, reduce manual effort or cycle time, are adopted and relied on by intended users, operate within sensible cost and latency bounds, and improve a real traveler or business metric.</p><p>A clever agent that nobody uses — or that costs more to run than the work it replaces — is a useful experiment, not a template to scale.</p><h3>How this changes the enterprise</h3><p>Taken together, these layers add up to more than tools and documents. They represent an operating-model shift.</p><p>Agents are becoming part of how Expedia Group thinks about how work moves through the organization, how decisions are supported, how knowledge is captured and reused, and how responsibility is shared across domains and teams. Experiments are encouraged, and so is documenting what was learned. Local innovation is valued, and so is connecting successful work back into shared rails and standards.</p><p>The story of agentic AI at Expedia Group is not simply that there will be more agents. It is that there will be more structure underneath them: a clear definition of what an agent is, specialized agents that can become a coherent whole, evaluations designed in from the start, and shared foundations that help the company innovate with speed, direction, and trust.</p><p><a href="https://careers.expediagroup.com/life/"><em>Learn about life at Expedia Group</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f7555fcce506" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/building-the-operating-model-for-agentic-ai-at-expedia-group-f7555fcce506">Building the Operating Model for Agentic AI at Expedia Group</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[The Context Tax of Agentic Development]]></title>
            <link>https://medium.com/expedia-group-tech/the-context-tax-of-agentic-development-0bb9de03237c?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/0bb9de03237c</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <category><![CDATA[teamwork]]></category>
            <category><![CDATA[context-engineering]]></category>
            <category><![CDATA[engineering-management]]></category>
            <dc:creator><![CDATA[A Talhan]]></dc:creator>
            <pubDate>Tue, 25 Aug 2026 11:01:02 GMT</pubDate>
            <atom:updated>2026-08-25T11:01:02.009Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/engineering">Expedia Group Technology — Engineering</a></h4><h4><em>Missing context used to slow a team down, with agents in the loop it speeds up the wrong work instead</em></h4><figure><img alt="People on motorcycles drive over a bridge with a beach in the background" src="https://cdn-images-1.medium.com/max/1024/0*aXhwMyXw4HgpjpwG" /><figcaption>Photo by Harley-Davidson on Unsplash</figcaption></figure><p>When we started using agents more seriously inside the team, the first bottleneck was not code generation —</p><blockquote>It was context coordination.</blockquote><p>That sounds like a documentation problem, but it did not feel like one. It felt like a delivery problem. A human engineer who is missing context usually slows down. They ask someone, search through old notes, or wait for the next sync. An agent does not always slow down. It can keep moving and produce work that is technically plausible, well formatted, and aimed at the wrong reality.</p><p>That is the agentic velocity trap: unclear context does not just delay work; it can accelerate the wrong work.</p><p>Rick Fast recently <a href="https://medium.com/expedia-group-tech/reimagining-platform-engineering-for-an-agentic-future-03e3f378a190">wrote</a> about the broader <a href="https://www.expediagroup.com/">Expedia Group</a>™ platform shift toward agent-friendly interfaces and operating surfaces. This is the ground-level version from one team: what did we have to change in our own planning loop so humans and agents could work from the same understanding?</p><h3>The problem was hidden context</h3><p>One of the clearest examples came from multi-environment handling in AI Workbench (our web console for machine learning (ML) artifacts and workload management).</p><p>From the outside, an AI Workbench URL appeared to represent one backend environment. Under the hood, it was backed by another. A team saw their model in the UI, reasonably assumed it existed in the environment implied by the URL, and then hit “model not found” when downstream jobs queried that expected backend.</p><p>Internally, the concrete case was a machine learning scientist team onboarding a model. It appeared in the .prodA AI Workbench URL, but that deployment was serving model registry prodB data rather than model registry prodA data. The platform services were not simply broken. The hidden semantic mapping was because of networking and access nuances.</p><p>That distinction matters. A human team lost time because the environment model was tribal knowledge. An agent asked to write onboarding or deployment automation against the same assumptions would not know to pause and ask, &quot;Does this URL actually map to the environment it appears to represent?&quot; It would likely generate confident, tidy, wrong work.</p><p>This is the kind of problem that made us rethink what &quot;context&quot; needs to mean in an agentic development cycle. It is not enough for the context to exist somewhere. It has to be explicit enough that people can align on it, and structured enough that agents can use it without inventing missing pieces.</p><h3>This is not just more documentation</h3><p>The obvious push-back is fair: engineers already do not have enough time to write docs.</p><p>The difference we are experimenting with is timing and function. Traditional documentation is often archival. It records what happened after the fact so a human can remember or discover it later. The context we need for agentic development has to be written closer to the beginning. It defines the semantic boundary of the work before the team accelerates.<br>This comes from the same instinct as Infrastructure-as-Code or Configuration-as-Code: move important intent into declarative files, keep them close to the work, and make them reviewable. We are not claiming the maturity of Terraform-style state, plans, CI enforcement, or drift detection. We are not there. But the instinct is similar.</p><p>In our team’s context repository, proposal documents capture the problem, scope, constraints, risks, trade-offs, and decisions. A small STATUS.yaml file carries the machine-readable part: current stage, priority, affected services, OKR alignment, target date, Jira, and stage notes. Markdown gives humans a readable narrative. YAML gives agents and scripts a compact routing layer.</p><p>For example, our in house Model Monitoring proposal status tells you, without reading the full Product Requirements Document (PRD), that the work is in implementation stage, affects services such as AI Workbench, Workflow Orchestration, Model Registry, and Deployments, aligns to embedded Model Monitoring, targets June 2026, and is tracked under a specific Jira.</p><p>That is not a replacement for judgment. It is a shared starting point.<br>The payoff is that agents do not start from a vague chat prompt. They can read the same structured context that product, engineering, and architecture review. Repository-level instructions define the rules of the road. Declared <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">AI skills</a> operate on those files to carry context forward, enrich proposals, validate gates, or generate next-stage drafts. The agent becomes less like a magic text box and more like a runner operating inside the same workflow as the team.</p><p><strong>AI skills</strong> deserve a short explanation, because they are what makes any of this repeatable rather than a one-off prompt someone got right on a Tuesday. An <a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">agent skill</a> is a folder containing a SKILL.md file: YAML frontmatter with a name and a description, then the procedure itself written in plain markdown.</p><p>The description does more work than it appears to. An agent loads only the name and description of every installed skill up front, and reads the full body only when it judges the skill relevant to the task at hand. The description is effectively the trigger condition; the body is the procedure. Here is a trimmed skill that checks whether a document is complete enough to build against:</p><pre>---<br>name: check-context<br>description: &gt;-<br>  Verify that a design document states everything needed to build against it.<br>  Use before generating code, tickets, or automation from a document.<br>---<br><br># Check Context<br><br>1. Read the status file beside the document. Note the stage it is in and<br>   the systems the change touches.<br>2. Confirm the previous stage was approved. If it was not, stop and say<br>   which approval is missing.<br>3. For each system listed, confirm the document says what that system is<br>   expected to do. List any that it does not.<br>4. For each environment named, confirm the document states which backend<br>   it maps to. Do not infer the mapping.<br>5. Report what is missing and stop. Do not fill in the gaps yourself.</pre><h3>How it changed the work</h3><p>The biggest shift was not that agents wrote more code. It was that we put more attention on what happens before code.</p><p>Inception became the place to answer: what problem are we solving, for whom, and what is out of scope? PRDs became less like static approval documents and more like structured context for later decisions.</p><p>Implementation plans became a way to separate work that needs human judgment from work that is safe to delegate, automate, or parallelize.<br>That sounds close to normal product development, and in some ways it is. The difference is the second reader. The document is no longer only for a PM, engineer, or architect. It is also for the agent that will later be asked to summarize trade-offs, identify missing context, generate tickets, draft implementation steps, or review whether the plan still matches the stated goals.</p><blockquote>That changes the quality bar.</blockquote><p>A vague “support multiple environments” requirement is hard enough for humans. For agents, it is an invitation to fill in the blanks. A structured proposal that names the confusing environment topology, the affected services, the user friction, the success criteria, and the out-of-scope areas gives both humans and agents a better boundary.</p><p>It also changes stakeholder alignment. A product lead or engineering lead does not need to read every page to answer basic state questions. The dashboard and status files show what stage a proposal is in, what services it touches, which OKR it supports, and which epic tracks delivery. The longer documents remain available for the why.</p><p>The cross-proposal links are where this starts to feel less like documentation and more like a working context system. Model Monitoring metrics are environment-scoped in our data lake. AI Workbench needs an environment selector so users can view the right monitoring data from the right environment. That dependency is not just in someone’s head; it is captured in proposal context where humans and agents can discover it.</p><h3>What happened when we used it</h3><p>In-house Model Monitoring is the cleanest positive example. The problem was concrete. We had 100s of models on the platform, but only a handful were onboarded to a vendor solution. That left a lot of opportunity on the table without standardized monitoring by default. Teams could build their own bespoke checks, but the platform had no consistent baseline for model health. The first useful agent contribution was not code. It was research compression.</p><p>The team already had important context scattered across previous monitoring work, Vendor evaluation notes, lessons from previous iterations of Model Monitoring and vendor experience, internal docs, and tool comparisons. Instead of asking an engineer to manually reassemble all of that from memory, search, and old links, the agent helped pull in the background through the enrichment workflow.</p><p>That enrichment saved hours of manual research but more importantly, it changed the shape of the discussion. The PRD records a comparison of nine alternatives across drift detection, data quality, evaluation, explainability, scalability, integration, and cost. It also captures licensing risks that matter in a platform setting: Deepchecks carried an AGPL 3.0 risk, while Seldon Alibi Detect used a BSL 1.1 license that would require care for production use.</p><p>Those details did not remove human judgment. They made the judgment easier to exercise.</p><p>Engineers and leads could spend more time deciding what mattered: time to market, reuse of existing vendor patterns, avoiding previous operational failure modes, data lake as the metrics store, and AI Workbench as the visualization surface. The agent helped surface and organize the terrain.</p><p>Humans still owned the decision.</p><p>The status layer then made the work legible beyond the immediate authors. A reader could see that Model Monitoring was P1, tied to the embedded monitoring OKR, linked to its delivery epic, and touching multiple services. That is the alignment benefit. The two hours saved is useful, but the repeatable pattern matters more: every proposal has a research phase and an alignment phase. Structured context makes both easier to reuse.</p><h3>What is still hard: context drift</h3><p>Context drift is what happens when the shared context was once correct, but reality moves on. If a decision happens in Slack, Zoom, or a hallway conversation and the proposal never changes, the agent is immediately behind. It will continue to reason from the stale file. The output may look clean, but it targets an older version of the team’s understanding.</p><p>This is where the model is still immature. We have conventions, templates, status files, and skills. We do not yet have perfect drift detection. We do not have a reliable way to fail a build because a hallway decision was never committed to the context. The current answer is discipline, lightweight rituals, and making the cost visible.</p><p>Too much structure becomes bureaucracy. Too little structure becomes noisy agent output. The balance we are trying to find is the smallest amount of context that keeps humans aligned and agents pointed at the right problem.</p><h3>Start with one workflow</h3><blockquote>The context repository is not the product. The habit is.</blockquote><p>For another team starting this journey, I would not begin with a full operating model. Pick one workflow where agents already help, or already create noise. Move the important context for that workflow close to the work. Make it structured enough to review, reuse, and carry forward.<br>Then treat context freshness as part of delivery, not a documentation chore after delivery.</p><p>In an agentic development loop, the limiting factor is not always how fast code can be generated. Often, it is how clearly the team can express what should be generated, why it matters, and what must not be lost along the way.</p><p><a href="https://careers.expediagroup.com/life/">Learn about life at Expedia Group</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0bb9de03237c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/the-context-tax-of-agentic-development-0bb9de03237c">The Context Tax of Agentic Development</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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 Keras 3 Helped Modernise Expedia Group’s Lodging Ranking Stack]]></title>
            <link>https://medium.com/expedia-group-tech/how-keras-3-helped-modernise-expedia-groups-lodging-ranking-stack-7fec96f052fd?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/7fec96f052fd</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[neural-networks]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[keras]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Conor Worthington]]></dc:creator>
            <pubDate>Tue, 11 Aug 2026 11:01:02 GMT</pubDate>
            <atom:updated>2026-08-11T11:01:01.781Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/data">Expedia Group Technology — Data</a></h4><h4>What happened when we treated a framework migration as an architecture modernisation — and cut P99 inference latency by two-thirds</h4><figure><img alt="People crossing millennium bridge in London, St Pauls in the background" src="https://cdn-images-1.medium.com/max/1024/1*1RVgqmkJXoRIs0BTHPuTiA.png" /><figcaption>St Paul’s and millennium bridge, London</figcaption></figure><p><a href="https://www.expediagroup.com/">Expedia Group</a>™ has always been a market leader in providing personalised search experiences for travellers. As our ranking models evolved, we saw an opportunity not just to migrate to <a href="https://keras.io/keras_3/">Keras 3</a>, but to modernise the broader stack around it so we can better serve travellers. This led us to rewrite key parts of our pipelines that made <strong>model training 30% faster </strong>and <strong>cut P99 inference latency by two-thirds</strong>.</p><p>Our main focus in this blog is to discuss our improvements to lodging ranking — our service which handles users’ search requests and returns a personalised property ranking on top of a lightweight candidate generator. This service is frequently retrained to improve customer experience as we get new signals over time.</p><figure><img alt="Screenshot of Expedia hotel search results for London showing four ranked properties: Vintry &amp; Mercer Hotel (9.8, £1,537), Sea Containers London (9.4, £1,420), Pan Pacific London (9.8, £2,274), and The Lalit London (9.2, £1,554). Each card shows a photo, guest rating, OneKeyCash earnings, and nightly price — illustrating the personalised property ranking the lodging ranking model produces." src="https://cdn-images-1.medium.com/max/780/1*qJ3twHSpQc0nT-MQqrmj-w.png" /><figcaption>Example of personalised lodging ranking on Expedia search result page for a London search</figcaption></figure><p>As such, our velocity to make changes needs to be fast, but more importantly we need to be able to build models which are state-of-the-art, enabling customers to easily find and book the most relevant property for their trip.</p><h3>The problem with Keras 2, eager training and combined serving</h3><p>The challenge was not that the old stack was broken. It was that it had gradually become a limiting factor.</p><p>On the training side, we were carrying dependencies on older components and missing out on modern Keras APIs, new optimisers, new layers and cleaner support for accelerator-oriented training. On the serving side, we had a path that was simple and operationally familiar, but not especially well suited to aggressively optimised inference for heavier ranking architectures.</p><p>This blockage on utilising new layers and optimisers has become more pronounced as LLMs see blistering amounts of innovation. Naturally, these new features are only available in Keras 3 or as separate backends from our default TensorFlow-only serving. As Machine Learning Scientists, this significantly impacted our innovation velocity, as we either had to recreate layers — such as mixture of experts implementations in TensorFlow — or we could not simply utilise the functionalities (such as new optimisers like <a href="https://arxiv.org/pdf/2502.16982">Muon</a>).</p><p>That mattered more as we started thinking about models whose compute is dominated by <strong>dense matrix operations</strong>. Mixer- and transformer-style ranking models benefit heavily from GPU execution, graph compilation, kernel fusion and mixed precision. If the stack cannot take advantage of those capabilities cleanly, latency becomes the main constraint on what you can train and serve.</p><figure><img alt="Architecture diagram of the RankMixer model. A RankMixer Block stacks L layers of Token Mixing and Per-token FFN sublayers, taking hundreds of feature embeddings as input and outputting scores via mean pooling. Detail panels show Token Mixing splitting tokens across H groups, and a Sparse Mixture-of-Experts (SMoE) FFN variant using ReLU routing to direct inputs across multiple expert sub-networks." src="https://cdn-images-1.medium.com/max/1024/1*MvNmyHc9ZSx2LXe_qFT0Mg.png" /><figcaption>Example of a Mixer-style architecture dominated by dense matrix operations: <a href="https://arxiv.org/abs/2507.15551">RankMixer: Scaling Up Ranking Models in Industrial Recommenders</a> from Zhu et al, 2025</figcaption></figure><h3>The prior state: a coupled preprocessing and scoring graph</h3><p>In our ranking stack, all the feature transformations happen in preprocessing layers generated by <a href="https://github.com/ExpediaGroup/kamae">Kamae</a>. Kamae is our open source preprocessing repository, covered in <a href="https://arxiv.org/pdf/2507.06021">our 2025 RecSys paper</a>, that maps TensorFlow operations to Spark operations. This enables fast, distributed, and one-time preprocessing before training, as well as produces a single preprocessing graph for serving. Kamae handles operations such as categorical lookups, one-hot encoding and other transformations over raw request features, including string-heavy inputs such as country codes.</p><figure><img alt="A woman in a karate outfit; the mascot of Kamae" src="https://cdn-images-1.medium.com/max/640/1*ZesLz_IU_RJG8GaHmRNBcA.jpeg" /><figcaption>Kamae Logo</figcaption></figure><p>That design led naturally to <strong>a coupled serving graph</strong>: as TensorFlow supported the string transformations we needed, the simplest production architecture was to <strong>bundle preprocessing and the neural network</strong> together into one fused TensorFlow graph. That made testing and deployment straightforward, and for earlier model generations, it was a very reasonable trade-off.</p><figure><img alt="Architecture diagram of the old unified serving architecture. Request input features (raw strings, categorical, and numerical) flow into a single TensorFlow SavedModel containing tightly coupled Preprocessing (string lookup, one-hot encoding, parsing) and a Neural Net. The combined graph outputs scores, ranking, and results. A note below reads: “Preprocessing and scoring are tightly coupled inside one TensorFlow graph.”" src="https://cdn-images-1.medium.com/max/692/1*35z-BhtsZKmI0HFwh1vNpA.png" /><figcaption>Our original serving pattern for feature ingestion and scoring</figcaption></figure><p><strong>But the same architecture also created a ceiling</strong></p><p>Because preprocessing and scoring were tightly coupled, we could not easily move the neural network into more aggressively optimised runtimes <strong>without first separating those graphs</strong>. The fused graph was stable, but it limited how far we could push compiled serving and hardware-aware optimisation.</p><p>Our string-based operations in preprocessing could only be run with eager training in contrast to JIT (Just-In-Time compilation).</p><p>JIT support is not new to Keras 3 but is a major part of our migration. <strong>JIT</strong> means Keras traces the model into a static computation graph and<strong> hands it to XLA </strong>(accelerated linear algebra, an ML compiler)<strong>, which can fuse operations together enabling more efficient hardware utilisation</strong>. The main catch is that <strong>compiled execution is built around numeric tensor operations, not string processing</strong>, so preprocessing stages that depend on things like one-hot encoding from raw strings or other vocabulary-based transformations cannot cleanly be compiled into the same optimised graph.</p><h3>What Keras 3 brought — multiple backends and new operations</h3><p>Keras 3 was a fairly substantial shift for the ecosystem because it moved Keras away from being treated primarily as tf.keras and back toward being its own higher-level framework with a cleaner import keras API, a stronger backend-agnostic model definition style and support for writing more logic in framework-neutral primitives such as keras.ops rather than directly in TensorFlow ops.</p><figure><img alt="Side-by-side code comparison of an attention block in Keras 2 versus Keras 3. Keras 2 uses TensorFlow-namespaced ops (tf.matmul, tf.math.sqrt, tf.cast, tf.nn.softmax). Keras 3 replaces these with backend-agnostic ops (ops.matmul, ops.sqrt, ops.cast, ops.softmax), and makes the transpose explicit. A mapping table below summarizes the six key substitutions." src="https://cdn-images-1.medium.com/max/1024/1*z6BQefy3HXuiRRH9pWuVXA.png" /><figcaption>Example of code differences of moving from TensorFlow import to import Keras</figcaption></figure><p>In practical terms, that meant cleaner imports, more emphasis on portability, a newer native .keras serialisation format and some breaking API cleanup around how models, variables, reductions and layer resolution are handled, compared with older tensorflow.keras patterns.</p><p>Alongside that, the broader ranking stack around Keras also changed. Keras 3 made it much more natural to target newer execution paths and backends, while older TensorFlow-coupled utilities became less attractive to depend on long term.</p><p>One important example is TensorFlow Ranking: its deprecation was not a feature of Keras 3 itself, but it landed in the same general modernisation wave, and many teams migrating to Keras 3 have used that moment to drop the dependency entirely and move ranking losses, metrics, and supporting logic into their own libraries or directly onto standard Keras APIs.</p><figure><img alt="Image showing Keras logo and logos of JAX, TF and PyTorch" src="https://cdn-images-1.medium.com/max/1024/1*8GBjdaMf5t4fUBiktvdyXg.png" /><figcaption>Keras logo and various backend-tech logos</figcaption></figure><p>This has also opened new opportunities for us to move towards <strong>more backend-agnostic code</strong> which could enable the usage of PyTorch or JAX. This means, should we wish to use highly optimised kernels which do not have native bindings in our chosen backend, our layers can easily migrate to match. We still predominantly use TensorFlow, as our preprocessing for most models still requires strings, but we have added flexibility.</p><h3>Beyond Keras 3 migration — speeding up training and inference</h3><p>Rather than treating Keras 3 as just an API update, we used the migration to modernise the core ranking stack. That included rewriting layers, losses, metrics, and training logic to be Keras 3 native and XLA-safe, while preserving parity with existing production behavior.</p><p>This allowed us to run testing within our existing training and ensure performance parity while benefitting from the speed-ups and the cleaner, more backend-agnostic APIs.</p><p>Kamae operations were rewritten to use backend-agnostic operations, with the exception of string operations that are maintained for legacy support in TensorFlow only. For our current models which rely on string inputs, nothing changes and we can still produce parity. For future models we can now <strong>choose XLA-safe preprocessing layers</strong> and as a result, <strong>support different backends should we want the entire model graph in JAX for instance</strong>.</p><p>Once the neural-network portion of the stack was cleanly XLA-safe and Keras 3 native, a more optimised serving design became possible. The key architectural move was to split the old fused graph into two stages to retain compatibility with string-containing preprocessing. Our new separate graphs consist of:</p><ul><li>Preprocessing in TensorFlow, where the string-heavy transformations still belong</li><li>Neural network in Keras, comprising purely numeric operations and enabling the graph to be exported to an optimised runtime</li></ul><p>We currently make use of ONNX and TensorRT to provide our faster runtime for the trained graph.</p><h3>Speeding up the graph</h3><p><strong>ONNX</strong> is an open-standard intermediate representation for machine learning models. ONNX Runtime takes an ONNX graph and applies hardware-specific optimisations at load time (e.g. collapsing a MatMul + BiasAdd + ReLU) into a single fused kernel. Because ONNX Runtime is a purpose-built inference engine rather than a full training framework, it carries none of the overhead that TensorFlow or PyTorch bring to serving.</p><p><strong>TensorRT</strong> is NVIDIA’s GPU inference optimiser and runtime, designed to take a neural network graph and compile it into a form that runs more efficiently on specific NVIDIA hardware. It improves latency and throughput by doing things like kernel fusion (see example illustrated below), hardware-specific optimisation, and lower-precision execution such as FP16 where accuracy allows. It further refines ONNX graphs in our case and enables us to optimise for the GPU we specifically are serving on.</p><figure><img alt="Side-by-side comparison of an un-optimised versus TensorRT-optimised neural network graph. The left shows many separate nodes (conv, bias, relu) for each operation. The right collapses these into fused CBR (Conv+Bias+ReLU) blocks, reducing the number of nodes and simplifying the execution graph while preserving the same logical structure." src="https://cdn-images-1.medium.com/max/1024/1*TvgWc-F5me6QNWAz5yMMDQ.png" /><figcaption>Example of an unoptimised TF graph vs TensorRT graph where Conv+Bias+Relu are collapsed into one more efficient ‘CBR’ operation [1]</figcaption></figure><h3>Serving multiple graphs</h3><p>With these more optimised runtimes, we can ensemble the preprocessing and model graph together at serving time with <a href="https://github.com/triton-inference-server/server">Triton Inference Server</a>. Triton Inference Server is a serving platform which enables users to utilise multiple graphs with low cost memory passes and supports TensorRT natively. Triton configurations are able to be autogenerated as we already know how the graphs relate from preprocessing to training.</p><figure><img alt="Architecture diagram of the new serving design. A hydrated request enters a Triton Inference Server, where a Ranking Ensemble routes input to two sequential models: a TensorFlow preprocessing graph, which outputs a processed feature vector to an ONNX/TensorRT model graph. The final rankings data is returned as a Property Rankings JSON — with preprocessing and neural network now running as separate graphs." src="https://cdn-images-1.medium.com/max/1013/1*xpwDhzbS9QJG1HtxTJxRoA.png" /><figcaption>New proposed end-to-end serving architecture decoupling strings</figcaption></figure><p>This separation matters because ONNX and its runtimes are much better suited to aggressively optimised numeric-inference than to vocabulary based string-transformation. ONNX Runtime can apply graph optimisations such as operator fusion and hardware-specific execution-provider selection, TensorRT can then go further on NVIDIA GPUs through <strong>device-specific compilation, kernel selection, and mixed-precision execution.</strong></p><p>So the migration ultimately did<strong> two things at once</strong>: it <strong>modernised the training stack</strong> and it created a cleaner boundary that made <strong>compiled serving practical</strong> but still able to combine both graphs should we need singular graph TF-Serving for legacy support.</p><h3>What we gained in training</h3><p>Some of the biggest wins from the migration showed up before inference even began.</p><p>Our earlier staging pipeline was slow and heavily constrained by serial work. In the Keras 3 rewrite, our data handling was redesigned around more parallel Spark-based handling and improved shard management. This aligned with our design philosophy behind Kamae of having these heavy preprocessing ops parallelized by Spark. <strong>On representative production workloads, that represented a 75% speed up in writing our datasets from preprocessing.</strong></p><figure><img alt="Bar chart comparing training time between Keras 2 and Keras 3. Keras 2 Trainer baseline: 1.00. Keras 3 Trainer: 0.69 — a 31% reduction. Time to train is scaled relative to Keras 2; lower is better." src="https://cdn-images-1.medium.com/max/1024/1*I6zGRHQCi-oH2wbfRcx7Ew.png" /><figcaption>Training of our models — scaled by percentage of time spent training relative to Keras 2, lower is better</figcaption></figure><p>Training also became substantially faster. On a representative production ranking model, <strong>training has become 30% faster </strong>since enabling JIT and rewriting our graph to be JIT friendly without any cost to our metrics.</p><p>As part of the rewrite, some of the legacy code which handled callbacks such as variable batch size was refactored to <strong>enable multi-GPU training.</strong> This has allowed us to throw much more compute at problems — for some of our models this means <strong>training is down from a working week to a day</strong>.</p><p>Those changes are important not just because they save time, but because they change the cost of experimentation. Faster staging and shorter training cycles mean teams can evaluate more ideas, iterate on larger models quicker, and spend less of their time waiting on experiments to complete.</p><h3>What we gained in serving</h3><p>The serving gains were just as important.</p><p>In isolated neural-network benchmarks, each step toward a more compiled runtime improved latency. On our old CPU-oriented serving model we saw ONNX providing major latency reductions both when we tested the model graph in isolation and tested it end to end.</p><figure><img alt="Stacked bar chart of CPU end-to-end serving latency split by preprocessing and neural network time. Keras 2 TF-serving: preprocessing 0.60, neural network 0.40, total 1.0. Keras 3 Triton: preprocessing unchanged at 0.60, neural network reduced to 0.05, total 0.65. Latency scaled relative to Keras 2; lower is better." src="https://cdn-images-1.medium.com/max/1024/1*lL2UmXHmC9Ht0VJvO65TCA.png" /><figcaption>Inference of CPU based models broken down by time spent in preprocessing vs in the neural network graph. Latency scaled relative to time to serve Keras 2 on TF-serving</figcaption></figure><p>We can see that while our preprocessing cost has not changed at all, <strong>we observe a reduction of 35% in total compute time due to the model graph speeding up.</strong> This is encouraging and with some of our model architectures being far more complex within the model graph than the preprocessing graph, which should enable us to better utilise our resources.</p><p>Utilising TensorRT means we can make use of hardware optimisation for mixed precision. Mixed precision means running most inference in lower precision where it is safe to do so, and in our Triton setup we use TensorRT with FP16 so the neural network can make better use of its resources. One question that naturally comes up with compiled GPU serving is whether FP16 changes ranking quality in ways that matter.</p><p>In practice, mixed precision looked like a safe trade-off for this class of model. Across our simulations, FP16 inference achieved an RBO (Rank-Biased Overlap, a measure of similarity of two ranked lists where disagreements at the top of a list matter much more than disagreements at the bottom) <strong>greater than 99% relative to the higher-precision baseline</strong>, indicating that ranking order stayed effectively unchanged while still benefiting from faster TensorRT execution.</p><figure><img alt="Bar chart of model-only offline GPU latency across three serving configurations. Keras 2 TF-Serving baseline: P99 1.0, P50 0.25. Keras 3 Triton: P99 0.66, P50 0.12. Keras 3 TensorRT FP16: P99 0.14, P50 0.07. Latency scaled relative to Keras 2 P99; lower is better." src="https://cdn-images-1.medium.com/max/1024/1*6RuqkxVySgf-Nw1gFDdeBA.png" /><figcaption>Inference of GPU based neural network component. Latency scaled relative to time to serve Keras 2 on TF-serving at P99. Broken down into P99 and P50 for each serving</figcaption></figure><p>We broadly see a substantial improvement across the board as we move to better optimised runtimes. Our P99 is higher than expected on our Triton ONNX serving pattern but still substantially lower than TF-Serving.</p><p>We then deployed our new service and compared it end to end, including preprocessing, hitting it with real traffic from <a href="https://www.expedia.com/">Expedia</a>®.</p><figure><img alt="Bar chart comparing end-to-end GPU serving latency across three configurations. Keras 2 TF-serving baseline: P99 1.0, P50 0.30. Keras 3 Triton: P99 0.30, P50 0.15. Keras 3 TensorRT FP16: P99 0.29, P50 0.10. Latency is scaled relative to Keras 2 P99; lower is better." src="https://cdn-images-1.medium.com/max/1024/1*-6mIS1LD4rylxFyh_UoS2g.png" /><figcaption>Inference of GPU based entire model i.e preprocessing and neural network. Latency scaled relative to time to serve Keras 2 on TF-serving at P99. Broken down into P99 and P50 for each serving.</figcaption></figure><p>That kind of improvement matters because it reduces tail latency and creates room for more expressive architectures. <strong>In this case our prior P50 is now our P99</strong>. Further improvements can be seen on TensorRT FP16 where our <strong>P50 latency is reduced to only 10% of our original P99 latency</strong> on TF-Serving.</p><h3>Why this mattered beyond one migration</h3><p>The most important outcome of this work is more than any one benchmark. It is that the stack now has a clearer shape while retaining total parity with our old stack.</p><p>Preprocessing and neural-network execution are separated to keep strings isolated. Training is built around Keras 3 native, XLA-safe components. Serving still supports our old bundled TF-Serving approach but now has practical faster compiled paths through Triton. The same pattern can be reused for future ranking models that need better accelerator utilisation and lower latency than the previous architecture could comfortably provide.</p><p>Once a stack can train efficiently, export separate numeric model graphs cleanly and serve them through a compiled inference path, it becomes much easier to operationalize models that would otherwise be too latency-sensitive to deploy.</p><p>Further, we have been unblocked on utilisation of code from other backends now and are much more capable to run experiments with newer features such as optimisers like Muon. This has massively increased the velocity of experimentation.</p><p><strong>Modernisation is often described as maintenance work. Sometimes, it is. But sometimes, modernisation is what makes the next generation of systems possible. For us, Keras 3 was exactly that kind of migration.</strong></p><h3>Acknowledgements</h3><p>This would not have been possible without the fantastic work of the Expedia Group Search Ranking AI team as a whole. However, a special shoutout must go to <a href="https://medium.com/@georgebarrowclough">George Barrowclough</a>, James Shinner and <a href="https://medium.com/@d.donghi">Daniele Donghi</a> who actively assisted with the Keras migration alongside maintenance of our core ranking repositories. A further thanks to <a href="https://medium.com/@rajatmalhotra.2798">Rajat Malhotra</a> who assisted with productionalisation and testing of the new serving pattern. Finally thanks to <a href="https://medium.com/@annemorvan">Anne Morvan</a>, <a href="https://medium.com/@tio.hafiz">Hafiz Tiomoko Ali</a>, <a href="https://medium.com/@youssefachenchabe">Youssef Achenchabe</a> and Zoe Yang for their careful review of the article and valuable feedback.</p><p>[1]: <a href="https://developer.nvidia.com/blog/deploying-deep-learning-nvidia-tensorrt/">Deploying Deep Neural Networks with NVIDIA TensorRT</a> (Gray et al., 2017)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7fec96f052fd" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/how-keras-3-helped-modernise-expedia-groups-lodging-ranking-stack-7fec96f052fd">How Keras 3 Helped Modernise Expedia Group’s Lodging Ranking Stack</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[Focus on the Feature, Not the Fixture: GenAI powered GraphQL mocks]]></title>
            <link>https://medium.com/expedia-group-tech/focus-on-the-feature-not-the-fixture-genai-powered-graphql-mocks-ea069670af02?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea069670af02</guid>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Samuel Vazquez]]></dc:creator>
            <pubDate>Fri, 31 Jul 2026 11:01:02 GMT</pubDate>
            <atom:updated>2026-07-31T11:01:01.722Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/engineering">Expedia Group Technology — Engineering</a></h4><h4>Combine a GraphQL schema, hints and a LLM to generate contextual GraphQL mock responses</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ga13LGqLNk-1OzWLjXYZrA.jpeg" /><figcaption>Photo by Samuel Vazquez somewhere in New Zealand</figcaption></figure><p>A product developer on our navigation header team spent an afternoon hand-typing a 200-line GraphQL JSON mock response so they could keep building the UI while a resolver was pending to be implemented. The schema changed the next morning. We threw the mock away. That is the boring tax on every GraphQL prototype: mocks that drift, fixtures that rot, frontends blocked on backends, and demos slipping because nobody wanted to update the same mock data again. Mock data is just data. It should not be the most expensive part of trying an idea.</p><h4>GenAI + GraphQL: a match made in heaven</h4><p>Most tools that promise “AI generates an API” share the same flaw: the model invents a shape from scratch, and you spend the afternoon reshaping its output to match your actual types.</p><p>GraphQL flips that. The selection set is the spec. The schema is the type system. Hand an LLM a query and it already knows the exact JSON it must return, field by field, type by type. That’s the unlock: LLMs are bad at inventing shapes and great at filling them in, and GraphQL hands them a bounded shape for free.</p><p><a href="https://github.com/ExpediaGroup/mockql-rs">mockql-rs</a> is what happens when you take that pairing seriously: mark what you want mocked directly in the query with a “@mock” directive:</p><pre>query LoyaltyRewards @mock {<br>  loyaltyRewards {<br>    heading @mock(hint: &quot;Platinum member&quot;) { <br>      text <br>    }<br>    subtitles @mock(hint: &quot;At least 4 items&quot;) { <br>      text <br>      theme <br>    }<br>  }<br>}</pre><p>Annotate the field, add a hint, keep building. <strong>mockql-rs</strong> parses the operation, validates it against the schema, and prompts an LLM to fill in <strong>only</strong> the fields you annotated with “@mock”.</p><pre>{<br>  &quot;data&quot;: {<br>    &quot;loyaltyRewards&quot;: {<br>      &quot;heading&quot;: { &quot;text&quot;: &quot;Welcome back, Platinum Member&quot; },<br>      &quot;subtitles&quot;: [<br>        { &quot;text&quot;: &quot;2,450 points until your next reward&quot;, &quot;theme&quot;: &quot;HIGHLIGHT&quot; },<br>        { &quot;text&quot;: &quot;3 nights earned this quarter&quot;, &quot;theme&quot;: &quot;STANDARD&quot; },<br>        { &quot;text&quot;: &quot;Breakfast included on your next stay&quot;, &quot;theme&quot;: &quot;POSITIVE&quot; },<br>        { &quot;text&quot;: &quot;Suite upgrade available at checkout&quot;, &quot;theme&quot;: &quot;POSITIVE&quot; }<br>      ]<br>    }<br>  }<br>}</pre><p>No handwritten fixtures. The mock lives in the query and the response matches the schema, always. GraphQL stays the contract. The LLM never invents an API, it fills in the shape that the schema has already drawn.</p><h4>Contextual mocking</h4><p>The real power of <strong>mockql-rs</strong> shows up when you need <strong>most</strong> of a response from your actual backend but one field isn’t ready, or even available yet.</p><pre>query TripDetails($id: ID!) {<br>  trip(id: $id) {<br>    property {<br>      name<br>      address<br>    }<br>    recommendations @mock(hint: &quot;5 most popular nearby restaurants&quot;) {<br>      title<br>      description<br>      distance<br>    }<br>  }<br>}</pre><pre>{<br>  &quot;data&quot;: {<br>    &quot;trip&quot;: {<br>      &quot;property&quot;: { // &lt;- Real upstream data<br>        &quot;name&quot;: &quot;Hotel Palazzo Pischedda&quot;,<br>        &quot;address&quot;: &quot;Via Roma, 09089 Bosa OR, Italy&quot;<br>      },<br>      &quot;recommendations&quot;: [ // &lt;- Context-aware GenAI mock data<br>        {<br>          &quot;title&quot;: &quot;Ristorante Sa Pischedda&quot;,<br>          &quot;description&quot;: &quot;Located directly at the hotel, famous for its authentic Sardinian seafood dishes, fresh handmade pasta, and excellent local Vermentino wine.&quot;,<br>          &quot;distance&quot;: &quot;0.0 miles&quot;<br>        },<br>        {<br>          &quot;title&quot;: &quot;Locanda di Corte&quot;,<br>          &quot;description&quot;: &quot;Charming restaurant nestled in the historic medieval center, serving traditional Bosa cuisine with a modern twist in an intimate courtyard.&quot;,<br>          &quot;distance&quot;: &quot;0.3 miles&quot;<br>        },<br>        {<br>          &quot;title&quot;: &quot;Ristorante Borgo Sant&#39;Ignazio&quot;,<br>          &quot;description&quot;: &quot;Cozy, rustic eatery offering hearty Sardinian meats, local cheeses, and traditional dishes inside a beautifully restored stone building.&quot;,<br>          &quot;distance&quot;: &quot;0.2 miles&quot;<br>        },<br>        {<br>          &quot;title&quot;: &quot;Aldualis Ristorante&quot;,<br>          &quot;description&quot;: &quot;Slightly elevated restaurant outside the historic center offering a refined contemporary take on regional recipes with stunning views over Bosa.&quot;,<br>          &quot;distance&quot;: &quot;0.6 miles&quot;<br>        },<br>        {<br>          &quot;title&quot;: &quot;Ristorante Ponte Vecchio&quot;,<br>          &quot;description&quot;: &quot;Lovely riverside venue near the old bridge, specializing in fresh catch-of-the-day fish and traditional Sardinian lobster dishes.&quot;,<br>          &quot;distance&quot;: &quot;0.1 miles&quot;<br>        }<br>      ]<br>    }<br>  }<br>}</pre><p>The real backend resolves the trip property, the LLM resolves real recommendations for that particular property. Your frontend gets a completely cohesive, realistic data without a single hardcoded fixture or configuration.</p><h4>How it works ?</h4><p><strong>mockql-rs</strong> sits between your GraphQL client and your GraphQL server, as a thin layer, a standalone CLI.</p><h4>Why a CLI ?</h4><p>a CLI is the smallest integration surface. Any client can use it because every language, test runner, CI job, Makefile, npm script, or demo environment <strong>can execute a process</strong>. You do not need an SDK, Apollo Client link, Relay or any backend framework. You hand to a process<strong> </strong>an operation, a schema, an upstream GraphQL URL, and a provider, and it returns a GraphQL response.</p><h4>Execution flow</h4><figure><img alt="Diagram of mockql-rs execution flow: Parse, Split, Prompt, and Merge stages showing how a GraphQL operation and schema are validated, split into real and mocked fields, sent to an LLM with a structured prompt, and merged with upstream data into a single GraphQL response." src="https://cdn-images-1.medium.com/max/1024/1*I658xjSqeAIN-ZnOpUpNww.png" /><figcaption>The four-stage mockql-rs pipeline: parse and validate against the schema, split real fields from <a href="http://twitter.com/mock">@mock</a>-annotated ones, prompt the LLM with schema constraints and upstream context, then merge everything into a single GraphQL response.</figcaption></figure><p>When <strong>mockql </strong>executes a GraphQL request it will:</p><ol><li><strong>Parse: </strong>the operation is parsed and validated against your schema using the <a href="https://github.com/apollographql/apollo-rs/tree/main/crates/apollo-compiler">apollo-compiler.</a></li><li><strong>Split: </strong>fields annotated with “@mock” are separated from real fields. Real fields are forwarded to the upstream server as normal. For those familiar with GraphQL Federation, this process is very familiar to <a href="https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/reference/query-plans">query planning</a>.</li><li><strong>Prompt: </strong>the operation, mocked fields, hints, and the subset schema are assembled into a structured prompt. The operation and the schema constrain the LLM’s output shape; it can’t hallucinate fields that don’t exist or return a string where the schema expects an enum.</li><li><strong>Merge:</strong> real upstream data and LLM-generated mock data are stitched back into a single response.</li></ol><h4>Why now ?</h4><p>GraphQL response mocking is finally practical. LLMs are exceptionally good following schemas and <strong>rust + apollo-compiler</strong> makes parsing and validation blazingly fast.</p><p><strong>Try it</strong></p><pre>mockql oneshot \<br>  --operation ./my-query.graphql \<br>  --graphql-url https://your-graphql-server/graphql \<br>  cli --provider claude</pre><p>Pick one handwritten fixture blocking your prototype. Replace it with “@mock”. Ship the demo on Friday.</p><ul><li><a href="https://github.com/ExpediaGroup/mockql-rs">Repository</a></li><li><a href="https://github.com/ExpediaGroup/mockql-rs/blob/main/docs/mock-specification.md">@Mock Spec</a></li><li><a href="https://opensource.expediagroup.com/mockql-rs/docs/">Getting Started</a></li></ul><p><a href="https://careers.expediagroup.com/life/">Learn about life at Expedia Group</a>™</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea069670af02" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/focus-on-the-feature-not-the-fixture-genai-powered-graphql-mocks-ea069670af02">Focus on the Feature, Not the Fixture: GenAI powered GraphQL mocks</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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 Expedia Group Builds AI That Lasts at Scale]]></title>
            <link>https://medium.com/expedia-group-tech/how-expedia-group-builds-ai-that-lasts-at-scale-434677770fe9?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/434677770fe9</guid>
            <category><![CDATA[data]]></category>
            <category><![CDATA[innovation]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Xavier Amatriain]]></dc:creator>
            <pubDate>Tue, 14 Jul 2026 11:01:01 GMT</pubDate>
            <atom:updated>2026-07-14T11:01:01.345Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/innovation">Expedia Group Technology — Innovation</a></h4><h4>A framework for how we build, deploy, and evolve AI systems for impact and scale</h4><figure><img alt="A panoramic view of central Madrid at sunset" src="https://cdn-images-1.medium.com/max/1024/0*TMheBxvHKcnhWDLq" /><figcaption>Photo by Florian Wehde on Unsplash</figcaption></figure><p>There’s an important distinction between Artificial Intelligence (AI) that just works today and AI that lasts at scale. Many companies optimize hard for the first one without ever asking whether they’re building the second.</p><p>Velocity without discipline and strategic direction is a liability, not an asset. The hardest part of building AI at scale isn’t getting a model to work once. It’s building systems that continue to work, scale beyond individual teams and use cases, and improve consistently over time.</p><p>Today’s AI systems do more than just predict and optimize. They converse, reason, and increasingly take action. An autonomous system making decisions on a traveler’s behalf creates a very different set of expectations around reliability, governance, and accountability. As AI takes on more of those roles, the principles behind how these systems operate matter more than ever.</p><p>At <a href="https://www.expediagroup.com/">Expedia Group</a>™, we have spent years applying AI and machine learning across the traveler journey from personalization, ranking, and recommendations, to fraud prevention, customer support, and, more recently, generative and agentic AI experiences. That depth of experience is what led us to develop a set of machine learning and AI principles to guide how we build, deploy, and evolve AI systems across the company.</p><p>The goal is simple: make sure the systems we build create real business value, scale across the company, and operate safely. These principles define how we measure, design, govern, and operate the systems we use.</p><h3><strong>From principles to practice</strong></h3><p>Publishing principles is the easy part. The harder and more important work is turning them into operating mechanisms: recommendations, requirements, tooling, and release processes that teams actually use. At Expedia Group, we have started doing this through Agentic Release tollgates: a set of recommended and, in some cases, required checks before launching agentic AI features. These tollgates translate principles like clear ownership, risk-based governance, evaluation, safe rollout, and monitoring into concrete expectations for teams. Some of these recommendations and requirements are already being automated and integrated into the software development life cycle (SDLC). Over time, the goal is for these expectations to become embedded in how we design, evaluate, approve, launch, and monitor AI systems from the start.</p><h3><strong>Outcomes: measuring what actually matters</strong></h3><p>The first test for any model at Expedia Group isn’t whether it improves a technical metric. It’s whether it improves a business outcome and, ultimately, the traveler experience.</p><ul><li><strong>Align models to metrics with business impact: </strong>Every machine learning effort must tie directly to a key business outcome or traveler experience metric. Technical optimizations are useful midpoints, not end goals<strong>.</strong></li><li><strong>Optimize for return on cost</strong>: The value a model creates has to justify what it costs to develop, train, and monitor it, plus the operational complexity it adds. Favor solutions that deliver lasting impact relative to what they cost to run.</li><li><strong>Justify complexity against strong baselines: </strong>Complexity should be earned, not assumed. Start with a strong baseline: an existing general model, a simple heuristic, an off-the-shelf solution. Reach for specialized models or more complex architectures only when simpler options genuinely can’t meet the bar.</li><li><strong>Require both offline and online evaluation</strong>: No model goes to broad deployment on offline validation alone or jumps straight to A/B testing. Every model must perform in both offline and online evaluations. Over time, our offline evaluations should reliably predict what we see online.</li></ul><h3><strong>Design: building systems that scale beyond the teams that build them</strong></h3><p>Getting a model to work is one challenge. Making its value extend beyond a single team or use case is the harder one.</p><ul><li><strong>Build on shared foundations; specialize only when justified:</strong> Favor shared, platform-wide foundations for core capabilities, data representations, and model building blocks. Specialization should build on those foundations, not spin up isolated stacks, so when the foundation improves, the gains flow across the organization.</li><li><strong>Treat data as a core product</strong>: A model’s quality is bounded by the quality of its data. We need to maintain robust pipelines, clear lineage, reproducibility, and reusable features built with documented ownership, clear schemas, and SLAs that other teams can rely on.</li><li><strong>Prioritize generality over local optimization</strong>: When two approaches perform similarly, favor the one whose learnings, assets, and operating patterns can be reused across teams, brands, and use cases. We should optimize not just for local performance, but for how quickly improvements can diffuse across the company and compound over time.</li><li><strong>Minimize and sunset manual business rules: </strong>Manual rules are sometimes necessary for policy, safety, or compliance, but they should be explicit and reviewed regularly, never silent patches for weak models or a source of permanent maintenance debt.</li><li><strong>Reproducibility and traceability by default</strong>: Training data, features, configurations, evaluation results, deployment versions, and key decisions should all be documented and recoverable. That’s what lets you debug a production issue months later and hand off ownership without losing institutional knowledge.</li></ul><h3><strong>Trust: ownership, governance, and operating responsibly at scale</strong></h3><p>The bar for deploying AI isn’t just “does it work?” It’s “can we stand behind it?” Trust isn’t something you add at the end; it’s earned over time and maintained across the full lifecycle of every model we ship.</p><ul><li><strong>Assign clear ownership and accountability:</strong> Every model needs defined ownership across its lifecycle: a business owner, a product owner, an AI owner, and an operational owner. These don’t need to be four people, but the responsibilities must be explicit. Who’s accountable for outcomes? Who responds if the model drifts? Who answers the incident at 2 a.m.? Without this in place, models become orphaned and problems surface with no one to own them.</li><li><strong>Adhere to standards and governance:</strong> AI and machine learning models must use approved platforms and comply with established company standards, release gates, and governance processes. Operating outside these guardrails requires a clear, defined path to remediation or deprecation, rather than an open-ended exception.</li><li><strong>Govern proportionally to risk</strong>: The level of review, evaluation rigor, and human oversight should scale with a model’s impact. A customer-facing model that affects pricing or availability for millions of travelers demands a far higher bar than an internal tool used by a small team. For high-impact, safety-sensitive, or highly autonomous systems, human-in-the-loop checkpoints are built in from the start.</li><li><strong>Design for fairness, privacy, and transparency</strong>: We build with traveler and partner trust in mind: actively testing for unintended bias, practicing strong data guardrails, and favoring explainability when decisions meaningfully affect users. These are designed from the start, not added on.</li><li><strong>Design for safe rollout, rollback, and control</strong>: Deployments are progressive, with rollback paths, fallback mechanisms, and circuit breakers ready before launch. The ability to safely undo a deployment matters as much as the ability to ship it.</li><li><strong>Monitor continuously and adapt:</strong> Once live, teams need to actively monitor quality, drift, latency, cost, and business performance and retrain or recalibrate when the data shifts. A team should always be able to explain how its model is performing now, not just how it performed when it launched. For example, we recently introduced algorithm score cards. Updated regularly, these scorecards provide a simple, consistent framework for reviewing how key marketplace algorithms support business goals, perform in practice, and contribute to marketplace health.</li></ul><p>These principles do more than define how we build. They define what we’re willing to ship and how we stand behind it. In a world where AI systems are increasingly consequential and make real decisions for real travelers and partners, these standards matter. Applied consistently, they build responsible AI that lasts.</p><p><a href="https://careers.expediagroup.com/life/">Learn about life at Expedia Group</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=434677770fe9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/how-expedia-group-builds-ai-that-lasts-at-scale-434677770fe9">How Expedia Group Builds AI That Lasts at Scale</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[Using LLMs to Analyze Spark SQL Plans: A Practical Approach to Debugging Long-Running Jobs]]></title>
            <link>https://medium.com/expedia-group-tech/using-llms-to-analyze-spark-sql-plans-a-practical-approach-to-debugging-long-running-jobs-35eace7eeec4?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/35eace7eeec4</guid>
            <category><![CDATA[apache-spark]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[innovation]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[Sagibhuvana]]></dc:creator>
            <pubDate>Tue, 30 Jun 2026 11:01:01 GMT</pubDate>
            <atom:updated>2026-06-30T11:01:00.959Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/innovation/home">Expedia Group Technology — Innovation</a></h4><h4>Using large language models to reveal bottlenecks in Spark SQL execution plans</h4><figure><img alt="Tall green forest with dense, straight tree trunks and moss-covered ground, with a single person walking near the center of the scene" src="https://cdn-images-1.medium.com/max/1024/1*mI08ZnXv1LyQDEzzemLJrA.jpeg" /><figcaption>Photo by Luis del Río</figcaption></figure><p>If you’ve ever stared at a 300-plus-node physical plan at 2 a.m. trying to spot a missing broadcast or one cursed skewed partition, this is for you.</p><p>Spark makes it deceptively easy to write complex SQL that <em>looks</em> correct but quietly turns into a performance and cost problem at scale. A query that runs fine on day one can slow to a crawl as data grows, joins get wider, and aggregations become more nested. Suddenly, jobs take hours instead of minutes, clusters need to be over-provisioned, and costs climb with no clear explanation.</p><p>When things go wrong, the situation gets worse. Debugging Spark SQL often means wading through deeply nested physical plans, fragmented UI metrics, and stage-level statistics that don’t clearly point to the root cause. Engineers are left guessing whether the issue is a bad join strategy, skewed data, excessive shuffling, or memory pressure — turning optimization into a time-consuming, trial-and-error exercise.</p><p>So we built an automated, LLM-powered workflow that reads Spark SQL plans, flags performance anti-patterns, and suggests concrete fixes. Here’s why we built it, how it works, and what we learned after running it on real workloads.</p><figure><img alt="Diagram of a Spark execution plan with multiple light blue nodes connected by lines across a white canvas, illustrating the flow and relationships between stages or operations in a long-running job." src="https://cdn-images-1.medium.com/max/1024/1*KQB6Cl1PUX4tUPhY9VO6-A.png" /></figure><h3>Why we even needed this</h3><p>Across teams we kept bumping into the same pain points:</p><ul><li>Slow or failing jobs that needed deep Spark expertise to debug</li><li>SQL that didn’t behave the way people assumed</li><li>Skew, spills, or partition issues hiding beneath the surface</li><li>Tuning efforts that were reactive and inconsistent</li></ul><p>At some point, the obvious question came up:</p><blockquote>Can an LLM look at a Spark SQL plan and tell you what’s likely to go wrong — where cost will spike, where execution will drag, and where seemingly harmless SQL hides real operational risk?</blockquote><p>Turns out: <strong>yes</strong></p><h3><strong>How we approached it</strong></h3><h4><strong>1. Bring in real context with the Spark MCP server</strong></h4><p>We used an open-source Spark MCP server that exposes:</p><ul><li>SQL execution metadata</li><li>Final and physical plans</li><li>Stage and task metrics</li><li>Shuffle, spill, and I/O stats</li></ul><p>This gave us clean, structured insight into the slowest SQL statements for each app — perfect for an LLM to work with.</p><p><strong>Reference:</strong> GitHub — kubeflow/mcp-apache-spark-history-server (list_slowest_sql_queries).</p><h4><strong>2. Replace free-form chat with pattern-guided prompting</strong></h4><p>Early attempts with generic prompts failed.</p><p>“Analyze the SQL plan and suggest optimizations.”</p><p>The prompt was too general, and produced inconsistent results, hallucinated insights, and missed actual issues.</p><p>So we stopped being vague.</p><p>We defined clear, rule-driven <strong>anti-patterns with strict criteria</strong>. The model’s job became simple: detect only these patterns, nothing else. This made the analysis consistent and grounded in real metrics.</p><h4><strong>3. Enforce traceability</strong></h4><p>Every finding has to point to actual evidence: stage IDs, task IDs, node IDs, operator names, and table references. All output goes into a strict JSON format so downstream systems can consume it without guessing.</p><h3>What the model detects</h3><p>We focused on a small set of <strong>high-impact Spark SQL execution risks</strong>. What makes this different from traditional tooling is <strong>traceability</strong>: the LLM doesn’t just flag a problem — it ties it back to <strong>specific stages, plan nodes, and data characteristics</strong>, explains <em>why</em> Spark behaved that way, and points engineers to the exact lever to pull.</p><p>In every case below, the workflow was the same:</p><blockquote>The LLM surfaced a concrete risk → we verified it in the physical plan → we made a targeted change → we measured the impact.</blockquote><h3>Missed Broadcast Joins</h3><p>At first glance, the SQL looked reasonable. Nothing obviously wrong.<br>Yet one of our job consistently ran for over an hour.</p><h4>What the LLM surfaced</h4><p>The model traced the bottleneck to a SortMergeJoin and highlighted something Spark had quietly ignored:</p><p><em>One side of the join was </em><strong><em>~96 MB</em></strong><em> — small enough to broadcast — yet Spark still chose a shuffle join.</em></p><pre>{<br>  &quot;name&quot;: &quot;Missing Broadcast Joins&quot;,<br>  &quot;detections&quot;: [{<br>    &quot;stage_id&quot;: &quot;10.0&quot;,<br>    &quot;join_details&quot;: {<br>      &quot;right&quot;: {<br>        &quot;name&quot;: &quot;destination_attribute&quot;,<br>        &quot;size_human&quot;: &quot;96.6 MiB&quot;<br>      },<br>      &quot;chosen_strategy&quot;: &quot;SortMergeJoin&quot;,<br>      &quot;expected_strategy&quot;: &quot;BroadcastHashJoin&quot;<br>    },<br>    &quot;notes&quot;: &quot;Broadcast threshold likely too low; AQE skew handling favored SMJ.&quot;<br>  }]<br>}</pre><h4><strong>How we validated it</strong></h4><p>We went back to the physical plan. The screenshot below confirms exactly what the LLM predicted: a shuffle-heavy join where a broadcast should have been possible.</p><figure><img alt="Spark execution plan highlighting a shuffle-heavy join and skew in the physical plan." src="https://cdn-images-1.medium.com/max/1024/1*b7uX2iJzPLWiIRgNcPWUrw.png" /></figure><h4><strong>Impact:</strong></h4><p>We increased the broadcast threshold and the Runtime dropped from <strong>~1 hour to ~30 minutes</strong> — with zero query rewrites.</p><h3>Skewed partitions</h3><p>Another job technically “succeeded”, but ran for <strong>more than three hours</strong> with a long tail of straggler tasks.</p><h4><strong>What the LLM surfaced</strong></h4><p>The model didn’t just say <em>“there is skew.”</em><br>It pinpointed <strong>where</strong> and <strong>how bad</strong> the skew was:</p><ul><li>Median partition size: <strong>0 B</strong></li><li>Largest partition: <strong>~280 GB</strong></li></ul><pre>{<br>  &quot;name&quot;: &quot;Skewed Partitions&quot;,<br>  &quot;detections&quot;: [{<br>    &quot;stage_id&quot;: 35,<br>    &quot;node_type&quot;: &quot;SortMergeJoin(skew=true)&quot;,<br>    &quot;partition_sizes&quot;: {<br>            &quot;min_human&quot;: &quot;0.0 B&quot;,<br>            &quot;med_human&quot;: &quot;0.0 B&quot;,<br>            &quot;max_human&quot;: &quot;279.7 GiB&quot;,<br>            &quot;partition_count&quot;: 16<br>          },<br>    &quot;likely_cause&quot;: &quot;Extreme skew detected in join output.&quot;<br>  }]<br>}</pre><h4><strong>How we validated it</strong></h4><p>Looking at the SQL plan and join keys, the issue became obvious:<br>a left join with <strong>null-heavy keys</strong> was funneling almost all data into a single partition.</p><figure><img alt="Spark plan diagram showing data concentrated into a single partition during a null-heavy join." src="https://cdn-images-1.medium.com/max/524/1*N9q80DXTxc1JTDiOPs765g.png" /></figure><h4><strong>Impact</strong></h4><p>We fixed the join logic to handle null keys correctly. A job that previously ran <strong>3+ hours</strong> brought back to expected runtimes.</p><h3>Full table scans from missing filters</h3><p>This pattern is especially painful — and easy to miss.</p><h4><strong>What the LLM surfaced</strong></h4><p>The model flagged a table scan in one of our jobs that was silently reading <strong>23.5 TB</strong> of data — with <strong>no partition or date filters applied</strong>.</p><pre>{<br>  &quot;name&quot;: &quot;Missing Partition or Date Filters&quot;,<br>  &quot;detections&quot;: [{<br>    &quot;table&quot;: &quot;lodging_content_vrbo_room_type_profile_history&quot;,<br>    &quot;input_size_human&quot;: &quot;23.5 TiB&quot;,<br>    &quot;partition_filters&quot;: [],<br>    &quot;pushed_filters&quot;: [],<br>    &quot;issue&quot;: &quot;Full table scan without partition filters.&quot;<br>  }]<br>}</pre><h4><strong>How we validated it</strong></h4><p>The physical plan confirmed it: a full scan of a massive Iceberg table, triggered by a missing predicate.</p><figure><img alt="Spark plan diagram highlighting a full table scan caused by missing partition predicates." src="https://cdn-images-1.medium.com/max/1024/1*N9SBsxqrZas-3J0gIlkm6A.png" /></figure><h4><strong>Impact</strong></h4><p>We added the correct partition predicates.⚡ Runtime dropped from <strong>~20 minutes to ~1 minute</strong>.</p><h3>Oversized broadcasts</h3><p>Broadcast joins are usually good — until they aren’t.</p><h4><strong>What the LLM surfaced</strong></h4><p>The model detected <strong>multiple multi-GB broadcasts</strong> happening in the job, something that doesn’t always fail immediately but creates driver pressure and unstable performance.</p><pre>{<br>  &quot;name&quot;: &quot;Oversized Broadcasts&quot;,<br>  &quot;detections&quot;: [<br>    {<br>      &quot;node_id&quot;: 55,<br>      &quot;broadcast_details&quot;: {<br>        &quot;table_name&quot;: &quot;supply.lodging_profile_vrbo&quot;,<br>        &quot;broadcast_size_human&quot;: &quot;2.7 GiB&quot;,<br>        &quot;join_type&quot;: &quot;BroadcastHashJoin (feeds node 13)&quot;<br>      },<br>      &quot;impact&quot;: &quot;Large broadcast may increase driver memory pressure and prolong broadcast/build time.&quot;<br>    },<br>    {<br>      &quot;node_id&quot;: 25,<br>      &quot;broadcast_details&quot;: {<br>        &quot;table_name&quot;: &quot;supply.lodging_profile_eg&quot;,<br>        &quot;broadcast_size_human&quot;: &quot;2.2 GiB&quot;,<br>        &quot;join_type&quot;: &quot;BroadcastHashJoin (feeds node 24)&quot;<br>      },<br>      &quot;impact&quot;: &quot;Multiple &gt;1 GiB broadcasts in the same query amplify driver/network overhead.&quot;<br>    }<br>  ]<br>}</pre><h4><strong>How we validated it</strong></h4><p>The Spark UI confirmed long broadcast built times and elevated driver memory usage.</p><figure><img alt="Spark plan diagram highlighting an oversized broadcast feeding a broadcast hash join." src="https://cdn-images-1.medium.com/max/1024/1*qQyBAiT1aKitpSUyrMv9Vg.png" /></figure><h4><strong>Impact</strong></h4><p>We reworked the join strategy to avoid broadcasting large datasets. 🚀 Runtime dropped from <strong>~20 minutes to ~1 minute</strong>, with improved stability.</p><h3><strong>Other patterns the model detects</strong></h3><p>The model also identifies subtle performance anti-patterns that often go unnoticed but significantly impact runtime and cost.</p><h4>Aggressive coalesce operations</h4><p>The model flags overly aggressive AQE coalescing when shuffle partitions grow excessively large (for example, <strong>&gt;2 GB or &gt;10× the advisory partition size</strong>) and overall parallelism drops below available executor cores. This typically results in underutilized clusters and long-running straggler tasks.</p><h4>High spill volumes</h4><p>Excessive memory or disk spills are detected when <strong>total spill size exceeds 1 GB</strong>, spills account for a large fraction of input data (≈<strong>20%+</strong>), or when spills occur across a majority of tasks in a stage. Higher severity is assigned when spill volumes exceed <strong>5 GB</strong>, indicating sustained memory pressure.</p><h4>Data explosion due to joins</h4><p>The model identifies joins where output rows grow disproportionately — <strong>2× or more</strong> than the largest input — often caused by duplicate keys, missing predicates, or incorrect join grain. These joins can silently inflate data volume and downstream shuffle costs.</p><p>Each pattern is backed by concrete metrics and physical plan operators, enabling the model to convert complex Spark execution details into clear, actionable guidance.</p><h3>The prompting strategy that finally worked</h3><p>The structured prompt tells the model to:</p><ul><li>Analyze only the slowest SQL in the application</li><li>Cross-check evidence before reporting anything</li><li>Ground every observation with stage/task/node IDs</li><li>Output only structured JSON with validated detections</li></ul><p>This eliminated false positives and made the analysis predictable.</p><p>Prompt Used:</p><p><a href="https://gist.github.com/bhuvanaSagi/149518ddeabc05020cafd95a3c19e83d">https://gist.github.com/bhuvanaSagi/149518ddeabc05020cafd95a3c19e83d</a></p><h4>What we saw in real workloads</h4><p>Across multiple Spark workloads, the LLM-based analysis consistently identified all the specified patterns in the SQL Plans.</p><p>We used these insights to tune Broadcast thresholds, add partition filters, and adjust AQE settings.</p><ul><li>Hours of manual debugging saved per incident</li><li>Runtime dropped by <strong>40–95%</strong> on long-running jobs</li><li>Compute cost dropped by <strong>50–90%</strong> per job</li></ul><p>The exact numbers vary by workload, but the pattern was clear: faster debugging, fewer surprises.</p><h3>How you can extend this approach</h3><p>You can take this further in a bunch of ways:</p><ul><li>Add new rules as you see more performance issues</li><li>Introduce Delta or Iceberg-specific checks</li><li>Map slow stages to individual plan nodes</li><li>Adjust thresholds for skew, spills, and broadcasts</li><li>Feed detections into dashboards or alerts for proactive tuning</li></ul><p>Once the context is structured, extending the system becomes easy.</p><h3>Closing thoughts</h3><p>This isn’t about replacing Spark experts. It’s about giving them leverage.</p><p>LLMs handle the repetitive triage so engineers can focus on the fixes that matter. By combining structured plan data, execution metrics, and domain-guided prompting, we automated a debugging workflow that used to demand deep Spark internals knowledge.</p><p>If you operate large-scale Spark workloads, LLM-assisted SQL plan analysis is a genuinely useful addition to your toolbox.</p><p>References:</p><p><a href="https://github.com/kubeflow/mcp-apache-spark-history-server">https://github.com/kubeflow/mcp-apache-spark-history-server</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35eace7eeec4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/using-llms-to-analyze-spark-sql-plans-a-practical-approach-to-debugging-long-running-jobs-35eace7eeec4">Using LLMs to Analyze Spark SQL Plans: A Practical Approach to Debugging Long-Running Jobs</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group 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[The Most Expensive Milliseconds Are Unmeasured]]></title>
            <link>https://medium.com/expedia-group-tech/the-most-expensive-milliseconds-are-unmeasured-d6cfaaca881d?source=rss----38998a53046f---4</link>
            <guid isPermaLink="false">https://medium.com/p/d6cfaaca881d</guid>
            <category><![CDATA[time-to-interactive]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[Divya Gupta Arora]]></dc:creator>
            <pubDate>Wed, 03 Jun 2026 17:11:58 GMT</pubDate>
            <atom:updated>2026-06-03T17:11:56.743Z</atom:updated>
            <content:encoded><![CDATA[<h4><a href="https://medium.com/expedia-group-tech/subpage/engineering">Expedia Group Technology — Engineering</a></h4><h4>How a screen-level performance metric reshaped platform decisions, engineering ownership, and release discipline</h4><figure><img alt="People in row boats on the water with mountains in the background" src="https://cdn-images-1.medium.com/max/1024/0*2C2DF8u6MlbkeuC5" /><figcaption>Photo by Pietro De Grandi on Unsplash</figcaption></figure><p>For the last few years, my responsibility has been straightforward to state but hard to execute — <strong>owning the traveler login experience across mobile platforms</strong>.</p><p>Not just whether a feature works, but whether it <em>feels</em> responsive, predictable, and trustworthy in the moments that matter most. During login, those moments are unforgiving: if a login screen hesitates travelers don’t interpret it as ‘a slow render’, they interpret it as risk.</p><p>And when the majority of travelers interact through our mobile apps, performance stops being a technical concern and becomes a <strong>product promise</strong>.</p><p>This post is a case study of how, within <a href="https://www.expediagroup.com/">Expedia Group</a>™’s login domain, we extended <strong>Native Time to Interactive (NTTI)</strong> across login screens — moving performance from a late-stage check to a first-class signal we can use to validate technology investments, compare platform behavior, and prevent silent regressions as we ship.</p><h3>The Problem: Mobile reliability rarely fails loudly</h3><p>Mobile performance rarely breaks with a crash.</p><p>It usually degrades quietly,</p><ul><li>a button takes a beat longer to respond</li><li>a screen <em>looks</em> ready, but taps don’t register</li><li>the UI stutters just enough to feel “off”</li></ul><p>Those are the expensive milliseconds — because they erode trust without triggering obvious alarms.</p><p>In our login flows, we were shipping consistently, evolving a major part of our stack, and supporting increasing product complexity. Yet we didn’t have a consistent way to answer the user’s real question — <strong>“When can I actually use this screen?”</strong></p><h3>Why our existing signals failed</h3><p>We were not blind. We tracked many useful things,</p><ul><li>crashes and ANRs</li><li>backend latency and service SLIs</li><li>some component-level timing signals</li><li>limited <a href="https://developer.mozilla.org/en-US/docs/Glossary/Time_to_interactive">Time to Interactive</a> tracking on onboarding and initial login</li></ul><p>But we had a gap — we had visibility into system health, not screen readiness.</p><p>Once travelers moved beyond the initial login screen into deeper login flows, performance became harder to compare release-to-release. We could detect obvious failures, but we lacked a consistent, screen-level metric that told us,</p><ul><li>Did this flow get slower after shipping feature X?</li><li>Did the new tech stack improve what travelers feel?</li><li>Are iOS and Android behaving similarly, or are we averaging away differences?</li></ul><p>And during a major tech stack migration, leadership asked fair questions — <strong>Are we actually delivering a better traveler experience ? Or just different architecture?</strong></p><p>We needed something more aligned with user-perceived readiness.</p><h3>What we chose to measure</h3><p>Expedia didn’t find an industry-standard metric that captured <em>native screen interactivity</em> the way Web Vitals capture <a href="https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint">LCP</a> and <a href="https://developer.mozilla.org/en-US/docs/Glossary/Time_to_interactive">TTI</a> on the web. So we coined a native-inspired equivalent: <strong>Native Time to Interactive (NTTI)</strong>.</p><p><strong>Definition:</strong> <em>All components that are visible in the initial viewport are loaded and available for interaction.</em></p><p><strong>NTTI measures:</strong><br> The time from when the screen starts loading to when its key components have loaded.</p><h4>Key terms</h4><ul><li><strong>Screen Start:</strong> when a new screen starts rendering and the user can’t interact yet (user is effectively blocked)</li><li><strong>Key Components:</strong> essential UI components that must be ready for meaningful interaction (typically in the initial viewport)</li><li><strong>Last Key Component:</strong> when the final key component completes render and is ready for interaction</li></ul><h4>Formula</h4><p><strong>NTTI = Last Key Component − Screen Start</strong></p><figure><img alt="Diagram illustrating screen load process from load to usable state, highlighting key stages and user interactions. It uses colour-coded labels and icons to show timeline from screen start to readiness, including components loading, tap failures, and successful tap, with total measured time of 8 seconds." src="https://cdn-images-1.medium.com/max/1024/1*FlRYf9a5EZjfDPsDlErQRA.png" /><figcaption>What is Time to Interactive wrt native devices</figcaption></figure><h4>How we instrumented NTTI across login screens</h4><p>Performance instrumentation wasn’t absent before — but it wasn’t consistent across the login journey.</p><p>We had:</p><ul><li>component-level signals on both platforms</li><li>limited screen-level visibility on a few launch flows</li><li>gaps across deeper login screens</li></ul><p>Extending NTTI across login screens required more than “adding timers.” We had to make deliberate decisions.</p><h4>Why NTTI worked for us</h4><p>NTTI anchors to a human outcome:</p><p><strong>A screen isn’t “fast” when it renders. It’s fast when it responds.</strong></p><p>A screen that draws quickly but doesn’t respond to touch feels broken. A slightly slower screen that responds reliably feels fast.</p><p>NTTI helped us measure what travelers actually experience: <strong>readiness to interact</strong>, not just rendering completion.</p><h3>The work broke down into four steps</h3><h4>1. Define “interactive” from a traveler perspective</h4><p>We aligned on what “usable” means for each screen — not in theory, but in practice.<br>This forced healthy product + engineering conversations: what’s essential vs what’s nice-to-have.</p><h4>2. Choose key components per screen</h4><p>We selected a subset of components that represent real readiness.<br>Not every UI element should gate interactivity; if everything counts, NTTI becomes noisy and less actionable.</p><h4>3. Standardize measurement across platforms</h4><p>We ensured the measurement approach was comparable across iOS and Android even though runtime behavior differs.</p><h4>4. Make the data reliable enough for decisions</h4><p>A metric is only useful if people trust it. That meant validating instrumentation, watching for gaps, and ensuring consistency across releases.</p><p>This wasn’t just observability work. It was building <strong>an operational contract</strong>: if we ship UI changes, we must be able to measure the impact.</p><figure><img alt="Diagram illustrating web page loading process with a timeline labelled “Time to Interactive.” It features three key points: “Screen Start” marked by a blue circle, an intermediate interaction point with a hand cursor over a form, and “Ready” marked by a green check, showing transition from loading to full interactivity." src="https://cdn-images-1.medium.com/max/1024/1*028mkpjglx8iMVPp0WxWSA.png" /><figcaption><em>NTTI captures when a screen becomes usable, not just when it finishes rendering.</em></figcaption></figure><h3>What we learned: iOS and Android are not symmetric</h3><p>One of the most important things NTTI made visible is what experienced mobile engineers already know, but organisations often ignore — <strong>iOS and Android behave differently even when the UI looks identical.</strong></p><h4>iOS issues often surfaced as</h4><ul><li>subtle UI delays</li><li>main-thread contention</li><li>strict lifecycle + rendering constraints</li></ul><h4>Android issues often surfaced as</h4><ul><li>device + OS fragmentation</li><li>OEM-specific behavior</li><li>hardware-dependent variance</li></ul><p>Before NTTI, it was easy to blend results and assume parity. After NTTI, we could see platform deltas clearly and evaluate them fairly.</p><p>The same backend and product flow could yield different NTTI uplift across platforms. That wasn’t a product inconsistency — it was platform behaviour and implementation nuance.</p><figure><img alt="Line graph comparing Android and iOS over time with fluctuating blue line representing data points against a background of coloured horizontal bands indicating different value ranges. Graph includes labels for time on x-axis and a smooth trend line option, highlighting variable trends and occasional spikes within a consistent range." src="https://cdn-images-1.medium.com/max/1024/1*SnQrmzjoE4TAwU9StWULtA.png" /><figcaption>Same backend. Same Feature. Different Experience.</figcaption></figure><p>NTTI made those differences measurable instead of debatable.</p><figure><img alt="Line graph comparing performance data before and after improvements of Android over multiple days, with two distinct blue lines representing each period. The graph shows higher and more variable peaks before improvements and lower, more stable values after, highlighting a clear reduction in performance fluctuations." src="https://cdn-images-1.medium.com/max/1024/1*uPSdoCKsgAkzrq95-Q6ZAQ.png" /><figcaption>Before vs After performance improvements in Android (46.3% faster)</figcaption></figure><h3>The unexpected outcome: Better ownership</h3><p>One of the most valuable outcomes had little to do with the metric itself.</p><p>Once NTTI was introduced, teams naturally started to</p><ul><li>instrument what they shipped</li><li>read dashboards with intent instead of curiosity</li><li>connect code changes directly to traveler experience</li><li>identify and fix regressions earlier in the cycle</li></ul><p>Observability stopped feeling like a downstream activity. It became a core engineering discipline. And because the metric reflected real user experience, performance stopped being “just an engineering concern.” It became a shared responsibility. Product teams aligned on performance as a release guardrail and part of the user experience discussion. Design helped define what “ready” actually meant from an interaction standpoint. Engineering made those expectations measurable and actionable.</p><p>Over time, ownership shifted from <em>“Someone will catch this later.” </em>to <em>“If I ship it, I monitor it.” </em>That kind of cultural shift is difficult to enforce top-down.<br>NTTI helped create it through consistency, visibility, and repeated feedback loops.</p><h4>How this changes release conversations</h4><p>Today, NTTI is one of the first signals I look at</p><ul><li>before features ramp to wider production</li><li>while validating major technical initiatives</li><li>during release readiness discussions</li></ul><p>We haven’t yet fully adopted NTTI as a formal rollout blocker, but that’s the direction we’re moving toward.</p><h4>What we intend to do next</h4><ul><li>Treat NTTI degradation as a “pause and investigate” signal</li><li>Require explicit conversations before ramping when performance moves outside expected thresholds</li><li>Protect hard-earned performance gains from slowly regressing over time</li></ul><p>To be clear, NTTI won’t be the only release signal, but it will become an important guardrail because it forces the right conversations to happen early, while fixes are still practical and inexpensive.</p><h3>What NTTI tells us — and what it doesn’t</h3><p>Being explicit about scope has helped us use the metric responsibly.</p><h4>What we know</h4><ul><li>NTTI measures screen readiness from an interaction perspective</li><li>the same change can yield different outcomes across platforms</li><li>it works as an early regression guardrail</li><li>it improves release conversations through concrete signals</li><li>it increases engineering ownership of performance</li></ul><h4>What we don’t claim</h4><ul><li>NTTI doesn’t explain user intent or behavior</li><li>it isn’t used in isolation</li><li>it doesn’t prove causality beyond what it measures</li><li>platform parity is not assumed or enforced</li></ul><h3>Closing thought</h3><p>NTTI didn’t just provide another metric, instead it gave us</p><ul><li>earlier visibility into traveler friction</li><li>a concrete way to validate tech stack investments</li><li>a growing culture of engineers owning performance</li><li>a shared language for cross-platform performance conversations</li></ul><p>During login where trust, speed, and reliability are inseparable this discipline matters. Performance isn’t something we optimize later. With NTTI, it’s something we design for from day one.</p><p><a href="https://careers.expediagroup.com/life/">Learn about life at Expedia Group</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d6cfaaca881d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/expedia-group-tech/the-most-expensive-milliseconds-are-unmeasured-d6cfaaca881d">The Most Expensive Milliseconds Are Unmeasured</a> was originally published in <a href="https://medium.com/expedia-group-tech">Expedia Group Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>