<?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: Aditya Negandhi</title>
    <description>The latest articles on DEV Community by Aditya Negandhi (@0xadityaa).</description>
    <link>https://dev.to/0xadityaa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1513554%2F0d4d0528-eb22-4fc3-b907-79ddf11352df.jpeg</url>
      <title>DEV Community: Aditya Negandhi</title>
      <link>https://dev.to/0xadityaa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xadityaa"/>
    <language>en</language>
    <item>
      <title>Going Event-Driven? Read the Bill First.</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:02:01 +0000</pubDate>
      <link>https://dev.to/0xadityaa/going-event-driven-read-the-bill-first-10f8</link>
      <guid>https://dev.to/0xadityaa/going-event-driven-read-the-bill-first-10f8</guid>
      <description>&lt;p&gt;Picture this. You ship a gorgeous event-driven architecture. Everything is async, decoupled, cloud-native, the works. You're proud of it. Three weeks later someone drops a screenshot of the AWS bill in Slack, and suddenly nobody's talking about how elegant it is.&lt;/p&gt;

&lt;p&gt;Pretty architecture and cheap architecture are two different things.&lt;/p&gt;

&lt;p&gt;I spent the past year working closely with solution architects, and I kept seeing the same movie: a team goes event-driven for all the right reasons, then gets blindsided by costs nobody modelled. So here's my claim. &lt;strong&gt;An event-driven system is billed per event, per consumer, and per kilobyte, which means three whiteboard decisions set your bill: fan-out, payload size, and retry behaviour.&lt;/strong&gt; You can do that math before writing a line of code. I did it below so you can steal it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Events are great when services don't need to wait on each other. Service A publishes "this happened" and gets on with its life. The catch is that every one of those events is metered. On public AWS prices, one event fanned out to 12 small Lambda consumers costs about $5.90 per million events. That's pocket change at a million events a month. It's about $5,900 at a billion, and that's before a chunky payload or a retry loop gets involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with good old HTTP calls?
&lt;/h2&gt;

&lt;p&gt;When services call each other over REST, the whole chain is as fast as its slowest link. &lt;code&gt;Checkout Service&lt;/code&gt; calls &lt;code&gt;Payment Service&lt;/code&gt;, which calls &lt;code&gt;Fraud Service&lt;/code&gt;, which calls &lt;code&gt;Analytics Service&lt;/code&gt;. One of them hangs, the user's request hangs. One flaky service downstream and your p99 latency is on the moon.&lt;/p&gt;

&lt;p&gt;That's the &lt;strong&gt;distributed monolith&lt;/strong&gt;. It looks like microservices and behaves like a monolith. You split the code and kept all the coupling. Worst of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/201701-event-driven.html" rel="noopener noreferrer"&gt;Event-driven architecture&lt;/a&gt; fixes it by deleting the waiting. Service A publishes an event (say, "Order Placed") to a broker like &lt;a href="https://kafka.apache.org/" rel="noopener noreferrer"&gt;Kafka&lt;/a&gt; or &lt;a href="https://aws.amazon.com/eventbridge/" rel="noopener noreferrer"&gt;Amazon EventBridge&lt;/a&gt;. Whoever cares about that event picks it up and reacts in their own time. Service A is already serving the next request. Nobody waits on anybody.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when should something be an event?
&lt;/h2&gt;

&lt;p&gt;Not every call deserves to be one. Here are the two rules I use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Service A can finish its job without knowing what Service B did with the info, make it an event.&lt;/strong&gt; When someone checks out, writing the order to the database has to succeed right now, so that stays synchronous. The confirmation email, the analytics update, the loyalty points? Nobody at the checkout button is waiting on those. Make them event consumers and your checkout gets faster for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your traffic is spiky, events are your shock absorber.&lt;/strong&gt; Synchronous APIs fall over under sudden load. A broker soaks up the spike and lets your workers chew through it at a pace they can survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  OK, the bill
&lt;/h2&gt;

&lt;p&gt;This is the part I wish someone had sent me before we shipped. To be clear, these numbers are a model I built from the public price list, not a bill from my employer. Prices are AWS list prices in US East, checked on 9 October 2026, free tier ignored.&lt;/p&gt;

&lt;p&gt;The inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt; charges $1.00 per million custom events published. Delivery to a target in the same account is free. Each 64 KB chunk of payload counts as one event.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;Lambda&lt;/a&gt; charges $0.20 per million requests, plus $0.0000166667 per GB-second of compute.&lt;/li&gt;
&lt;li&gt;My assumption for a consumer: 128 MB of memory and 100 ms per invocation. That's 0.0125 GB-seconds, or about $0.21 per million invocations in compute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So one "Order Placed" event with 12 consumers costs this much per million events:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Calculation&lt;/th&gt;
&lt;th&gt;Cost per 1M events&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Publish to EventBridge&lt;/td&gt;
&lt;td&gt;1M x $1.00/M&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda requests&lt;/td&gt;
&lt;td&gt;12M x $0.20/M&lt;/td&gt;
&lt;td&gt;$2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda compute&lt;/td&gt;
&lt;td&gt;12M x 0.0125 GB-s x $0.0000166667&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5.90&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And here it is at three volumes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Events per month&lt;/th&gt;
&lt;th&gt;1 consumer&lt;/th&gt;
&lt;th&gt;12 consumers&lt;/th&gt;
&lt;th&gt;12 consumers, 256 KB payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 million&lt;/td&gt;
&lt;td&gt;$1.41&lt;/td&gt;
&lt;td&gt;$5.90&lt;/td&gt;
&lt;td&gt;$8.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 million&lt;/td&gt;
&lt;td&gt;$141&lt;/td&gt;
&lt;td&gt;$590&lt;/td&gt;
&lt;td&gt;$890&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 billion&lt;/td&gt;
&lt;td&gt;$1,408&lt;/td&gt;
&lt;td&gt;$5,900&lt;/td&gt;
&lt;td&gt;$8,900&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things jump out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fan-out is the multiplier.&lt;/strong&gt; The broker is the cheap part. More than eighty percent of the 12-consumer cost is the consumers. Every new subscriber on a busy event adds its own request line and compute line, forever. So be picky about who gets to listen to what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep events skinny.&lt;/strong&gt; A 256 KB payload is billed as four events, which turns the $1.00 publish line into $4.00. Send an ID and what changed, like &lt;code&gt;{"order_id": "abc123", "status": "placed"}&lt;/code&gt;. If a consumer needs the whole entity, it can go fetch it from the database or a cache. That's the &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/claim-check" rel="noopener noreferrer"&gt;claim-check pattern&lt;/a&gt;, and the 256 KB column is what it saves you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infinite loops will ruin your week.&lt;/strong&gt; A service that hears an event, processes it, and accidentally emits the same event again will keep going until something stops it. At $5.90 per million that sounds harmless. But a loop doesn't run at your traffic rate. It runs as fast as the platform will scale. Set up &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html" rel="noopener noreferrer"&gt;dead-letter queues&lt;/a&gt;, retry limits, and loop detection before you ship. Lambda has &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/invocation-recursion.html" rel="noopener noreferrer"&gt;recursive loop detection&lt;/a&gt; for some services now. It only covers some of them, so go read which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It's a small model and I picked the inputs.&lt;/strong&gt; Twelve consumers at 128 MB and 100 ms is a believable fan-out, not a measured one. Double the duration or the memory and the compute line doubles too. I also left out logs, data transfer, the database reads claim-check adds, and whatever your consumers call downstream. Any of those can cost more than my whole table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At small volume, ignore all of this.&lt;/strong&gt; Under a few million events a month the entire event bill is lunch money, and your time is the expensive thing. Optimise for clarity and close this tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best objection is that the bill is the wrong thing to worry about.&lt;/strong&gt; Martin Fowler's &lt;a href="https://martinfowler.com/articles/201701-event-driven.html" rel="noopener noreferrer"&gt;survey of event-driven patterns&lt;/a&gt; argues the real price is losing sight of the flow: there's no single place in the code that shows what happens after "Order Placed". I think he's right, and I didn't put a number on that cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kafka changes the math.&lt;/strong&gt; A cluster you run or rent is priced by brokers and storage, not per event. Fan-out gets close to free and idle capacity becomes the thing you pay for. Everything above is about serverless brokers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Most teams go event-driven too early and for the wrong reasons. Events aren't the default way for services to talk. They're what you reach for when you need things to happen async and at scale. If a service needs another service's answer to finish its own job, a REST call is still the right tool, and there's zero shame in that.&lt;/p&gt;

&lt;p&gt;When you do make the jump, do the multiplication first: events per month, times consumers per event, times 64 KB chunks per payload. If that number scares you on a whiteboard, wait until it shows up on an invoice.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>eventdriven</category>
      <category>finops</category>
    </item>
    <item>
      <title>Microservices? Do You Really Need Them?</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:00:49 +0000</pubDate>
      <link>https://dev.to/0xadityaa/microservices-do-you-really-need-them-4gfd</link>
      <guid>https://dev.to/0xadityaa/microservices-do-you-really-need-them-4gfd</guid>
      <description>&lt;p&gt;Feels like every team runs &lt;a href="https://martinfowler.com/articles/microservices.html" rel="noopener noreferrer"&gt;microservices&lt;/a&gt; now. Split the monolith, they said. It'll scale better, they said. And honestly, they're not wrong. But chopping your app into a pile of independent services is the easy part. Keeping those services in sync when something blows up halfway through? That's the actual job.&lt;/p&gt;

&lt;p&gt;I've spent the past year as an associate engineer shipping and scaling production microservices, working closely with solution architects and principal engineers. And early on, one thing really bugged me. Our UI called an orchestrator, and the orchestrator called the real service. Why the extra hop? Just call the service directly. One less network call, less latency, simpler everything... right?&lt;/p&gt;

&lt;p&gt;Boy, was I wrong.&lt;/p&gt;

&lt;p&gt;So, do you really need microservices? &lt;strong&gt;Only if you're ready to own distributed transactions, because that's what you sign up for the second each service gets its own database.&lt;/strong&gt; And once you're there, my claim is that an orchestrated saga is how you run them. One component that owns the workflow beats a bunch of services reacting to each other's events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;When your data lives in several independent services, a boring multi-step operation (like placing an order) becomes a distributed transaction. If step three fails, something has to cleanly undo steps one and two. The &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;saga pattern&lt;/a&gt; is how you do that. It comes in two flavours: choreography (decentralised, event-driven) and orchestration (centralised, command-driven). Orchestration takes more setup and is almost always the right call as you grow. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What even is a distributed transaction?
&lt;/h2&gt;

&lt;p&gt;In a monolith, if something breaks mid-operation, the database rolls everything back for you. ACID has your back and life is good.&lt;/p&gt;

&lt;p&gt;With microservices, that safety net is gone. &lt;code&gt;Order Service&lt;/code&gt;, &lt;code&gt;Payment Service&lt;/code&gt;, and &lt;code&gt;Inventory Service&lt;/code&gt; each own their own database (the &lt;a href="https://microservices.io/patterns/data/database-per-service.html" rel="noopener noreferrer"&gt;database-per-service&lt;/a&gt; rule). No single transaction covers all three. So if the payment goes through and the inventory reservation fails, congrats, you have a half-finished order in production and real money stuck in the middle.&lt;/p&gt;

