<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ishaan Gaba</title>
    <description>The latest articles on DEV Community by Ishaan Gaba (@ishaanthedev).</description>
    <link>https://dev.to/ishaanthedev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3790044%2F7320718c-3d73-4a9d-9908-db8ec9e451cc.png</url>
      <title>DEV Community: Ishaan Gaba</title>
      <link>https://dev.to/ishaanthedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishaanthedev"/>
    <language>en</language>
    <item>
      <title>Designing Uber: A Real-Time Ride Matching System at Scale</title>
      <dc:creator>Ishaan Gaba</dc:creator>
      <pubDate>Sun, 22 Mar 2026 17:26:38 +0000</pubDate>
      <link>https://dev.to/ishaanthedev/designing-uber-a-real-time-ride-matching-system-at-scale-pc9</link>
      <guid>https://dev.to/ishaanthedev/designing-uber-a-real-time-ride-matching-system-at-scale-pc9</guid>
      <description>&lt;p&gt;A user opens Uber. Taps a destination. Hits confirm.&lt;/p&gt;

&lt;p&gt;Within 10 seconds, a driver is assigned, a route is calculated, and an ETA appears on screen.&lt;/p&gt;

&lt;p&gt;That moment feels instant. It is not. Behind it sits one of the most demanding real-time distributed systems ever built — coordinating millions of moving devices, unpredictable networks, and a matching problem that must resolve in seconds or users abandon the app entirely.&lt;/p&gt;

&lt;p&gt;This is not a tutorial on how Uber works. This is an engineering breakdown of &lt;em&gt;why&lt;/em&gt; it's hard, and how you'd design it if you were the one responsible for keeping it running.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Uber Is a Hard System
&lt;/h2&gt;

&lt;p&gt;Most systems are hard because of scale. Uber is hard because of &lt;strong&gt;scale plus real-time constraints plus physical world unreliability&lt;/strong&gt; — all at once.&lt;/p&gt;

&lt;p&gt;Consider what has to be true simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Millions of drivers are broadcasting their GPS coordinates every 4–5 seconds&lt;/li&gt;
&lt;li&gt;Riders expect a match response in under 10 seconds&lt;/li&gt;
&lt;li&gt;Driver locations are stale the moment they're recorded&lt;/li&gt;
&lt;li&gt;Networks drop. Phones die. Drivers cancel mid-assignment.&lt;/li&gt;
&lt;li&gt;The matching decision has to be &lt;strong&gt;globally fair&lt;/strong&gt; — not just fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ride-matching system is not a database lookup. It's a real-time geospatial optimization problem running at internet scale, with human behavior injected at every layer.&lt;/p&gt;

&lt;p&gt;Miss the latency target and riders churn. Miss the consistency target and two riders get the same driver.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flow, Before the Diagrams
&lt;/h2&gt;

&lt;p&gt;Before any architecture discussion, walk through what actually happens when a ride is requested.&lt;/p&gt;

&lt;p&gt;A rider opens the app. The client is already maintaining a WebSocket connection to Uber's backend — location updates, surge data, and nearby driver markers are streaming in continuously. The rider is not idle; they're already consuming data before they book anything.&lt;/p&gt;

&lt;p&gt;The rider submits a trip request. This hits the API Gateway, gets authenticated, and routes to the &lt;strong&gt;Dispatch Service&lt;/strong&gt; — the core orchestrator of the entire ride lifecycle.&lt;/p&gt;

&lt;p&gt;The Dispatch Service doesn't look up drivers in a SQL table. It queries a &lt;strong&gt;geospatial index&lt;/strong&gt; — a live, in-memory store of driver locations organized by geography. It pulls a candidate set of nearby available drivers, ranks them by proximity, ETA, and acceptance rate, and sends a trip offer to the top candidate.&lt;/p&gt;

&lt;p&gt;The driver receives a push notification and has roughly 15 seconds to accept. If they accept, the match is confirmed. If they don't — timeout, decline, or network failure — the system moves to the next candidate.&lt;/p&gt;

&lt;p&gt;The whole thing has to complete in under 10 seconds from the rider's perspective. Every second beyond that degrades conversion.&lt;/p&gt;




&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3ku9z0ghqgfjnef1c5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3ku9z0ghqgfjnef1c5n.png" alt=" " width="800" height="1105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The API Gateway handles auth, rate limiting, and routing. The Location Service is a high-throughput write path — it needs to ingest millions of location pings per minute and keep the geospatial index fresh. The Dispatch Service owns the match lifecycle. The Notification Service handles the real-time push to drivers.&lt;/p&gt;

&lt;p&gt;These are not monolithic. Each is a horizontally scalable service with its own failure boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-Time Location Tracking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Every active driver is a moving data point. The system needs to know where they are, right now, not 30 seconds ago. At 5 million active drivers globally, that's roughly 1 million location updates per minute hitting your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's hard:&lt;/strong&gt; You can't write every GPS ping to a relational database. The write throughput would bury it. But you also can't lose location data — stale coordinates mean wrong ETAs and bad matches.&lt;/p&gt;

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

&lt;p&gt;Driver apps send a location update every 4–5 seconds over a persistent connection. These updates hit the Location Service, which does two things in parallel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates the &lt;strong&gt;in-memory geospatial index&lt;/strong&gt; for live querying&lt;/li&gt;
&lt;li&gt;Publishes to a &lt;strong&gt;Kafka topic&lt;/strong&gt; for downstream consumers — ETA computation, analytics, surge pricing
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified location update handler
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_location_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Update in-memory geo index (sub-millisecond)
&lt;/span&gt;    &lt;span class="n"&gt;geo_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Publish to Kafka async — don't block the write path
&lt;/span&gt;    &lt;span class="n"&gt;kafka_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;driver-locations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;driver_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;driver_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the geospatial index, Uber uses &lt;strong&gt;S2 geometry&lt;/strong&gt; — a library that maps the Earth's surface to a hierarchical grid of cells. Each driver is indexed by their current cell. A proximity query becomes a cell lookup, which is O(1) in the average case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; In-memory indexing is fast but volatile. If the Location Service crashes, the index needs to rebuild from Kafka replay. Acceptable — the rebuild is fast, and correctness beats stale data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Matching Engine
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Given a rider at a location, find the best available driver, send them an offer, handle their response, and confirm the match — all within the latency budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's hard:&lt;/strong&gt; "Best available driver" is not a simple nearest-neighbor query. You're optimizing across proximity, ETA, driver rating, acceptance rate, vehicle type, and surge zone simultaneously. And the candidate set is changing every second as drivers move.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdcmksqugu1fo3u9xgxn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdcmksqugu1fo3u9xgxn.png" alt=" " width="736" height="1140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The matching engine pulls a candidate pool from the geospatial index — typically drivers within a configurable radius. It scores each candidate using a weighted function that factors in ETA (dominant), acceptance rate (secondary), and trip efficiency. The top-scored driver gets the offer first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The offer timeout matters.&lt;/strong&gt; 15 seconds is long enough for a driver to react, short enough that the rider's session doesn't expire. If the driver doesn't respond, the system moves to the next candidate immediately — no waiting, no retry on the same driver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Sequential offering is fair but slow under high demand. Uber has experimented with &lt;strong&gt;batched offers&lt;/strong&gt; — sending to multiple drivers simultaneously and accepting the first response. This reduces latency but requires careful deduplication to prevent double-assignment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-Time Communication
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Both the rider and driver need live updates — driver moving on map, ETA countdown, trip status changes. Polling is not acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's hard:&lt;/strong&gt; WebSocket connections are stateful. You can't just add servers and route arbitrarily — the connection has to be maintained on a specific node. At millions of concurrent connections, connection management becomes infrastructure.&lt;/p&gt;

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

&lt;p&gt;Uber uses &lt;strong&gt;long-lived WebSocket connections&lt;/strong&gt; for both rider and driver apps. These are maintained by a dedicated connection management layer — a stateful service that maps user IDs to open connections.&lt;/p&gt;

&lt;p&gt;When a location update needs to be pushed to a rider (driver is moving), the flow is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmml3210urcz04qqyyo7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmml3210urcz04qqyyo7x.png" alt=" " width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; WebSockets are more operationally complex than HTTP polling. Connection drops require reconnect logic with exponential backoff. But the UX difference is stark — smooth driver movement on the map is a core product experience, not a nice-to-have.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Ride Flow: From Booking to Completion
&lt;/h2&gt;

&lt;p&gt;A user opens Uber, enters a destination, and taps "Book Ride."&lt;/p&gt;