&lt;p&gt;That's the distributed transaction problem. It's the first wall every team runs into when they go all in on microservices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter the saga
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/saga" rel="noopener noreferrer"&gt;saga&lt;/a&gt; is a chain of local transactions. Each service does its bit. If a step fails, you run &lt;strong&gt;compensating transactions&lt;/strong&gt; to undo the steps that already worked. It's an undo plan baked into your architecture.&lt;/p&gt;

&lt;p&gt;There are two ways to wire one up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choreography vs orchestration
&lt;/h2&gt;

&lt;p&gt;Think of a restaurant kitchen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choreography&lt;/strong&gt; is a kitchen with no head chef. Every station listens for the previous one to yell that they're done, then does its thing. Grill hears "prep done!" and fires the steak. Fry hears "steak done!" and drops the fries. It works, right up until somebody misses a shout, or two stations react to the same shout, and now nobody knows what state the order is in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; is a kitchen with a head chef holding the ticket. They tell grill to fire, wait for the nod, then tell fry to drop. If something goes wrong, they know exactly what happened and who has to undo what.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choreography
&lt;/h3&gt;

&lt;p&gt;Services publish events. Other services listen and react. No central controller, everything decoupled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The good part:&lt;/strong&gt; services truly don't know about each other. On paper it's clean, pure microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bad part:&lt;/strong&gt; once a workflow grows past 3 or 4 services (my rule of thumb, not a measured threshold), debugging turns into a murder mystery. There's no single place to check where a transaction is. You're stitching together logs from six services and praying the timestamps line up. And cyclic dependencies creep in, because services start listening to each other's failure events just to compensate for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orchestration
&lt;/h3&gt;

&lt;p&gt;One central &lt;strong&gt;orchestrator&lt;/strong&gt; runs the whole workflow. It sends each service an explicit command, waits for a reply, and decides what happens next.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command-driven:&lt;/strong&gt; the orchestrator tells &lt;code&gt;Payment Service&lt;/code&gt; to charge and gets back "success". It tells &lt;code&gt;Inventory Service&lt;/code&gt; to reserve and gets back "failed". It tells &lt;code&gt;Payment Service&lt;/code&gt; to refund. Done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can see everything:&lt;/strong&gt; at any moment you can ask the orchestrator which step a transaction is on and why it stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compensation lives in one place:&lt;/strong&gt; when something fails, the orchestrator knows what already succeeded and fires the rollbacks in order. No guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Written out, that failed order is a short script you can read top to bottom, which is the whole point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI            -&amp;gt; Orchestrator : placeOrder(order-42)
Orchestrator  -&amp;gt; Payment      : charge(order-42)        &amp;lt;- ok
Orchestrator  -&amp;gt; Inventory    : reserve(order-42)       &amp;lt;- FAILED: out of stock
Orchestrator  -&amp;gt; Payment      : refund(order-42)        &amp;lt;- ok   (compensation)
Orchestrator  -&amp;gt; UI           : order-42 rejected: out of stock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With choreography the same five steps still exist. They're just scattered across the event handlers of three services, and no file has them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to my "why the extra hop?" question
&lt;/h2&gt;

&lt;p&gt;Here's why calling the service directly would have been worse.&lt;/p&gt;

&lt;p&gt;When our UI calls the orchestrator, the orchestrator isn't a middleman forwarding traffic. It's running a multi-step transaction that might touch 3 or 4 services. If the UI called each service itself, the UI would have to run the saga: decide what to call next, handle partial failures, send the compensation calls. You'd be building an orchestrator in your frontend. Please don't.&lt;/p&gt;

&lt;p&gt;Route through the orchestrator and the UI stays dumb and focused. The orchestrator stays smart and focused. Each service stays dumb and focused. Everybody has one job. That "extra network call" isn't overhead. It's the architecture doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why orchestration wins as you grow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Visibility.&lt;/strong&gt; One place to check the state of any business transaction. Priceless when you're on call at 2am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simpler services.&lt;/strong&gt; A service runs commands and reports back. It doesn't need to know the bigger business flow. That's the orchestrator's problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No cyclic dependencies.&lt;/strong&gt; Choreography breeds coupling in disguise, because services end up reacting to each other's events to handle failures. With orchestration the arrows only point one way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Easier testing.&lt;/strong&gt; Mock the service replies and you can test the whole workflow. No need to spin up the entire event mesh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The best objection: the orchestrator is a single point of failure and a bottleneck.&lt;/strong&gt; AWS says so in its own &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-orchestration.html" rel="noopener noreferrer"&gt;guidance on saga orchestration&lt;/a&gt;. Fair hit. Every workflow now leans on one component, so it has to persist its state, survive a restart mid-saga, and scale with all your traffic. That's why teams reach for a durable workflow engine like &lt;a href="https://aws.amazon.com/step-functions/" rel="noopener noreferrer"&gt;AWS Step Functions&lt;/a&gt; or &lt;a href="https://docs.temporal.io/" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; instead of rolling their own, and that's real operational weight choreography doesn't carry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple flows don't need it.&lt;/strong&gt; Two or three services and no compensation logic? &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-choreography.html" rel="noopener noreferrer"&gt;Choreography&lt;/a&gt; is less to build and less to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestrators attract business logic.&lt;/strong&gt; Keep yours to sequencing and compensation. Once pricing rules and validation move in, you've rebuilt the monolith with extra network calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You might not need the split at all.&lt;/strong&gt; Martin Fowler's &lt;a href="https://martinfowler.com/bliki/MonolithFirst.html" rel="noopener noreferrer"&gt;MonolithFirst&lt;/a&gt; points out that most successful microservice systems started as a monolith that got too big. A single database transaction is still the cheapest saga there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And this isn't a benchmark.&lt;/strong&gt; The comparison comes from working inside an orchestrated system and from reading, not from running both styles side by side on the same workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Choreography is a fine place to start. Orchestration is where a microservices setup that ships and scales tends to end up.&lt;/p&gt;

&lt;p&gt;Lots of teams start with choreography because it feels elegant. And for simple stuff, it is. But complexity always shows up eventually, and then you're spending more time debugging event flows than building features. An orchestrator forces your business logic to be explicit, visible, and testable. That's not overhead. That's just being kind to future you.&lt;/p&gt;

&lt;p&gt;My rule: if a business operation needs a compensating step in more than one service, give it an orchestrator. Future you, and whoever's on call, will say thanks.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>saga</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>LLMs Don't Read Your Prompt. They Count It.</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:59:24 +0000</pubDate>
      <link>https://dev.to/0xadityaa/llms-dont-read-your-prompt-they-count-it-2j0e</link>
      <guid>https://dev.to/0xadityaa/llms-dont-read-your-prompt-they-count-it-2j0e</guid>
      <description>&lt;p&gt;Ever typed a prompt into ChatGPT or Claude and wondered how this thing actually understands you? Spoiler: it doesn't read text the way we do. I got curious, went digging into how Large Language Models (LLMs) handle input, and it changed how I write prompts.&lt;/p&gt;

&lt;p&gt;The whole post in one sentence: &lt;strong&gt;a model never sees your text, it sees a list of integers called tokens, and that list is what you get billed for and what your context limit counts.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;LLMs don't work with raw text. They turn it into &lt;em&gt;tokens&lt;/em&gt;, which are just numbers, using algorithms like &lt;a href="https://en.wikipedia.org/wiki/Byte_pair_encoding" rel="noopener noreferrer"&gt;byte-pair encoding&lt;/a&gt;. Tokens are the currency of AI. You pay per token, not per character. Below I show text turning into tokens, tokens turning back into text, and why any of this matters for cost and quality. There's code, so you can try it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a token, and why should you care?
&lt;/h2&gt;

&lt;p&gt;If you've ever looked at your OpenAI bill (and winced), you'll have noticed you're charged by tokens, not words. So what is one? Picture a sentence chopped into bite-sized pieces. Sometimes a piece is a whole word, sometimes half a word, sometimes a comma. Each piece is a token. That's how models like GPT-4o or Llama take in text, and everything they do runs on it.&lt;/p&gt;

&lt;p&gt;Why care? More tokens in or out means more money. And once you get how tokens work, you write tighter prompts and get better answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: text goes in, numbers come out
&lt;/h2&gt;

&lt;p&gt;Every bit of text you send gets chopped into tokens by a &lt;em&gt;tokenizer&lt;/em&gt;. I played with &lt;code&gt;js-tiktoken&lt;/code&gt;, a JavaScript port of OpenAI's &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; and the tokenizer GPT-4o uses, to watch it happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tiktoken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-tiktoken/lite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;o200k_base&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js-tiktoken/ranks/o200k_base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tiktoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="c1"&gt;// NOTE: o200k_base is the tokenizer for GPT-4o&lt;/span&gt;
     &lt;span class="nx"&gt;o200k_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textToTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;};&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;textToTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content length in characters:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Number of tokens:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxArrayLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran that on a markdown file with 2,294 characters. Guess how many tokens came out? Just 484.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Content length in characters: 2294
  Number of tokens: 484
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about 4.7 characters per token for this file. Tokens aren't letters. They're chunks. One token might be a whole word like "hello", or a slice of a longer word. That's why the token count is way lower than the character count. It's still the number on your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: what the model actually chews on
&lt;/h2&gt;

&lt;p&gt;Here's the kicker. LLMs are trained on tokens, not text. All that data they learned from got tokenized first. When a model "writes" a reply, it's predicting the next token, a number, based on patterns it picked up in training. It isn't writing words. It's producing numbers that get turned into words afterwards.&lt;/p&gt;

&lt;p&gt;So the loop is: your prompt becomes a list of numbers, the model crunches them to predict more numbers, and those numbers get decoded into text. Kind of wild, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: numbers back to text
&lt;/h2&gt;

&lt;p&gt;Now the reverse. After the model outputs tokens, the tokenizer decodes them into something readable. I decoded a random token to see what I'd get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokensToText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;13984&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokensToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token &lt;code&gt;13984&lt;/code&gt; decodes to "VC". Random, and kind of funny. This decoding step is how a pile of numbers ends up as the reply you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tokens matter beyond the bill
&lt;/h2&gt;

&lt;p&gt;Tokens aren't only about cost. They're the unit the model works in. More text means more tokens, which means more compute and more money. Same for the response. Want to save tokens? Keep prompts tight and skip the fluff.&lt;/p&gt;

&lt;p&gt;It also explains context limits. A model can only handle a fixed number of tokens at once (like 8k or 128k), so a long conversation eventually gets cut off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for me
&lt;/h2&gt;