&lt;p&gt;From that single tap, a coordinated chain of services fires in sequence — some in milliseconds, some running in parallel, all of them invisible to the person staring at their phone waiting for a match.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1 — Ride Request
&lt;/h3&gt;

&lt;p&gt;The rider submits a trip request from the client app over HTTPS.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;API Gateway&lt;/strong&gt; handles authentication, rate limiting, and routes the request to the &lt;strong&gt;Trip Service&lt;/strong&gt;, which initializes a trip record with state set to &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; rider ID, pickup coordinates, destination, vehicle type preference, timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; The trip record must be created atomically. A partial write — trip exists but state is undefined — causes inconsistency downstream. The Trip Service writes to a persistent store with a single transaction before anything else proceeds.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 — Driver Discovery
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Matching Engine&lt;/strong&gt; receives the trip request and immediately queries the &lt;strong&gt;Geospatial Index&lt;/strong&gt; for available drivers within the configured radius.&lt;/p&gt;

&lt;p&gt;This is not a SQL &lt;code&gt;SELECT WHERE distance &amp;lt; X&lt;/code&gt; query. The index is an in-memory structure built on S2 geometry cells — the query resolves in under a millisecond and returns a ranked candidate pool, not a flat list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; pickup coordinates, search radius, driver availability flags, vehicle type filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; The candidate pool is stale the moment it's returned. Drivers move. The system must account for position drift between query time and offer delivery — typically handled by padding ETA estimates conservatively.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3 — Matching and Dispatch
&lt;/h3&gt;

&lt;p&gt;The Matching Engine scores each candidate in the pool against a weighted function: ETA carries the most weight, followed by acceptance rate, driver rating, and trip efficiency.&lt;/p&gt;

&lt;p&gt;The top-scored driver gets the first offer. This is not random. It is a deliberate optimization — sending offers to drivers most likely to accept reduces total match latency across the system, which matters at scale.&lt;/p&gt;

&lt;p&gt;The offer is dispatched through the &lt;strong&gt;Notification Service&lt;/strong&gt; via push notification and WebSocket simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; driver ID, trip ID, pickup location, estimated pickup distance, offer expiry timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; The offer window is 15 seconds. Too short and drivers miss it. Too long and the rider's patience expires. This number is not arbitrary — it is derived from real conversion data.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4 — Driver Acceptance
&lt;/h3&gt;

&lt;p&gt;The driver taps accept. The acceptance hits the Matching Engine, which immediately attempts to acquire a &lt;strong&gt;distributed lock&lt;/strong&gt; on the trip ID.&lt;/p&gt;

&lt;p&gt;First write wins. If two drivers somehow accept within the same window — possible in a batched offer scenario — only one lock acquisition succeeds. The second driver receives a "trip no longer available" response. No double-assignment. Ever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; driver ID, trip ID, acceptance timestamp, current driver location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; The lock must be acquired, the trip state updated, and the rider notified — all before the offer window closes on another candidate. This is the tightest latency window in the entire flow.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5 — Trip Start
&lt;/h3&gt;

&lt;p&gt;The driver navigates to the pickup location. Both the rider and driver are now exchanging state through the &lt;strong&gt;Trip Service&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the driver arrives and taps "Arrived," the trip moves to &lt;code&gt;awaiting_rider&lt;/code&gt; state. When the rider boards and the driver taps "Start Trip," state transitions to &lt;code&gt;active&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; driver GPS coordinates, trip state transitions, ETA updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; State transitions must be idempotent. A driver tapping "Start Trip" twice — due to a slow network and a frustrated re-tap — must not corrupt the trip record or trigger duplicate downstream events.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 6 — Ride in Progress
&lt;/h3&gt;

&lt;p&gt;This is the longest phase and, architecturally, the most continuous.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Location Service&lt;/strong&gt; ingests driver GPS updates every 4–5 seconds. These are published to a Kafka topic, consumed by the Trip Update Service, and pushed to the rider's app over a persistent WebSocket connection. The rider sees the driver moving on the map in near real-time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; driver GPS stream, trip ID, rider connection reference, ETA recalculations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; WebSocket connections drop. The client must implement reconnect logic with exponential backoff, and the server must resume the stream without duplicating or dropping location events. The Kafka offset acts as the source of truth for where the stream left off.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 7 — Trip Completion
&lt;/h3&gt;

&lt;p&gt;The driver taps "End Trip." The Trip Service captures the final GPS coordinate, marks the trip &lt;code&gt;completed&lt;/code&gt;, and calculates the billable distance using the full GPS trace recorded during the ride — not straight-line origin-to-destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; final GPS coordinate, full route trace, trip duration, computed fare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; GPS traces are noisy. A driver on a highway doesn't teleport — but raw coordinates sometimes suggest otherwise. Fare calculation uses map-snapped routes, not raw GPS, to prevent billing anomalies.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 8 — Payment Processing
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Payment Service&lt;/strong&gt; receives the computed fare and initiates the charge against the rider's stored payment method.&lt;/p&gt;

&lt;p&gt;This is an asynchronous operation. The rider sees "Trip Complete" immediately — the payment processes in the background. If the charge fails, the system retries with exponential backoff. Persistent failures are queued for manual resolution and the rider is notified separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flowing:&lt;/strong&gt; rider ID, payment method token, fare amount, trip ID, idempotency key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; Idempotency is non-negotiable. A retry must never result in a double charge. Every payment request carries a unique idempotency key tied to the trip ID — the payment processor uses it to deduplicate retries at the transaction level.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 9 — Post-Ride Actions
&lt;/h3&gt;

&lt;p&gt;Trip data fans out to multiple downstream consumers asynchronously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rating prompts are pushed to both rider and driver&lt;/li&gt;
&lt;li&gt;The receipt is generated and emailed&lt;/li&gt;
&lt;li&gt;The trip record is written to the analytics pipeline&lt;/li&gt;
&lt;li&gt;Driver earnings are updated in the earnings ledger&lt;/li&gt;
&lt;li&gt;Surge pricing models are updated with the completed trip signal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these block the core flow. They consume from the same Kafka event that marked the trip completed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key challenge:&lt;/strong&gt; Downstream consumers must be designed for eventual consistency. The analytics pipeline does not need to reflect the trip in real-time. The earnings ledger does — a driver who completes a trip expects their balance to update promptly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where the Flow Breaks
&lt;/h3&gt;

&lt;p&gt;Every handoff above is a potential failure point. The ones that matter most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Driver doesn't respond within 15 seconds.&lt;/strong&gt; The Matching Engine moves to the next candidate immediately. From the rider's perspective, the search continues. The timed-out driver sees the offer expire silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple drivers accept simultaneously.&lt;/strong&gt; Distributed lock on the trip ID ensures only one match is confirmed. The losing driver gets a rejection with no trip context exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network failure mid-ride.&lt;/strong&gt; Driver GPS stream pauses. The rider's map freezes. The system buffers the location gap and resumes when connectivity returns. Fare calculation uses the recovered trace — not the gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payment failure at completion.&lt;/strong&gt; Trip is marked complete regardless. Payment retries asynchronously. The rider is not held at the end screen waiting for a payment confirmation that might never come.&lt;/p&gt;

&lt;p&gt;The end-to-end flow is where the system's real complexity lives — not in any single service, but in the handoffs between them, the state transitions under failure, and the guarantees the system must maintain while moving faster than the user can perceive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Scenarios
&lt;/h2&gt;