&lt;p&gt;When I started tuning prompts for my AI projects, thinking in tokens made a real difference. Shorter prompts didn't only save money. Responses came back faster and were often &lt;strong&gt;more accurate&lt;/strong&gt;, because the model wasn't wading through stuff it didn't need. If you build with LLMs, go play with a tokenizer. Watching a paragraph shrink into a list of numbers is weirdly satisfying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;These numbers are one tokenizer and one file.&lt;/strong&gt; &lt;code&gt;o200k_base&lt;/code&gt; is OpenAI's encoding. Claude, Gemini, and Llama each have their own, so the same text gets a different count on each. For Claude you ask the API through its &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/token-counting" rel="noopener noreferrer"&gt;token counting endpoint&lt;/a&gt;. My 4.7 characters per token came from English Markdown. Code, JSON, and non-English text usually split into more tokens per character.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Shorter prompts are more accurate" is what I saw, not a law.&lt;/strong&gt; Cutting junk context helped my projects. Cutting context the model needed would have hurt. The habit worth keeping is measuring, not trimming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens aren't the whole bill.&lt;/strong&gt; Output tokens cost more than input tokens on most price lists, and cached input is priced differently again. A count alone won't tell you the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;LLMs don't speak text. They speak tokens. It's all numbers back there, and knowing that makes you better at working with these things. Before you ship a prompt, run it through the tokenizer for the model you're actually calling. It's ten lines of code, and it swaps a guess for a number.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>tokens</category>
    </item>
    <item>
      <title>Semantic Search: What It Is and How I Built It</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:58:06 +0000</pubDate>
      <link>https://dev.to/0xadityaa/semantic-search-what-it-is-and-how-i-built-it-52bg</link>
      <guid>https://dev.to/0xadityaa/semantic-search-what-it-is-and-how-i-built-it-52bg</guid>
      <description>&lt;p&gt;A while back I got a fun one at work: add semantic search to our API pipeline so our LLM could answer customer questions more accurately, in real time. I had a rough idea of how semantic search worked (shoutout to my ML professor, that class finally paid off), but I had no clue where to start building it. So here's how it went.&lt;/p&gt;

&lt;p&gt;What I learned, in one sentence: &lt;strong&gt;most of the work in semantic search isn't the embedding model. It's picking a vector size and an index your database can actually serve fast.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Semantic search matches what you mean, not which words you typed. I built it with OpenAI's embeddings model, trimmed the vector dimensions so queries stayed quick, added an index, and switched our query flow from parameter matching to similarity. The result was search that got smarter and faster, which made the LLM's answers better too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is semantic search, and how is it different?
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxj72qepjpv200urlfrwm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxj72qepjpv200urlfrwm.png" alt="rest vs semantic search based agents" width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Old-school search does &lt;em&gt;lexical matching&lt;/em&gt;. It looks for the exact word, or part of it. Search for "laptop" and you get results with the word "laptop" in them. It has no idea that "chromebook" or "MacBook" might be what you're after.&lt;/p&gt;

&lt;p&gt;Semantic search &lt;em&gt;tries to match meaning&lt;/em&gt; instead of strings. So even if you don't type the exact keyword that's in the data, it can still find the stuff that's conceptually close. That's the whole trick, and it's why it beats plain keyword search for questions written like a human wrote them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Pick a model and generate embeddings
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;First you turn your data into embeddings. An embedding is a long list of numbers (a vector) that captures what a piece of text means.&lt;/li&gt;
&lt;li&gt;I used OpenAI's &lt;a href="https://platform.openai.com/docs/guides/embeddings" rel="noopener noreferrer"&gt;&lt;code&gt;text-embedding-3-large&lt;/code&gt;&lt;/a&gt;. You send text, you get a vector back. Mine came out at 3,072 dimensions. Great for accuracy, rough for performance, so I cut it to 2,000. More on why in a sec.&lt;/li&gt;
&lt;li&gt;You don't need multimodal embeddings unless you're dealing with images or video. Text-only did the job for me.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Pick the right number of dimensions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;More dimensions means more nuance and better results. It also means more compute and slower queries, unless you happen to own a supercomputer.&lt;/li&gt;
&lt;li&gt;Bigger vectors slow queries down and lean on your database, especially at scale. So you experiment until you find the balance. For me, going from 3,072 to 2,000 was a good trade between speed and relevance.&lt;/li&gt;
&lt;li&gt;That number is less random than it looks. The &lt;code&gt;text-embedding-3&lt;/code&gt; models are designed to be shortened through the API's &lt;code&gt;dimensions&lt;/code&gt; parameter, and &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; can only index its &lt;code&gt;vector&lt;/code&gt; type up to 2,000 dimensions.&lt;/li&gt;
&lt;li&gt;Your right number depends on your data, your use case, and how fast you need it to be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Index your embeddings
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Once the vectors are stored, you need an index. Without one, every query scans the whole dataset, and that gets slow fast.&lt;/li&gt;
&lt;li&gt;This is where &lt;a href="https://www.mongodb.com/resources/basics/ann-search" rel="noopener noreferrer"&gt;approximate nearest neighbor&lt;/a&gt; (ANN) search comes in. You give up a little recall and get a huge speedup, which is a trade most production setups happily take.&lt;/li&gt;
&lt;li&gt;The three index types I looked at:

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/understand-inverted-file-flat-vector-indexes.html" rel="noopener noreferrer"&gt;IVFFlat&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Uses k-means clustering to split your data into lists. A search only scans some of the lists, and you choose how many to probe, so you can dial speed against accuracy.&lt;/li&gt;
&lt;li&gt;It needs training, and it's good when you want knobs to tune.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pinecone.io/learn/series/faiss/hnsw/" rel="noopener noreferrer"&gt;HNSW&lt;/a&gt; (&lt;a href="https://arxiv.org/abs/1603.09320" rel="noopener noreferrer"&gt;original paper&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;Builds a multi-layered graph of vectors. The search starts at the top layer and narrows in as it gets closer to the best matches.&lt;/li&gt;
&lt;li&gt;Usually a better speed-to-recall trade than IVFFlat, with no training. You can even create the index on an empty table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.timescale.com/blog/understanding-diskann" rel="noopener noreferrer"&gt;DiskANN&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;Built for scale. High recall and low latency even with billions of records. It takes more time and memory to build, but it's a solid pick when you need high throughput.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4. Change the query flow
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;In a normal search setup you parse the user's query, pull out parameters, and build a SQL or NoSQL query. Semantic search flips that.&lt;/li&gt;
&lt;li&gt;You embed the user's plain-language question, then compare that vector against the ones stored in your database.&lt;/li&gt;
&lt;li&gt;There are a few ways to measure how close two vectors are:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Euclidean_distance" rel="noopener noreferrer"&gt;Euclidean distance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.datacamp.com/tutorial/manhattan-distance" rel="noopener noreferrer"&gt;Manhattan (or Taxicab) distance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Inner_product_space" rel="noopener noreferrer"&gt;Negative inner product&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Cosine_similarity" rel="noopener noreferrer"&gt;Cosine similarity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I went with cosine similarity because it worked well for text. Compare, grab the top n matches, return them. Now people can ask in plain language and still get relevant results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Was it worth it? Here are the benchmarks I ran:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37y0yxwzzddovldizspp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37y0yxwzzddovldizspp.png" alt="semantic search performance benchmarks" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Those benchmarks are one dataset on one system.&lt;/strong&gt; They show what happened with our data and our queries. Trust the direction, not the exact numbers, and rerun them on your own data before you believe anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic search is bad at exact matches.&lt;/strong&gt; Embeddings blur a product code, an error ID, or a rare name into its neighbours. Keyword search nails those instantly. Production systems usually run both and merge the results (hybrid search), and Postgres already ships &lt;a href="https://www.postgresql.org/docs/current/textsearch.html" rel="noopener noreferrer"&gt;full-text search&lt;/a&gt; for the keyword half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ANN indexes trade recall for speed, and they do it quietly.&lt;/strong&gt; An approximate index can miss the true best match and nothing will tell you. Measure recall against an exact scan on a sample before you trust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fewer dimensions means losing information.&lt;/strong&gt; Shortening the vector kept quality good enough for us. On a different corpus the cut-off that works could be higher or lower.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Semantic search and LLMs are a great pair. Instead of shoving all your data at the model, you use semantic search to narrow it down to the relevant bits first.&lt;/p&gt;

&lt;p&gt;That gets you faster responses, fewer tokens, and better answers. It also cuts down on hallucinations, because the model gets focused, relevant context and not a giant haystack to reason over.&lt;/p&gt;

&lt;p&gt;Short version: semantic search &lt;em&gt;filters&lt;/em&gt;, the LLM &lt;em&gt;interprets&lt;/em&gt;, and your users get something that feels a lot smarter.&lt;/p&gt;

</description>
      <category>vectorembeddings</category>
      <category>semanticsearch</category>
      <category>llm</category>
      <category>db</category>
    </item>
    <item>
      <title>Do You Really Need AI Agents?</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:56:42 +0000</pubDate>
      <link>https://dev.to/0xadityaa/do-you-really-need-ai-agents-3gp2</link>
      <guid>https://dev.to/0xadityaa/do-you-really-need-ai-agents-3gp2</guid>
      <description>&lt;p&gt;Whether you work with LLMs or not, you've heard "agent" and "agentic AI" about four thousand times by now. After months of building projects (hobby ones and production ones), eating a ton of content, and working daily with agentic frameworks like &lt;a href="https://langchain-ai.github.io/langgraph/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; and &lt;a href="https://cloud.google.com/vertex-ai?hl=en" rel="noopener noreferrer"&gt;Google Vertex AI&lt;/a&gt;, I have some thoughts.&lt;/p&gt;

&lt;p&gt;The short answer to the title: &lt;strong&gt;usually not. Most of what gets built as an agent would be cheaper, faster, and easier to trust as a fixed workflow. An agent should have to earn its spot with a measured result.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Agents are great when a task needs adaptability: messy inputs, lots of steps you can't predict, automation at scale. They're also wildly overused. For simple, predictable jobs, a workflow or even a single LLM call is more efficient and more reliable. So start with the boring solution, measure it, and bring in an agent only when the extra complexity clearly pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is everyone so hyped about agents?
&lt;/h2&gt;

&lt;p&gt;LLMs opened up automation that old-school algorithms couldn't touch. They can reason through a problem on the fly, so naturally every dev and every company wants that in their product, automating the repetitive stuff like updating docs or reviewing PRs. Even coding is semi-automated now, which backs up what OpenAI, Anthropic, and Google have been saying about LLMs handling the small stuff.&lt;/p&gt;

&lt;p&gt;Agentic AI wants to take that all the way and automate the whole process. As a software engineer, I get the urge. But the hype also brought a lot of FOMO, and now agents get thrown at problems a much simpler solution would handle. It's like using a bazooka to kill a fly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows vs agents: what's the difference?
&lt;/h2&gt;

&lt;p&gt;I use the split Anthropic draws in &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt;: in a workflow, your code decides the path. In an agent, the model does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflows
&lt;/h3&gt;

&lt;p&gt;A workflow is a predefined sequence. The LLM follows the code path you wrote, so it's predictable and structured. Every step has a clear goal.&lt;/p&gt;

&lt;p&gt;Say a user sends a message. A workflow could classify it as "query", "complaint", "feedback", or "information", then send back the matching predefined response. Perfect when consistency matters more than flexibility.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0shi6orr8g5e1pnc02r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0shi6orr8g5e1pnc02r.png" alt="ai workflow" width="800" height="854"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Agents
&lt;/h3&gt;