&lt;p&gt;This is where most system design discussions go shallow. Real systems fail in real ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Driver accepts but network drops:&lt;/strong&gt;&lt;br&gt;
The driver taps accept, but the acknowledgment never reaches the server. From the server's perspective, the offer timed out. The system moves to the next driver. The first driver's app eventually reconnects and tries to confirm — the system rejects it because the match is already assigned elsewhere. The first driver sees an error. Rider experience is unaffected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple drivers accept simultaneously (race condition):&lt;/strong&gt;&lt;br&gt;
In a batched offer scenario, two drivers both accept within milliseconds of each other. The system must guarantee only one match. This is solved with &lt;strong&gt;distributed locking&lt;/strong&gt; on the trip ID — first write wins, second write is rejected with a conflict response. The second driver gets a "trip no longer available" notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No drivers available:&lt;/strong&gt;&lt;br&gt;
The geo query returns an empty candidate set. The Dispatch Service doesn't fail — it enters a &lt;strong&gt;retry loop with expanding radius&lt;/strong&gt;, checking every few seconds as new drivers become available or existing drivers complete trips. The rider sees a "finding your driver" state. If no driver is found within a threshold, the request fails gracefully with a clear user message.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trade-offs That Matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consistency vs. availability:&lt;/strong&gt; For matching, you want consistency — two riders must not get the same driver. Uber leans toward consistency here, accepting that under extreme load, some match attempts will fail and retry rather than produce a double-assignment. The cost is occasional latency. The alternative — a confused driver with two riders — is worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency vs. accuracy:&lt;/strong&gt; Driver location is inherently stale. A location updated 4 seconds ago is already wrong for a moving vehicle. ETA calculations account for this by using historical speed models per road segment rather than pure straight-line distance. Accepting slight inaccuracy in location enables the throughput needed to serve the system at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost vs. performance:&lt;/strong&gt; In-memory geospatial indexes are fast but expensive. You're paying for RAM at massive scale. The alternative — disk-based geospatial queries — is cheaper but too slow for the latency target. For a system where matching latency directly drives revenue conversion, the memory cost is justified. This is a deliberate engineering economics decision, not an oversight.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Insights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The matching problem is not a database problem.&lt;/strong&gt; It's a real-time geospatial optimization problem. Design accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate your write path from your query path.&lt;/strong&gt; Location ingestion and location querying are different workloads with different requirements. Don't conflate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure handling is not error handling.&lt;/strong&gt; Design every step of the match lifecycle assuming the next step will fail. What does the system do? That answer needs to be encoded, not improvised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful services are unavoidable.&lt;/strong&gt; WebSocket connections, geospatial indexes, distributed locks — you cannot build this system as purely stateless services. Accept the operational complexity and design for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency budgets are product decisions, not engineering ones.&lt;/strong&gt; The 10-second match window isn't arbitrary — it's derived from rider churn data. Your architecture must be built to serve that number, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every second of that 10-second match window has a system decision behind it.&lt;/strong&gt; The end-to-end flow is where the system's real complexity lives — not in any single service, but in the handoffs, the state transitions, and the guarantees maintained under failure.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;A user opens Uber and gets a driver in 10 seconds.&lt;/p&gt;

&lt;p&gt;What they don't see: a geospatial index serving thousands of queries per second, a matching engine racing through candidate ranking, a WebSocket layer maintaining millions of live connections, a distributed lock preventing double-assignment, and a failure handler quietly retrying in the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The system's job is to make complexity invisible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the real benchmark for production-grade distributed systems — not whether they work when everything goes right, but whether users never notice when things go wrong.&lt;/p&gt;

&lt;p&gt;Build for the failure path first. The happy path takes care of itself.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>kafka</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Most AI Agents Fail (And How to Design Them Right)</title>
      <dc:creator>Ishaan Gaba</dc:creator>
      <pubDate>Tue, 17 Mar 2026 16:54:46 +0000</pubDate>
      <link>https://dev.to/ishaanthedev/why-most-ai-agents-fail-and-how-to-design-them-right-o70</link>
      <guid>https://dev.to/ishaanthedev/why-most-ai-agents-fail-and-how-to-design-them-right-o70</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Most AI agents shipped to production are not agents. They are dressed-up chatbots with a tool list and a prayer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a provocative claim, but after building and reviewing LLM-powered systems across customer support, internal tooling, and real-time messaging platforms, the pattern is impossible to ignore. Teams integrate an LLM, wire up a few API calls, and call it an "agent." Then latency spikes, context breaks down, the agent calls the wrong tool, and suddenly the engineering post-mortem is asking: &lt;em&gt;what went wrong?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post breaks down exactly why AI agents fail in production — and how to engineer them so they don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hype vs. The Reality
&lt;/h2&gt;

&lt;p&gt;The demo looks flawless. The agent reads a user message, reasons over it, calls a function, and returns a clean response. Three minutes to build. Everyone applauds.&lt;/p&gt;

&lt;p&gt;Production is different. Real users are unpredictable. Messages are ambiguous. Tool calls fail. Latency matters. And the agent, designed for linear tasks in controlled demos, collapses under the weight of real-world variability.&lt;/p&gt;

&lt;p&gt;The problem is not the LLM. The problem is architecture.&lt;/p&gt;