&lt;p&gt;An agent is more free-range. It doesn't follow a set path. It decides, step by step, how to find and use information. That makes it non-deterministic: it adapts to the context and to whatever tools it has.&lt;/p&gt;

&lt;p&gt;So where the workflow just classifies the message, an agent might go poke around several data sources, combine what it finds, and write a tailored answer. Powerful for complex tasks, and also a lot harder to predict and control.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuash6bxrv0p9xj3pdm11.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuash6bxrv0p9xj3pdm11.png" alt="ai agent" width="800" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you use an agent?
&lt;/h2&gt;

&lt;p&gt;When small time savings add up. Take expense reports: an agent can scan receipts, categorise expenses, and build the report. A person spends several minutes per receipt on that. Multiply by hundreds of reports and the manual work mostly disappears.&lt;/p&gt;

&lt;p&gt;Agents also shine when you need flexibility and lots of decisions that can't be scripted ahead of time. Just know the deal you're making: you pay in cost and latency to get better results on the task. Only take that deal when it's worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you NOT use an agent?
&lt;/h2&gt;

&lt;p&gt;When the inputs are so ambiguous that you can't verify the output. Think automated legal contract review, where the nuance of language and intent needs a human expert. Or generating personalised investment strategies, where subjective judgment is the whole game. In cases like that, a small misread turns into an expensive mistake.&lt;/p&gt;

&lt;p&gt;If the task needs strict reliability, a workflow is the better pick, because predictability and consistency are what get you there. And for a lot of apps, one well-tuned LLM call with &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" rel="noopener noreferrer"&gt;retrieval&lt;/a&gt; and a few in-context examples gets the job done without any of the extra machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I wrote this in March 2025, and models have moved since.&lt;/strong&gt; Things that needed a carefully scripted workflow back then can go to a single capable model call now, and agents fail less often than they used to. The line between "workflow" and "agent" shifts with every model generation. Treat my examples as dated and the method as the part that lasts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best case for going agent-first&lt;/strong&gt; is that a workflow encodes how you understand the task today, and you pay to rewrite it every time the task shifts. For open-ended work like coding or research, where you can't list the steps up front, starting with an agent is the honest design. Anthropic's guide carves out the same exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I didn't put numbers on any of this.&lt;/strong&gt; The cost and latency trade-off is stated, not measured. A fair version of this post would run one task both ways and report tokens, seconds, and error rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;My prediction when I wrote this: 2025 would be the year we go from "agentic" to "multi-agent" systems, with several specialised agents working together on complex workflows. Exciting stuff. It's also a great way to pile on complexity and cost if you're not measuring at every step.&lt;/p&gt;

&lt;p&gt;My rule: write the task as a single prompt first. Then as a fixed workflow. Reach for an agent only when you can point at the exact step where a fixed path fails, and you have an eval showing the agent does better.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>agents</category>
      <category>workflows</category>
    </item>
    <item>
      <title>What Exactly Is MCP?</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:55:23 +0000</pubDate>
      <link>https://dev.to/0xadityaa/what-exactly-is-mcp-47j7</link>
      <guid>https://dev.to/0xadityaa/what-exactly-is-mcp-47j7</guid>
      <description>&lt;p&gt;If you hang out in tech corners of LinkedIn, YouTube, or X, your feed has probably been buried in MCP posts. Mine was. So a few days into the noise I decided to actually dig in, and this post is everything I managed to find and understand so far.&lt;/p&gt;

&lt;p&gt;My one-line take: &lt;strong&gt;MCP is to AI tools what a common plug is to hardware. It doesn't make a model smarter. It makes a tool you write once usable from every app that speaks the protocol.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; (Model Context Protocol) is &lt;strong&gt;not&lt;/strong&gt; just another API. It's a standard way to hand structured context to AI models. Think USB-C for AI: one plug for data sources, tools, and workflows.&lt;/p&gt;

&lt;p&gt;Why does that matter? Because models are often context-blind. They struggle to use real-world information well. MCP helps by bridging data silos, tightening up security, and making integrations reusable across models. For devs, that means less glue code, faster builds, and setups that don't break every time you swap a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the big deal?
&lt;/h2&gt;

&lt;p&gt;Lots of people assume MCP is one more API. If you're picturing REST endpoints and JSON payloads, think bigger. MCP is a protocol that gives a model proper context about the tools it has, then lets the model's own reasoning decide which tool it needs and when.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does context matter so much?
&lt;/h3&gt;

&lt;p&gt;Today's models are seriously capable, but they trip up when they don't have the right context. In frameworks like LangChain, we handled that by giving models tools and teaching them when and how to use each one. MCP goes a step further and structures that context from the start, so the model isn't leaning on a vague system prompt and some tool descriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More context ~= better, more accurate output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's an analogy. Think about asking a friend for homework help. You wouldn't just say "How do I solve this?" You'd say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Hey, I'm working on algebra homework." (Domain)&lt;/li&gt;
&lt;li&gt;"We're learning about quadratic equations." (Specific topic)&lt;/li&gt;
&lt;li&gt;"I need to solve x² + 5x + 6 = 0." (Task)&lt;/li&gt;
&lt;li&gt;"Can you walk me through the steps?" (Request)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The traditional way, a model gets a prompt, reads whatever the system prompt says, and tries to work out which tools to use and how.&lt;/p&gt;

&lt;p&gt;MCP gives it structured context up front, so it can make a much better call about which tool to pick.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's wrong with how AI uses tools today?
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf1blb25zbs2ym0djna5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf1blb25zbs2ym0djna5.png" alt="AI tool use without MCP" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Image credit: Matt Pocock&lt;/cite&gt;&lt;/p&gt;

&lt;p&gt;Right now, models don't magically know how to use external tools. You have to teach them. Usually that means building custom data access tools, writing prompts (like docstrings) explaining when and how to use each one, and attaching it all to the LLM through a system prompt. Then you hope the model picks the right tool for the user's question.&lt;/p&gt;

&lt;p&gt;That approach has some real downsides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Glue code overload.&lt;/strong&gt; You write a pile of extra code just to make the tools behave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt engineering headaches.&lt;/strong&gt; The model depends on carefully worded system prompts to understand its tools, and getting that wording right is fiddly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tight coupling.&lt;/strong&gt; Change a tool and you have to update its docs, the system prompt, and the implementation. That's a maintenance headache that never goes away.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does MCP fix this?
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl38eog5yckdd0g20lcbe.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl38eog5yckdd0g20lcbe.png" alt="AI tool use with MCP" width="799" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Image credit: Matt Pocock&lt;/cite&gt;&lt;/p&gt;

&lt;p&gt;MCP takes out the guesswork by standardising how tools and data are accessed, so you're not starting from scratch for every new source. You build a tool once and reuse it across agents and apps.&lt;/p&gt;

&lt;p&gt;The model gets structured context up front, which cuts the ambiguity and the dependence on system prompts. You're no longer asking it to infer everything from fuzzy instructions. The key details are just there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bridging data silos.&lt;/strong&gt; A big problem in enterprise AI is that the important data is everywhere. Some in databases, some in cloud storage, some buried in Slack. MCP gives the model one unified way to reach all of it, so it's working with the full picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works with any model.&lt;/strong&gt; MCP isn't tied to one model. Claude, GPT, your own custom LLM, it handles context the same way across all of them. And it's modular, so it plays nicely with different data sources and systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security and access control.&lt;/strong&gt; Dealing with sensitive data? MCP builds in controlled access to data and tools, so a model only touches what it's supposed to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-built integrations.&lt;/strong&gt; MCP ships with ready-made connectors for things like Google Drive, Slack, GitHub, and databases, so you can hook a model up to common data sources quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you get started?
&lt;/h3&gt;

&lt;p&gt;MCP is still new, but it's an open standard, so you can start playing with it today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the official docs.&lt;/strong&gt; The &lt;a href="https://modelcontextprotocol.io/docs/learn/architecture" rel="noopener noreferrer"&gt;architecture overview&lt;/a&gt; and the &lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;specification&lt;/a&gt; are the primary sources. The &lt;a href="https://github.com/modelcontextprotocol/servers" rel="noopener noreferrer"&gt;reference servers&lt;/a&gt; are the best code to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try pre-built MCP servers.&lt;/strong&gt; Since it's open source, npm-style registries are popping up with servers for tools we use every day. The most useful ones I've found so far: &lt;a href="https://smithery.ai/" rel="noopener noreferrer"&gt;smithery&lt;/a&gt;, &lt;a href="https://mcp-get.com/" rel="noopener noreferrer"&gt;mcp-get&lt;/a&gt;, &lt;a href="https://glama.ai/mcp/servers" rel="noopener noreferrer"&gt;glama&lt;/a&gt;, &lt;a href="https://mcp.so/" rel="noopener noreferrer"&gt;mcp.so&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your own.&lt;/strong&gt; The &lt;a href="https://modelcontextprotocol.io/tutorials/building-mcp-with-llms" rel="noopener noreferrer"&gt;quick start guide&lt;/a&gt; walks you through creating a custom MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You don't need MCP for one app with a few tools.&lt;/strong&gt; Plain function calling does the same job with less machinery. The protocol pays off when the same tool has to work across several clients, or when you want to use servers other people wrote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best objection is security.&lt;/strong&gt; An MCP server is somebody else's code and instructions, running with your credentials and feeding text straight into your model. The spec's own &lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;security section&lt;/a&gt; says descriptions of tool behaviour should be treated as untrusted unless they come from a trusted server, and that users must explicitly consent to data access and tool use. Installing a server from a registry deserves the same care as installing a package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More tools isn't more capability.&lt;/strong&gt; Every connected server adds its tool definitions to the context window. Connect ten and your model burns tokens, and attention, reading menus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a summary of my reading.&lt;/strong&gt; Nothing here is a measurement or a production report. Treat it as an explainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;MCP is early. It promises a smarter way to connect AI to real-world data, and it's too soon to say how big it'll get. On the upside, standardising context could mean better accuracy, easier integrations, and smoother automation. The pre-built connectors and modular design make it tempting if you want simpler workflows.&lt;/p&gt;

&lt;p&gt;There are open questions too. Will companies actually adopt it? Can it scale across different models and enterprise systems? Right now it's an interesting idea with a lot of potential. If you're in the AI space, it's worth watching and worth a weekend of tinkering. Will it &lt;em&gt;truly&lt;/em&gt; change the agentic AI game? Guess we twiddle our thumbs and find out.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>A Folder That Contains Nothing Is a Lie</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:44:02 +0000</pubDate>
      <link>https://dev.to/0xadityaa/a-folder-that-contains-nothing-is-a-lie-5gnp</link>
      <guid>https://dev.to/0xadityaa/a-folder-that-contains-nothing-is-a-lie-5gnp</guid>
      <description>&lt;p&gt;Have you ever noticed that Gmail doesn't have folders?&lt;/p&gt;

&lt;p&gt;It has &lt;a href="https://support.google.com/mail/answer/118708" rel="noopener noreferrer"&gt;labels&lt;/a&gt;. An email can carry three labels, or none, and "removing" one doesn't delete anything. I never thought much about why, until I ran into the exact problem labels solve.&lt;/p&gt;