&lt;p&gt;Agents are not just LLMs with tools attached. They are &lt;strong&gt;autonomous reasoning systems&lt;/strong&gt; that must handle state, uncertainty, failure, and feedback — often across multiple steps. Treating them otherwise is where most teams go wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real-World Case: The Chat Platform Agent
&lt;/h2&gt;

&lt;p&gt;Imagine you're building an AI-powered layer on top of a Slack/WhatsApp-style messaging system. The agent is supposed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate smart reply suggestions&lt;/li&gt;
&lt;li&gt;Detect and flag inappropriate messages&lt;/li&gt;
&lt;li&gt;Summarize long threads&lt;/li&gt;
&lt;li&gt;Trigger message actions (react, reply, archive)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a realistic scope. Here is what the naive architecture looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsfuvxowo1wug5ksbhnyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsfuvxowo1wug5ksbhnyj.png" alt="scope" width="800" height="823"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simple enough. But now trace through what actually happens in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1 — Context Collapse:&lt;/strong&gt; A user sends a sarcastic message: &lt;em&gt;"Oh great, another outage."&lt;/em&gt; The smart reply agent, lacking thread history, suggests: &lt;em&gt;"Glad to hear things are going well!"&lt;/em&gt; The agent had no context. It made up a coherent but catastrophically wrong response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2 — Tool Ambiguity:&lt;/strong&gt; The user says, &lt;em&gt;"React to that with a thumbs up."&lt;/em&gt; The agent calls &lt;code&gt;send_message&lt;/code&gt; instead of &lt;code&gt;add_reaction&lt;/code&gt; because both tools accept similar inputs and the descriptions are vague. The action is wrong. The user is confused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 3 — Latency Death Spiral:&lt;/strong&gt; The agent decides it needs to summarize the thread, moderate the content, and suggest a reply — all in a single turn. Three LLM calls, two API calls, and a 14-second response time. Users abandon it.&lt;/p&gt;

&lt;p&gt;These are not edge cases. These are the first three weeks of production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Six Ways Agents Fail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Treating the Agent Like a Chatbot
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; Engineers build a single-turn request-response loop. User sends a message, LLM responds. No planning, no persistence, no feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; The chatbot mental model is deeply ingrained. Most LLM tutorials are structured this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Design agents around a &lt;strong&gt;Think → Plan → Act → Validate&lt;/strong&gt; loop. The agent should reason about &lt;em&gt;what it needs to do&lt;/em&gt; before doing it. This means separating the reasoning step from the execution step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fudx4mmlhmr1ajkgk3fym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fudx4mmlhmr1ajkgk3fym.png" alt="Diagram" width="800" height="106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A chatbot responds. An agent &lt;em&gt;decides&lt;/em&gt;, then responds.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Poor Tool Design
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; Tools are defined as thin wrappers around APIs with no semantic clarity. Names like &lt;code&gt;do_action&lt;/code&gt; or &lt;code&gt;process_data&lt;/code&gt; give the LLM no guidance. Overlapping tool signatures cause the model to pick the wrong one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; Engineers think about tools as functions — not as cognitive affordances for the LLM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Treat tool design like API design for a developer who has never seen your codebase. Names, descriptions, and parameter schemas must be unambiguous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;BAD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message_action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do something with a message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;GOOD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add_reaction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Adds an emoji reaction to a specific message in a channel. Use this when the user wants to react to a message, not when they want to send a reply."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string — The unique ID of the target message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emoji"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string — Emoji shortcode, e.g. ':thumbsup:'"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The test:&lt;/strong&gt; If you gave these tool definitions to a junior engineer with no context, would they know exactly when to call each one? If not, neither will the LLM.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Lack of Context Structuring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; The agent receives a raw message and nothing else. Or worse — it receives a 4,000-token chat dump with no structure. Either way, the LLM reasons from noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; Teams focus on getting the tool wiring right and treat context as an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Structure context as a deliberate input, not a log dump. Separate signal from noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg_991&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;React to that with a thumbs up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sender&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-03-15T10:42:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recent_thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sender&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_77&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The deploy broke staging again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sender&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yeah I saw that — not great&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add_reaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flag_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;react&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent now knows the message, the thread context, what it's allowed to do, and who it's talking to. This is the minimum viable context for a messaging agent.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. No Execution Control
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; The agent acts immediately. No retry logic, no rollback, no confirmation for destructive operations. A moderation agent deletes a message it misclassified. No undo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; Execution is treated as a side effect, not a first-class concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Classify tool calls by risk level and enforce execution gates accordingly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk Level&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Execution Policy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Read thread, summarize, suggest reply&lt;/td&gt;
&lt;td&gt;Execute directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Send message, add reaction&lt;/td&gt;
&lt;td&gt;Execute with logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Delete message, ban user, bulk action&lt;/td&gt;
&lt;td&gt;Require confirmation or human review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For high-risk actions, surface a &lt;strong&gt;confirmation step&lt;/strong&gt; before execution. In async systems, push high-risk operations into a review queue with a timeout.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Weak Memory Handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; Every conversation starts cold. The agent has no memory of previous interactions, user preferences, or prior decisions. Users repeat themselves. The agent contradicts itself across sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; Stateless is the default. Teams don't architect memory as a subsystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Build a layered memory model:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h2upyjaadaep24yg13z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h2upyjaadaep24yg13z.png" alt="Diagram" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Long-term memory does not have to be complex. A vector store with summarized user interactions and outcomes is sufficient for most production use cases. What matters is that memory is &lt;strong&gt;retrieved&lt;/strong&gt;, not assumed.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Missing Guardrails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What goes wrong:&lt;/strong&gt; The agent hallucinates a tool parameter. It calls a tool outside its defined scope. It enters an infinite planning loop. It generates a reply that violates policy. None of this is caught before it reaches the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt; Guardrails are seen as post-launch polish, not core architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix it:&lt;/strong&gt; Guardrails are not optional. They are the difference between a demo and a production system. Implement them at three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input validation:&lt;/strong&gt; Classify and sanitize user input before it reaches the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output validation:&lt;/strong&gt; Check the agent's planned actions and tool calls against a schema and policy ruleset before execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response filtering:&lt;/strong&gt; Run final responses through a lightweight classifier before delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the messaging agent, this means: if the agent attempts to call &lt;code&gt;delete_message&lt;/code&gt; outside of a moderation flow, reject the action and route to a human reviewer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Right Architecture
&lt;/h2&gt;