&lt;p&gt;Here's what I learned, and I learned it by getting it wrong: &lt;strong&gt;a folder makes you decide where something belongs at the moment you save it. That's a bet on where you'll need it later, and for some kinds of data you lose that bet constantly.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A folder is a decision made at write time about where you'll need something later.&lt;/li&gt;
&lt;li&gt;I filed every note by where it was written. Ask from anywhere else and the note was invisible, which broke the one thing the project was for.&lt;/li&gt;
&lt;li&gt;The fix: search everything, and treat "where it was written" as a ranking hint, not a wall.&lt;/li&gt;
&lt;li&gt;If a name in your interface promises containment and the system doesn't contain, rename it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The design that looked totally fine
&lt;/h2&gt;

&lt;p&gt;I've been building a memory layer for AI tools called &lt;a href="https://gethivemind.xyz" rel="noopener noreferrer"&gt;Hivemind&lt;/a&gt;. The idea is that you tell one tool something and your other tools already know it.&lt;/p&gt;

&lt;p&gt;My first data model was the obvious one. Every note gets a folder when it's written. Every search looks inside exactly one folder. Projects stay separate, results stay relevant, and everybody understands folders. Lovely.&lt;/p&gt;

&lt;p&gt;Then I made saving automatic. Nobody wants a "where should I file this?" popup every time they say something worth keeping. So the folder had to be guessed from wherever you were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a coding tool, connected over &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;: the repo you're in.&lt;/li&gt;
&lt;li&gt;In a browser chat: the site you're on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still sounds reasonable, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug you can't fix with code
&lt;/h2&gt;

&lt;p&gt;Now walk through the one thing the project exists to do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;In Claude Code, inside &lt;code&gt;acme/payments&lt;/code&gt;, you say you're migrating off Stripe Checkout&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acme/payments&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Later, in ChatGPT, you ask about the migration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chatgpt.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The search looks in one folder&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chatgpt.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;Nothing. The note never comes back.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The note is sitting right there in the database. The search will never see it, because it's in a different folder.&lt;/p&gt;

&lt;p&gt;That's not an edge case. That's the entire point of the thing, blocked by the storage model. And no ranking tweak can save you, because the note gets excluded before ranking even runs.&lt;/p&gt;

&lt;p&gt;Two smaller cracks pointed the same way. A company-wide convention you happen to mention inside one repo isn't a fact about that repo. And "the site you're on" says almost nothing about which project is in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide at read time
&lt;/h2&gt;

&lt;p&gt;The fix was to stop filing things.&lt;/p&gt;

&lt;p&gt;Notes don't go anywhere now. Every search looks at everything. What used to be a folder became a saved search, the same move as Gmail's labels or a &lt;a href="https://en.wikipedia.org/wiki/View_(SQL)" rel="noopener noreferrer"&gt;database view&lt;/a&gt;: a named way of looking at the data, not a box the data lives in.&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fwall-vs-weight.svg" 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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fwall-vs-weight.svg" alt="Diagram: with folders the note sits behind a wall the search never crosses, and with views there is one store where the note's origin only adjusts its rank" width="940" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "where was this written" signal didn't get thrown away. It got demoted from a wall to a weight. Each note still remembers where it came from, and if you ask from that same place, notes from there rank a bit higher. They're just never the only thing you can get.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Staging runs Postgres 16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask from &lt;code&gt;acme&lt;/code&gt; and that note gets a boost. Ask from anywhere else and it can still show up, if it's the best answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I had to rename it too
&lt;/h2&gt;

&lt;p&gt;I could've kept calling them folders. Everyone knows what a folder is.&lt;/p&gt;

&lt;p&gt;And that's the problem. If you take something out of a folder, you expect it to be gone from somewhere. With a saved search it isn't, because it was never "in" there. The name promises containment and the system can't deliver it. Keep the name, keep the broken promise.&lt;/p&gt;

&lt;p&gt;So the word is "view" now, everywhere.&lt;/p&gt;

&lt;p&gt;I also resisted reviving an older name I'd already retired once. Recycling dead vocabulary is how I ended up with one word, "session", meaning two unrelated things in the same schema: a stretch of work, and a browser login. That's exactly as confusing as it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A weight is not a wall.&lt;/strong&gt; If you need hard separation, say client A's context must never show up while you're working for client B, ranking won't give you that. A preference isn't a boundary. For real isolation you need a real partition, like separate accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Searching everything costs you.&lt;/strong&gt; With no folder to shrink the haystack, your relevance cutoff has to be good, or unrelated projects leak into each other's answers. I traded a structural guarantee for a quality problem. I think it's the right trade here. It's still a trade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Folders are great for plenty of things.&lt;/strong&gt; Files, photos, anything you'll go looking for yourself. Hierarchies are how people navigate. This is about data that has to find you, somewhere you didn't predict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Decide at read time" can go too far.&lt;/strong&gt; &lt;a href="https://en.wikipedia.org/wiki/Data_lake" rel="noopener noreferrer"&gt;Schema-on-read&lt;/a&gt; is the same idea taken to its limit, and plenty of data lakes turned into swamps that way. Some structure at write time is what keeps data usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Any time you pick a container when something is saved, you're predicting the future. For things a person will go fetch, that's fine. For things that have to turn up on their own, the prediction is wrong often enough to break what you're building.&lt;/p&gt;

&lt;p&gt;My rule now: decide at read time when you can, and keep the write-time signal as a hint, never a gate. And if a name in your interface promises something the system doesn't do, change the name before a user finds out for you.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>design</category>
      <category>datamodeling</category>
      <category>ai</category>
    </item>
    <item>
      <title>5% a Day Sounds Gentle. It Isn't.</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:42:51 +0000</pubDate>
      <link>https://dev.to/0xadityaa/5-a-day-sounds-gentle-it-isnt-1f9h</link>
      <guid>https://dev.to/0xadityaa/5-a-day-sounds-gentle-it-isnt-1f9h</guid>
      <description>&lt;p&gt;I found a bug recently that I can't stop thinking about, mostly because it was one innocent-looking line.&lt;/p&gt;

&lt;p&gt;I've been building a memory layer for AI tools (&lt;a href="https://gethivemind.xyz" rel="noopener noreferrer"&gt;Hivemind&lt;/a&gt;, if you're curious). Part of its job is ranking: you ask a question, it scores every saved note, and the best ones get sent to the model. Somewhere in that scoring was a small age penalty. Older notes lose 5% of their score per day. Recent stuff matters more. Seems fair.&lt;/p&gt;

&lt;p&gt;It was quietly destroying everything older than a month.&lt;/p&gt;

&lt;p&gt;The lesson I took, and the reason I'm writing this down: &lt;strong&gt;compounding turns "a little" into "everything" much faster than your gut says, and a test can only catch a bug its data is able to express.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A 5% daily penalty compounds. It halves a score every 13.5 days and leaves one ten-thousandth after six months.&lt;/li&gt;
&lt;li&gt;In my ranking code that meant any note older than a couple of months could never win, however well it matched.&lt;/li&gt;
&lt;li&gt;The benchmark missed it because every test note was the same age: zero.&lt;/li&gt;
&lt;li&gt;The fix is a half-life in months with a floor underneath, and a test input where age actually varies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How bad can 5% be?
&lt;/h2&gt;

&lt;p&gt;Bad. The trick is that it's 5% of what's left, every single day. That's &lt;a href="https://en.wikipedia.org/wiki/Exponential_decay" rel="noopener noreferrer"&gt;exponential decay&lt;/a&gt;, the same maths as radioactive stuff.&lt;/p&gt;

&lt;p&gt;Go on, guess what a note is worth after six months. "Maybe a third" feels about right, doesn't it? Here's the real answer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Age of the note&lt;/th&gt;
&lt;th&gt;Score multiplier (0.95 ^ days)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;0.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;0.0099&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;0.0001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;365 days&lt;/td&gt;
&lt;td&gt;0.00000001&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After a month, a note has lost four fifths of its score. After six months it's worth one ten-thousandth of an identical note written today. It could match the question word for word and still lose to basically anything from this week.&lt;/p&gt;

&lt;p&gt;A handy way to feel this: 5% a day is a &lt;a href="https://en.wikipedia.org/wiki/Half-life" rel="noopener noreferrer"&gt;half-life&lt;/a&gt; of about 13.5 days. Every two weeks, half of what was left is gone. Thirteen halvings in six months. Nothing survives thirteen halvings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The funny part
&lt;/h2&gt;

&lt;p&gt;A different part of the same system was doing the exact opposite.&lt;/p&gt;

&lt;p&gt;It tracked how often each note got used, and after five uses it promoted the note to a "long-term" tier. The comment in the code literally called it earned permanence.&lt;/p&gt;

&lt;p&gt;So one piece of code was carefully deciding which notes deserved to live forever. And another piece, a single multiplication nobody had looked at in ages, was killing them anyway. Two subsystems, opposite policies, and the quiet one won.&lt;/p&gt;

&lt;h2&gt;
  
  
  And there was a second one hiding next to it
&lt;/h2&gt;

&lt;p&gt;While I was in there I found its sibling.&lt;/p&gt;

&lt;p&gt;The system is supposed to fit the most useful notes into a fixed token budget. I'd been calling that a &lt;a href="https://en.wikipedia.org/wiki/Knapsack_problem" rel="noopener noreferrer"&gt;knapsack&lt;/a&gt;: pick the most valuable set that fits. What the code actually did was sort by score and fill from the top until the budget ran out.&lt;/p&gt;

&lt;p&gt;That's not packing. That's top-k with a cutoff. Cost never enters into it, so one 2,000-token note scoring 0.9 would shove out twenty 100-token notes scoring 0.85. Same budget. One answer where there could've been twenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why didn't the tests catch any of this?
&lt;/h2&gt;

&lt;p&gt;This is the bit that gets me, because I had a benchmark and it looked great.&lt;/p&gt;

&lt;p&gt;It looked great because every note in a benchmark run gets loaded at the same moment. Every age is zero. Every multiplier is 1. I could have set the decay to 99% a day and the score wouldn't have moved.&lt;/p&gt;

&lt;p&gt;Same story for the packing bug. If your test notes are all about the same length, "sort by score" and "pack by value" pick nearly the same set.&lt;/p&gt;

&lt;p&gt;The benchmark wasn't wrong. It just had no way to see the thing that was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Age is a nudge now.&lt;/strong&gt; A half-life measured in months, with a floor that age can never push a note below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified from the real thing.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recencyMultiplier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Long-term notes don't decay. That's what the tier means.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;longterm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;halfLife&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// days&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// age can never push a note below this&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;halfLife&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;Same ages, both curves:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Age of the note&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;0.21&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;0.0099&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;0.0001&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;365 days&lt;/td&gt;
&lt;td&gt;0.00000001&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fdecay-curves.svg" 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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fdecay-curves.svg" alt="Chart: with 5% daily decay a note's score is near zero within two months, while a 180-day half-life with a 0.35 floor still keeps 0.68 after six months" width="940" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A year-old note keeps about half its weight. If it's the best match, it still wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Packing actually packs.&lt;/strong&gt; Candidates get ranked by value per token (score divided by size) and added greedily, then a swap pass puts back any high-value note the small ones were blocking. It takes microseconds on fifty candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every constant in there is a guess.&lt;/strong&gt; 180 days and 0.35 aren't measured truths. They live in one config so I can sweep them later, and I fully expect them to move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greedy isn't optimal.&lt;/strong&gt; Value-per-token plus a swap gets within a constant factor of the best possible set. Exact dynamic programming is affordable at this size. I haven't seen it change a result, so I haven't switched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More notes isn't always better.&lt;/strong&gt; Twenty small notes mean twenty things for the model to read, and models are &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;worse at the middle of a long context&lt;/a&gt;. Packing is only half the job. Ordering matters too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sometimes you want things to fade fast.&lt;/strong&gt; If it's a scratchpad for this week's task, steep decay is a feature. My bug wasn't that decay existed. It ran on everything, including stuff the system had already decided to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Two habits I'm keeping.&lt;/p&gt;

&lt;p&gt;First, whenever I see "a small percentage, repeatedly", I stop and work out the half-life. It takes one line and it has embarrassed me every time.&lt;/p&gt;

&lt;p&gt;Second, for every signal that feeds a ranking, I go find the test input where that signal isn't a constant. If I can't find one, the signal is untested, however green the suite looks.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>retrieval</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Does Every AI Tool Forget You?</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:41:43 +0000</pubDate>
      <link>https://dev.to/0xadityaa/why-does-every-ai-tool-forget-you-3i26</link>
      <guid>https://dev.to/0xadityaa/why-does-every-ai-tool-forget-you-3i26</guid>
      <description>&lt;p&gt;Here's something that kept bugging me. I tell Claude Code how my project is set up. The next day I open Cursor and explain it again. Then I paste the same paragraph into ChatGPT, because of course it has no idea either.&lt;/p&gt;

&lt;p&gt;These tools are scary smart. So why do they all have the memory of a goldfish?&lt;/p&gt;

&lt;p&gt;I went digging, and the answer is simpler than I expected: &lt;strong&gt;a language model doesn't remember anything, ever. Every "memory" feature is an app pasting text into your prompt before the model sees it.&lt;/strong&gt; Once that clicks, a lot of weird behaviour starts to make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A model keeps no state between calls. A chat only feels continuous because the app resends the conversation every time.&lt;/li&gt;
&lt;li&gt;"Memory" is a search over saved notes, plus a paste into your prompt.&lt;/li&gt;
&lt;li&gt;Those notes live inside each app, which is why your other tools know nothing.&lt;/li&gt;
&lt;li&gt;I compared four ways of choosing what to paste. Fusing keyword and meaning search found the right note 79.5% of the time, at one fortieth the tokens of pasting everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wait, the model really remembers nothing?
&lt;/h2&gt;

&lt;p&gt;Nothing. A model is a function. Text goes in, text comes out, and it keeps no state between calls.&lt;/p&gt;

&lt;p&gt;The reason a chat feels continuous is that the app resends the whole conversation every time you hit enter. All of it has to fit in the &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/context-windows" rel="noopener noreferrer"&gt;context window&lt;/a&gt;, which is the fixed amount of text a model can look at in one go. When a conversation gets too long, something has to be dropped or summarised, and that's when it "forgets" what you said an hour ago.&lt;/p&gt;

&lt;p&gt;Start a new chat and the window is empty again. Hello, goldfish.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is "memory", then?
&lt;/h2&gt;

&lt;p&gt;A database and a paste.&lt;/p&gt;

&lt;p&gt;When a tool says it remembers you, here's what's actually happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At some point it saved a few sentences about you somewhere. "Prefers TypeScript." "Deploys on Fridays, bravely."&lt;/li&gt;
&lt;li&gt;When you ask something, it searches those sentences for ones that look relevant.&lt;/li&gt;
&lt;li&gt;It pastes the winners into the prompt, above your question, where you can't see them.&lt;/li&gt;
&lt;/ol&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fmemory-is-a-paste.svg" 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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fmemory-is-a-paste.svg" alt="Diagram: saved notes are searched, the relevant few are pasted into the prompt above your question, and a stateless model reads the result" width="940" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it. It's &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" rel="noopener noreferrer"&gt;retrieval-augmented generation&lt;/a&gt; pointed at your own notes. The model isn't remembering. It's reading a cheat sheet somebody slipped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then why doesn't my other tool know?
&lt;/h2&gt;

&lt;p&gt;Because the cheat sheet lives inside the app that wrote it.&lt;/p&gt;

&lt;p&gt;ChatGPT's notes sit on OpenAI's servers. Your editor keeps its own rules file. Your CLI agent has a different file in a different format. None of them can read each other's, so you become the sync mechanism, copy-pasting your own context around like it's 2005.&lt;/p&gt;

&lt;p&gt;This is the gap &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; is nudging at. If tools can call out to a shared server, the cheat sheet doesn't have to live inside any one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I got curious about
&lt;/h2&gt;

&lt;p&gt;Saving sentences is easy. The hard part is step two: picking what to paste.&lt;/p&gt;

&lt;p&gt;Paste too little and the model doesn't know the thing. Paste too much and you've burned your context window on trivia, and models are measurably &lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;worse at using stuff buried in the middle&lt;/a&gt; of a long prompt anyway.&lt;/p&gt;

&lt;p&gt;So I wanted a number. How often does a search actually bring back the note that contains the answer?&lt;/p&gt;