&lt;p&gt;Here is what a production-grade chat agent architecture looks like when these principles are applied:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93sojq8mubf7fcu81j3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93sojq8mubf7fcu81j3o.png" alt="Correct Flow" width="800" height="1046"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not overengineering. Every node in this diagram corresponds to a failure mode described above. Each one exists because something broke in production without it.&lt;/p&gt;

&lt;p&gt;The key design properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning is separate from execution.&lt;/strong&gt; The LLM plans; execution is deterministic and validated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context is structured and retrieved&lt;/strong&gt;, not raw and assumed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tool call passes through a risk gate&lt;/strong&gt; before execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is a subsystem&lt;/strong&gt;, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-loop is built in&lt;/strong&gt;, not bolted on.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;On architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design the Think → Plan → Act → Validate loop first. Wire up tools second.&lt;/li&gt;
&lt;li&gt;Separate reasoning from execution. Treat them as distinct subsystems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On tooling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write tool descriptions for an LLM, not for a developer reading docs.&lt;/li&gt;
&lt;li&gt;Every tool should have a clear, non-overlapping purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On context:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structure context deliberately. Signal-to-noise ratio matters more than raw token count.&lt;/li&gt;
&lt;li&gt;Retrieval beats injection — pull what's needed, don't dump everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On reliability:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Risk-gate every tool call. Not all actions are reversible.&lt;/li&gt;
&lt;li&gt;Guardrails are not polish — they are architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On memory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build layered memory from day one. Cold-start agents fail users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every additional reasoning step costs latency. Profile your loop and set token budgets per step.&lt;/li&gt;
&lt;li&gt;Human-in-the-loop adds latency but prevents catastrophic errors for high-stakes actions. Make this a deliberate design choice, not an oversight.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The gap between a demo agent and a production agent is not a gap in capability — it is a gap in &lt;strong&gt;systems thinking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;LLMs have given engineers a powerful new primitive. But primitives do not build reliable systems. Architecture does. The teams shipping agents that actually work in production are not the ones who found the best prompt. They are the ones who treated context, memory, tooling, and execution control as first-class engineering concerns from day one.&lt;/p&gt;

&lt;p&gt;Build the loop. Structure the context. Gate the execution. Ship the guardrails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An AI agent is only as reliable as the system it runs inside.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