&lt;p&gt;I've been building a memory layer on the side (it's called &lt;a href="https://gethivemind.xyz" rel="noopener noreferrer"&gt;Hivemind&lt;/a&gt;, and it's the shared cheat sheet idea from the last section). I pointed it at &lt;a href="https://arxiv.org/abs/2402.17753" rel="noopener noreferrer"&gt;LoCoMo&lt;/a&gt;, a public benchmark of long conversations with questions about them, and compared three ways of picking ten notes per question. Same 1,536 questions for each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How the notes get picked&lt;/th&gt;
&lt;th&gt;Found the note with the answer&lt;/th&gt;
&lt;th&gt;Tokens pasted per question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paste the whole conversation&lt;/td&gt;
&lt;td&gt;99.7%&lt;/td&gt;
&lt;td&gt;18,853&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword search (&lt;a href="https://en.wikipedia.org/wiki/Okapi_BM25" rel="noopener noreferrer"&gt;BM25&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;63.9%&lt;/td&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meaning search (embeddings)&lt;/td&gt;
&lt;td&gt;70.4%&lt;/td&gt;
&lt;td&gt;389&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both searches fused, then filtered and packed&lt;/td&gt;
&lt;td&gt;79.5%&lt;/td&gt;
&lt;td&gt;465&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pasting everything "wins", and it's a trap.&lt;/strong&gt; You find the answer 99.7% of the time because the answer is in there somewhere, under 18,000 tokens of everything else. That's forty times the cost per question, every question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword search is better than its reputation.&lt;/strong&gt; 63.9% from an algorithm from the nineties. It's fast, it's cheap, and it nails exact names and IDs that embeddings blur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither search wins alone.&lt;/strong&gt; Fusing the two beat the better one by nine points. They fail on different questions, so together they cover for each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;That's retrieval, not answers.&lt;/strong&gt; "The note came back" isn't "the model answered right". When I measured the full loop with a tiny model answering, accuracy dropped a lot. Finding the note is necessary. It isn't sufficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One benchmark, in English, run offline.&lt;/strong&gt; LoCoMo is chatty conversation. Your codebase notes aren't. Against the live service, on a smaller sample, the same pipeline scored 78.2%, and I wrote up &lt;a href="https://gethivemind.xyz/docs/concepts/evidence" rel="noopener noreferrer"&gt;the caveats&lt;/a&gt; for that one too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I built one of the rows.&lt;/strong&gt; I tried to keep the comparison fair: same questions, same store, same number of notes. You should still read it knowing who ran it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bigger context windows change the maths.&lt;/strong&gt; As windows grow and tokens get cheaper, "paste everything" gets less silly. I don't think it wins, because attention is the scarce thing, not space. But I could be wrong about where the line ends up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Your AI tools don't have bad memories. They have no memories, and a thin layer of search-and-paste on top that happens to be locked inside each app.&lt;/p&gt;

&lt;p&gt;If you want to see it for yourself, try this. Tell one tool a fact. Open a different tool and ask a question that depends on it. Wherever that breaks is exactly where the cheat sheet stops, and now you know why.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>memory</category>
      <category>retrieval</category>
    </item>
    <item>
      <title>What Are LLMs, and How Do You Build Stuff With Them?</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 22:52:19 +0000</pubDate>
      <link>https://dev.to/0xadityaa/what-are-llms-and-how-to-build-stuff-using-it-4l68</link>
      <guid>https://dev.to/0xadityaa/what-are-llms-and-how-to-build-stuff-using-it-4l68</guid>
      <description>&lt;p&gt;Ever wondered how ChatGPT, Claude, Gemini, Deepseek, Llama and friends understand you and write back like a person? Here's the idea this whole post hangs on: &lt;strong&gt;an LLM only predicts the next token. So every useful app is the scaffolding you build around it: what context it sees, which tools it can call, and what it remembers.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is an LLM?
&lt;/h2&gt;

&lt;p&gt;A Large Language Model is an AI system built to process and generate human-like text. Under the hood it's deep learning, specifically the &lt;a href="https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)" rel="noopener noreferrer"&gt;Transformer architecture&lt;/a&gt; from the paper &lt;a href="https://arxiv.org/abs/1706.03762" rel="noopener noreferrer"&gt;Attention Is All You Need&lt;/a&gt;. These models are trained on enormous piles of text like &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb" rel="noopener noreferrer"&gt;fineweb&lt;/a&gt;, which is how they pick up language patterns, context, and meaning. That's what makes them good at generating text, translating, summarising, and a lot more.&lt;/p&gt;

&lt;p&gt;Old rule-based systems followed instructions someone wrote. LLMs are probabilistic. They predict the most likely next word based on what they saw in training. That's why they can handle such a wide range of questions and still give answers that fit the context.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five words you'll keep hearing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tokenization.&lt;/strong&gt; Breaking text into smaller units called tokens. A token can be a word, part of a word, or a single character, depending on the tokenizer. Models work on tokens, not raw text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings.&lt;/strong&gt; Lists of numbers (vectors) that represent words or phrases and capture how they relate to each other. They're how a model knows two different words mean similar things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context window.&lt;/strong&gt; The most text a model can handle at once. Bigger window, more it can keep in mind, more coherent the answers. Go past it and the early parts of the conversation fall off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature.&lt;/strong&gt; The randomness dial. Turn it up for creative, varied output. Turn it down for predictable, repeatable output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning.&lt;/strong&gt; Training a model further on a specific dataset so it gets better at a specialised job, like medical, legal, or financial work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What LLMs are good and bad at
&lt;/h2&gt;

&lt;p&gt;LLMs, and AI in general, are &lt;strong&gt;&lt;em&gt;NOT a magic bullet&lt;/em&gt;&lt;/strong&gt;. Here's where they shine and where they fall on their face.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where they're awesome
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Writing human-like, coherent text that fits the context.&lt;/li&gt;
&lt;li&gt;Pulling out and summarising big chunks of information fast.&lt;/li&gt;
&lt;li&gt;Working across lots of languages.&lt;/li&gt;
&lt;li&gt;Producing structured output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where they struggle
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;They hallucinate, and they're confident about it.&lt;/li&gt;
&lt;li&gt;Short memory and a limited context window.&lt;/li&gt;
&lt;li&gt;They can be biased, depending on the training data.&lt;/li&gt;
&lt;li&gt;Logical reasoning isn't their strong suit.&lt;/li&gt;
&lt;li&gt;They're expensive and energy-hungry to run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OK, that's the model. Now, how do you actually build something with it?&lt;/p&gt;

&lt;h2&gt;
  
  
  How to build with LLMs
&lt;/h2&gt;

&lt;p&gt;On its own, an LLM can't do much. If you want to solve a real problem and not just ship one more chatbot, you have to give it resources: internet access, tools for the tasks you need (calling an API, hitting a database), and so on. For that, you want &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangChain
&lt;/h3&gt;

&lt;p&gt;If you're serious about building apps with LLMs, LangChain is worth having in your toolbox. It gives you a structured way to talk to models and makes it much easier to hook them up to outside data sources, databases, and APIs. It comes with pre-built pieces for memory, for chaining several LLM calls together, and for using external tools like search APIs. Chatbot, research assistant, AI-powered knowledge base, whatever you're making, it takes a lot of the plumbing off your plate.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why LangChain rocks
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integrations.&lt;/strong&gt; Plug into different models, databases, and external APIs without much fuss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools.&lt;/strong&gt; The model can use search APIs, calculators, and other tools to give better answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chains.&lt;/strong&gt; String several LLM calls together like a conversation flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; Keep conversation context across multiple exchanges.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Keeping your AI chat on track
&lt;/h4&gt;

&lt;p&gt;LLMs have a limited attention span (that context window again), so long conversations get messy. Three ways to keep them manageable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trimming.&lt;/strong&gt; Cut the parts of the conversation you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering.&lt;/strong&gt; Keep only the most relevant messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarising.&lt;/strong&gt; Turn a long-winded chat into a short recap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  What can you build with it?
&lt;/h4&gt;

&lt;p&gt;Plenty: chatbots, smart search, writing assistants, automated research tools. It's great for anything that needs to talk to databases, external APIs, and knowledge retrieval systems.&lt;/p&gt;

&lt;p&gt;It has limits though. LangChain structures your AI interactions, but it's still stuck with the model's own constraints, like the context window and no long-term memory. And on its own it doesn't give you advanced decision-making or complex multi-step orchestration. That's where LangGraph comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  LangGraph
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://langchain-ai.github.io/langgraph/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; is a flexible framework for building dynamic, multi-step AI apps. Where a simple app is prompt in, answer out, LangGraph lets you build workflows with multiple decision points, several agents talking to each other, and structured automation. It's especially handy when your AI needs to go back and forth with users, juggle different workflows, or remember things across sessions.&lt;/p&gt;

&lt;p&gt;With it you can route queries, break a big task into smaller steps, and run things in parallel. Automating customer service flows, walking users through a process step by step, generating content with AI in the loop, it gives you the pieces to orchestrate all of that.&lt;/p&gt;

&lt;h4&gt;
  
  
  Storing conversation data
&lt;/h4&gt;

&lt;p&gt;Want your AI to "remember" things? LangGraph gives you options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory Saver.&lt;/strong&gt; Stores data locally for quick access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres Saver.&lt;/strong&gt; Uses PostgreSQL for external storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB.&lt;/strong&gt; Stores conversations in a NoSQL database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Saver.&lt;/strong&gt; Fast, efficient storage for AI memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Getting it production-ready
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Use a different thread ID for each conversation.&lt;/li&gt;
&lt;li&gt;Save conversation history with persistent checkpoints.&lt;/li&gt;
&lt;li&gt;Avoid huge user prompts that overwhelm the model.&lt;/li&gt;
&lt;li&gt;Stream responses so it feels smooth.&lt;/li&gt;
&lt;li&gt;Handle errors (rate limits, content moderation, and so on).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt engineering
&lt;/h3&gt;

&lt;p&gt;To get accurate, relevant answers out of an LLM, the prompt matters a lot. &lt;a href="https://en.wikipedia.org/wiki/Prompt_engineering" rel="noopener noreferrer"&gt;Prompt engineering&lt;/a&gt; is part art, part science, and how you structure the question changes the quality of the answer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Who's who in a conversation
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SystemMessage.&lt;/strong&gt; Sets the ground rules, behaviour, and personality of the AI. It's where you define a persona, a tone, or overall guidelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HumanMessage.&lt;/strong&gt; What the user sends, usually text typed by a person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AIMessage.&lt;/strong&gt; What the model sends back: text, or a request to call a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ToolMessage.&lt;/strong&gt; Carries the result of a tool call back to the model, usually when outside data got fetched or processed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You might not need a framework at all.&lt;/strong&gt; The best counter-argument comes from Anthropic's &lt;a href="https://www.anthropic.com/engineering/building-effective-agents" rel="noopener noreferrer"&gt;Building effective agents&lt;/a&gt;, which says to start with direct API calls, because frameworks add layers that hide the prompts and make debugging harder. If your app is one or two model calls, the plain SDK is less to learn and less to break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a map from February 2025.&lt;/strong&gt; Framework APIs in this space change every few months, and my list of things LLMs "struggle" with has shrunk since. Check the current docs before you copy a pattern from here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's an overview, not a build log.&lt;/strong&gt; There's no code in this post and no measurements. For a worked example, &lt;a href="https://www.0xadityaa.dev/projects/Finchat" rel="noopener noreferrer"&gt;Finchat&lt;/a&gt; is the app these notes came out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Building with LLMs is easier than ever with frameworks like LangChain and LangGraph. Chatbots, smart assistants, knowledge-search tools, they all come down to prompt engineering, context management, and conversation workflows.&lt;/p&gt;

&lt;p&gt;If you remember one thing: decide what the model needs to see, what it's allowed to do, and what it should remember. In that order. The right framework is whichever one makes those three decisions easiest to read.&lt;/p&gt;

&lt;p&gt;✌️ Stay curious, Keep coding, Peace nerds!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated 10 October 2026: rewrote this in plainer language and dropped the GIF. The central idea, primary sources, and limits section were added on 9 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>langchain</category>
      <category>langgraph</category>
    </item>
    <item>
      <title>I Wrote a JSON Parser From Scratch</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:25:51 +0000</pubDate>
      <link>https://dev.to/0xadityaa/implementing-a-json-parser-2od8</link>
      <guid>https://dev.to/0xadityaa/implementing-a-json-parser-2od8</guid>
      <description>&lt;p&gt;Ever wondered how an app actually understands the data an API sends it? Most of the time that data is JSON, a human-readable format that's all over the internet. But computers don't read "human-readable". Something has to turn that text into real data. That something is a JSON parser.&lt;/p&gt;

&lt;p&gt;In this devlog I build one with &lt;strong&gt;Deno&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt;. It can parse local JSON files and JSON responses from an API. The thing I want you to walk away with: &lt;strong&gt;a parser is two small programs. A tokenizer turns characters into tokens, and one recursive function per grammar rule turns tokens into a tree.&lt;/strong&gt; JSON's grammar is small enough that you can see the whole idea in one sitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JSON, and why is it everywhere?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.json.org/json-en.html" rel="noopener noreferrer"&gt;JSON&lt;/a&gt;, or &lt;em&gt;JavaScript Object Notation&lt;/em&gt;, is a lightweight, text-based format for exchanging data. It's the default for sending data around the internet, especially through APIs. Devs love it because it's simple and easy to read.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language-independent.&lt;/strong&gt; It isn't tied to any one programming language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real data structures.&lt;/strong&gt; You can nest objects and arrays as deep as you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to parse.&lt;/strong&gt; Which makes it fast to process.
&lt;/li&gt;
&lt;/ul&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;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;valid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;looks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;like:&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isDeveloper"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;You'll find it in API responses, config files, saved app settings, you name it. So how does software make sense of it? Parsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a parser?
&lt;/h2&gt;

&lt;p&gt;A parser is a program that turns text into structured data, and rejects anything that doesn't follow the grammar. For JSON, its job is to turn a raw string into objects you can use. That usually happens in two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tokenizing (lexical analysis).&lt;/strong&gt; Chop the input into meaningful chunks called tokens, like &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;"name"&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;"Alice"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing (building an AST).&lt;/strong&gt; Arrange those tokens into a tree, called an Abstract Syntax Tree (AST), that represents the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&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;Here's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;snippet:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isActive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;After tokenizing, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BraceOpen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Colon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Comma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isActive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Colon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;True&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BraceClose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after parsing, the &lt;strong&gt;Abstract Syntax Tree (AST)&lt;/strong&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;Here's the flow for a &lt;strong&gt;&lt;em&gt;valid&lt;/em&gt;&lt;/strong&gt; JSON:&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fjson-valid.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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fjson-valid.png" title="Valid JSON Parser Flow" alt="Valid JSON Parser Flow" width="798" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And for an &lt;strong&gt;&lt;em&gt;invalid&lt;/em&gt;&lt;/strong&gt; one:&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fjson-invalid.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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fjson-invalid.png" title="Invalid JSON Parser Flow" alt="Invalid JSON Parser Flow" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it with Deno and TypeScript
&lt;/h2&gt;

&lt;p&gt;I went with &lt;a href="https://deno.com/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt; because it supports TypeScript out of the box and ships with handy tools like a test runner and a coverage checker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The project structure is as follows:&lt;/span&gt;

/json-parser
      ├── main.ts          &lt;span class="c"&gt;# Entry point for the application&lt;/span&gt;
      ├── parser.ts        &lt;span class="c"&gt;# Contains the parser logic&lt;/span&gt;
      ├── tokenizer.ts      &lt;span class="c"&gt;# Contains the tokenizer logic&lt;/span&gt;
      ├── types.ts         &lt;span class="c"&gt;# Type definitions for tokens and AST nodes&lt;/span&gt;
      └── utils.ts         &lt;span class="c"&gt;# Utility functions for type checking&lt;/span&gt;
      │
      ├── main_test.ts     &lt;span class="c"&gt;# Contains tests for the parser and tokenizer&lt;/span&gt;
      └── test-data.json   &lt;span class="c"&gt;# Sample JSON data for testing&lt;/span&gt;
      │
      ├── deno.json                &lt;span class="c"&gt;# Deno configuration file&lt;/span&gt;
      ├── .vscode/settings.json     &lt;span class="c"&gt;# VSCode settings for Deno&lt;/span&gt;
      └── deno.lock                &lt;span class="c"&gt;# Deno lock file for dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The tokenizer (lexical analysis)
&lt;/h3&gt;

&lt;p&gt;Tokenizing is always step one when you write an interpreter or a compiler. Even your favourite code formatter tokenizes the whole file before it prettifies anything. You're breaking the input into small parts the parser can reason about, so it knows where each thing starts and stops.&lt;/p&gt;

&lt;p&gt;It also gives you the structure of the input: keywords, symbols, literals. And it's a big part of error handling. Once you can identify and categorise tokens, catching syntax errors gets a lot easier.&lt;/p&gt;

&lt;p&gt;One rule here: &lt;em&gt;only use the grammar from &lt;a href="https://ecma-international.org/publications-and-standards/standards/ecma-404/" rel="noopener noreferrer"&gt;ECMA-404&lt;/a&gt; when tokenizing JSON&lt;/em&gt;. (The diagrams on &lt;a href="https://www.json.org/json-en.html" rel="noopener noreferrer"&gt;json.org&lt;/a&gt; are the same grammar, drawn out.) That's how you make sure the tokenizer recognises JSON tokens correctly.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;tokenizer.ts&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BraceOpen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// handlers for other token types (e.g., strings, numbers, etc.)&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;h3&gt;
  
  
  The parser (building the AST)
&lt;/h3&gt;

&lt;p&gt;The parser is where the tokens start to mean something. Now we build the Abstract Syntax Tree. The AST captures the structure of the data as a tree, with the relationships between all the pieces: which values belong to which keys, what's nested inside what.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;parser.ts&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ASTNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;True&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;False&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BraceOpen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseObject&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// recursive fn&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BracketOpen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// recursive fn&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unexpected token type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing it
&lt;/h3&gt;

&lt;p&gt;To check the parser works and actually follows the &lt;strong&gt;&lt;em&gt;ECMA-404 standard&lt;/em&gt;&lt;/strong&gt;, we need tests. And we need the annoying edge cases too: &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt;\t&lt;/code&gt;, &lt;code&gt;\r&lt;/code&gt;, &lt;code&gt;\f&lt;/code&gt;, and &lt;code&gt;\b&lt;/code&gt; in strings, &lt;code&gt;-&lt;/code&gt; for negative numbers, and so on.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;main_test.ts&lt;/code&gt; looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parser should correctly parse an array with mixed types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[-1, "string\n\t\r\f\b", true, null, {}]`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ASTNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;String&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="se"&gt;\n\t\r\&lt;/span&gt;&lt;span class="s2"&gt;f&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;parser should throw an error for invalid JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`{
    "key": "value",
    "invalid": 
  }`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expected an error to be thrown for invalid JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unexpected token type: BraceClose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// More tests...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Tokenizing is the foundation. Break the input into small, understandable parts and the parser's job becomes manageable.&lt;/p&gt;

&lt;p&gt;The tokenizer takes a JSON string and hands back an array of tokens that describe the structure of the data. The parser takes those tokens and builds an AST that neatly organises the key-value pairs and nested structures.&lt;/p&gt;

&lt;p&gt;Put the two together and, yep, that's a JSON parser. I built one. Feels good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Passing my tests isn't the same as being correct.&lt;/strong&gt; My tests cover the cases I thought of. &lt;a href="https://github.com/nst/JSONTestSuite" rel="noopener noreferrer"&gt;JSONTestSuite&lt;/a&gt; has hundreds of files that real parsers disagree on, and it's the honest benchmark for any parser that claims to follow the standard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The standards leave things open.&lt;/strong&gt; ECMA-404 defines the syntax only. &lt;a href="https://datatracker.ietf.org/doc/html/rfc8259" rel="noopener noreferrer"&gt;RFC 8259&lt;/a&gt; adds interoperability advice and still leaves choices to whoever's implementing it: what to do with duplicate keys, how big a number can be, how deep nesting can go. My parser makes those choices without telling you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive descent has a depth limit.&lt;/strong&gt; Every nested array or object is another function call, so a deep enough document will blow the call stack. Production parsers either cap the depth or use an explicit stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please don't use this in production.&lt;/strong&gt; &lt;code&gt;JSON.parse&lt;/code&gt; is native code and heavily optimised. The reason to write your own is to understand the one you already have.&lt;/p&gt;




&lt;p&gt;Want the full code? Here's the &lt;a href="https://github.com/0xadityaa/json-parser" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;✌️ Stay curious, Keep coding, Peace nerds!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated 10 October 2026: rewrote this in plainer language and gave it a new title. A broken sentence, the ECMA-404 link, and the limits section were fixed or added on 9 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>typescript</category>
      <category>parsers</category>
    </item>
    <item>
      <title>I Built a Spell Checker to Watch an Algorithm Think</title>
      <dc:creator>Aditya Negandhi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:07:37 +0000</pubDate>
      <link>https://dev.to/0xadityaa/building-a-spell-checker-3i19</link>
      <guid>https://dev.to/0xadityaa/building-a-spell-checker-3i19</guid>
      <description>&lt;p&gt;Hey fellow devs! 👋 Let me tell you about my little adventure with the &lt;a href="https://en.wikipedia.org/wiki/Levenshtein_distance" rel="noopener noreferrer"&gt;Levenshtein distance algorithm&lt;/a&gt;. You know those moments when you find something that makes you go &lt;em&gt;"wait, that's actually pretty cool"&lt;/em&gt;? This was one of those.&lt;/p&gt;

&lt;p&gt;The idea in one line: &lt;strong&gt;a spell checker is an edit-distance function plus a dictionary. Count how many single-character edits separate the typed word from each known word, and suggest the closest ones.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The discovery
&lt;/h2&gt;

&lt;p&gt;There I was, grinding through DSA problems (like we all do), when I ran into Levenshtein distance. At first it looked like one more dynamic programming problem to stuff into my interview prep. Then I dug a little deeper and found out this algorithm is the backbone of spell checkers. You know, the thing that saves us from embarrassing typos in MS Word.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lightbulb moment
&lt;/h2&gt;

&lt;p&gt;My first thought was, "What if I could actually &lt;em&gt;see&lt;/em&gt; this algorithm work?" Not just input and output, but the whole process. So I built a spell checker visualizer. Nothing fancy, just something to help me (and maybe you) understand how the algorithm thinks. There's a &lt;a href="https://levenshtein-spell-checker.vercel.app/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;, and the &lt;a href="https://github.com/0xadityaa/levenshtein-spell-checker" rel="noopener noreferrer"&gt;source is on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The algorithm: Levenshtein distance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How it works (time complexity: O(m*n))
&lt;/h3&gt;

&lt;p&gt;Say you're comparing two words and you want to know how many changes it takes to turn one into the other. Let's use "wrld" and "world".&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make a grid (a matrix) where:

&lt;ul&gt;
&lt;li&gt;The first row is the letters of the first word, plus an empty slot.&lt;/li&gt;
&lt;li&gt;The first column is the letters of the second word, plus an empty slot.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fill the first row and first column with 0, 1, 2, 3 and so on. That's how many changes it takes to get from an empty string to each prefix.&lt;/li&gt;
&lt;li&gt;Now the fun part. For each cell, look at the two letters it lines up with and ask: &lt;strong&gt;"Are these the same?"&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If they're the &lt;strong&gt;SAME&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Copy the number from the diagonal upper-left cell.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If they're &lt;strong&gt;DIFFERENT&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Look at three neighbours (up, left, and diagonal upper-left).&lt;/li&gt;
&lt;li&gt;Take the smallest number.&lt;/li&gt;
&lt;li&gt;Add 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The number in the bottom-right cell is your answer: the minimum number of changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A basic implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;levenshteinDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;str2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="c1"&gt;// Matrix initialization&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;str1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;str2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// The magic happens here&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;str1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;str2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;str2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// substitution&lt;/span&gt;
            &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;     &lt;span class="c1"&gt;// insertion&lt;/span&gt;
            &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;// deletion&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;str1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;str2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;h3&gt;
  
  
  The dictionary
&lt;/h3&gt;

&lt;p&gt;Here's a simple example of how the dictionary is set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Dictionary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dictionary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;programming&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... more words&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;h2&gt;
  
  
  Building the visualizer
&lt;/h2&gt;

&lt;p&gt;I grabbed my favourite tools for the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite + React&lt;/strong&gt; (because who doesn't love a speedy dev experience?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (to keep my errors honest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS + shadcn/ui&lt;/strong&gt; (for a clean look, and yes, there's dark mode)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does is pretty simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You type some text.&lt;/li&gt;
&lt;li&gt;The app spots the misspelled words.&lt;/li&gt;
&lt;li&gt;And the cool part: it shows the Levenshtein matrix for each suggestion.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What it looks like
&lt;/h3&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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fmatrix.jpeg" 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%2Fwww.0xadityaa.dev%2Fimages%2Fblog%2Fmatrix.jpeg" title="Levenshtein Distance Matrix" alt="Interactive matrix visualization showing the Levenshtein distance calculation between 'wrld' and 'world'" width="480" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other ways to do it
&lt;/h2&gt;

&lt;p&gt;Levenshtein is great, but it's not the only game in town:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance" rel="noopener noreferrer"&gt;&lt;strong&gt;Damerau-Levenshtein distance&lt;/strong&gt;&lt;/a&gt;: also handles two adjacent letters being swapped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soundex&lt;/strong&gt;: matches words that sound alike.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N-gram similarity&lt;/strong&gt;: useful for longer text and for finding similar phrases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where I'd stop trusting this post
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Comparing against every word doesn't scale.&lt;/strong&gt; The matrix costs O(m*n) per pair, and a naive checker runs it against the entire dictionary for every typo. Fine for a hardcoded word list, way too slow for a real one. The usual fix is to index the dictionary so most words never get compared, for example with a &lt;a href="https://en.wikipedia.org/wiki/BK-tree" rel="noopener noreferrer"&gt;BK-tree&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain Levenshtein gets common typos wrong.&lt;/strong&gt; Swapping two adjacent letters ("teh") counts as two edits, so it can rank a worse suggestion first. It also has no idea which words are common. Peter Norvig's &lt;a href="https://norvig.com/spell-correct.html" rel="noopener noreferrer"&gt;spelling corrector&lt;/a&gt; shows how much word frequency matters, in about twenty lines of Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It checks words, not sentences.&lt;/strong&gt; "Their" when you meant "there" is spelled correctly and will never get flagged. Context needs a language model, and that's a different tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Is it basic? Absolutely. The dictionary is hardcoded and it won't be replacing Grammarly any time soon. But that was never the goal. I wanted to watch the algorithm turn "wrld" into "world" one edit at a time. And you know what? It works.&lt;/p&gt;

&lt;p&gt;Sometimes the simplest projects are the most satisfying. This wasn't about building the next big thing. It was about taking an algorithm out of interview prep and watching it solve a real problem.&lt;/p&gt;

&lt;p&gt;Not every project has to change the world. Sometimes just understanding how a thing works is reward enough. So here's to the small wins, the &lt;em&gt;"aha"&lt;/em&gt; moments, and the joy of seeing an algorithm come to life.&lt;/p&gt;

&lt;p&gt;✌️ Stay curious, Keep coding, Peace nerds!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated 10 October 2026: rewrote this in plainer language and gave it a new title. The core idea, sources, and limits section were added on 9 October 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
