<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged backend</title>
<link>https://coderlegion.com/tag/backend</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building an ATS Resume Scorer API with FastAPI, Celery, and Docker</title>
<link>https://coderlegion.com/33059/building-an-ats-resume-scorer-api-with-fastapi-celery-and-docker</link>
<description>&lt;p&gt;Most resumes are filtered by ATS software before a human ever reads them. This project is a backend API that checks how well a resume matches a job description and suggests how to improve it.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What it does&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PDF resume parsing:&lt;/strong&gt; extracts text from uploaded resumes using pdfplumber&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ATS keyword matching:&lt;/strong&gt; compares the resume against a job description and calculates a match score&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart suggestions:&lt;/strong&gt; generates actionable tips to improve the resume for a specific role&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT authentication:&lt;/strong&gt; secured endpoints with role-based access for Job Seekers and Recruiters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch scoring:&lt;/strong&gt; recruiters can score many resumes at once, handled in the background by Celery + Redis&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech stack&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Technology&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Backend &lt;/td&gt;&lt;td&gt;Python, FastAPI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Auth &lt;/td&gt;&lt;td&gt;JWT (python-jose, passlib)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Database &lt;/td&gt;&lt;td&gt;MySQL 8.0, SQLAlchemy ORM, Alembic&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Task queue &lt;/td&gt;&lt;td&gt;Celery + Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PDF parsing &lt;/td&gt;&lt;td&gt;pdfplumber&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DevOps &lt;/td&gt;&lt;td&gt;Docker, Docker Compose, AWS EC2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CI/CD &lt;/td&gt;&lt;td&gt;GitHub Actions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Testing &lt;/td&gt;&lt;td&gt;Pytest&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;design-highlights&quot;&gt;Design highlights&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Background processing:&lt;/strong&gt; batch scoring runs through a Celery queue, so the API stays responsive while many resumes are processed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role-based access:&lt;/strong&gt; Job Seekers and Recruiters get separate permissions on the same API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducible setup:&lt;/strong&gt; the whole stack (API, MySQL, Redis) starts with a single Docker Compose command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated deployment:&lt;/strong&gt; a GitHub Actions pipeline deploys automatically to AWS EC2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database migrations:&lt;/strong&gt; schema changes are managed with Alembic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;source-code&quot;&gt;Source code&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/priyadharshiny13/ats-resume-scorer&quot;&gt;https://github.com/priyadharshiny13/ats-resume-scorer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Feedback is welcome. If you were using a tool like this, what would matter more: a more accurate match score or more detailed improvement suggestions?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33059/building-an-ats-resume-scorer-api-with-fastapi-celery-and-docker</guid>
<pubDate>Fri, 30 Oct 2026 07:57:26 +0000</pubDate>
</item>
<item>
<title>Building a Booking System with Node.js, Google Calendar, and Google Meet</title>
<link>https://coderlegion.com/32649/building-a-booking-system-with-node-js-google-calendar-and-google-meet</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Booking a consultation should be straightforward for the person making the appointment. Behind the scenes, however, an automated booking system has to coordinate several moving parts: collecting customer information, checking availability, scheduling an event, generating a meeting link, saving the booking, and notifying the people involved.&lt;/p&gt;
&lt;p&gt;I encountered these challenges while developing the booking system for JOPAD Advisory. I built the system from scratch, using Node.js and Express for the backend, MongoDB for storing bookings, Google Calendar for scheduling, Google Meet for virtual meetings, and Resend for email notifications.&lt;/p&gt;
&lt;p&gt;In this article, I'll walk through how I implemented the booking workflow, how the different services work together, and some of the engineering considerations that come with connecting external APIs to a backend application.&lt;/p&gt;
&lt;h3 id=&quot;1-the-architecture&quot;&gt;1. The Architecture&lt;/h3&gt;
&lt;p&gt;The backend coordinates the booking process. Each service has a specific responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js and Express: Handle booking requests and coordinate the workflow.&lt;/li&gt;
&lt;li&gt;MongoDB and Mongoose: Store booking details and provide access to existing appointments.&lt;/li&gt;
&lt;li&gt;Google Calendar API: Create calendar events for scheduled sessions.&lt;/li&gt;
&lt;li&gt;Google Meet: Generate a virtual meeting link through the Calendar event's conference configuration.&lt;/li&gt;
&lt;li&gt;Airtable: Receive a secondary record of the booking.&lt;/li&gt;
&lt;li&gt;Resend: Power the email notification workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The booking process follows this sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A customer submits a booking request through the website.&lt;/li&gt;
&lt;li&gt;The Express endpoint validates the submitted fields.&lt;/li&gt;
&lt;li&gt;The backend calculates the appointment's start and end times.&lt;/li&gt;
&lt;li&gt;Existing bookings are checked for overlapping time intervals.&lt;/li&gt;
&lt;li&gt;Google Calendar creates an event and requests a Google Meet conference.&lt;/li&gt;
&lt;li&gt;The booking is saved to MongoDB with its meeting link.&lt;/li&gt;
&lt;li&gt;The backend attempts to send a secondary record to Airtable.&lt;/li&gt;
&lt;li&gt;Booking notification emails are triggered.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This design brings several services together behind a single booking endpoint, giving the frontend one place to submit the request.&lt;/p&gt;
&lt;h3 id=&quot;2-building-the-booking-endpoint&quot;&gt;2. Building the Booking Endpoint&lt;/h3&gt;
&lt;p&gt;I used an Express POST endpoint to receive booking requests. The request contains the appointment time, duration, customer's contact details, session description, and other booking information.&lt;/p&gt;
&lt;p&gt;The first step is to reject incomplete requests before performing external API operations.&lt;/p&gt;
&lt;p&gt;A simplified version of the validation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;router.post(&quot;/book&quot;, async (req, res) =&amp;gt; {
  const {
    time,
    duration,
    email,
    firstName,
    lastName,
    description
  } = req.body;

  if (
    !time ||
    !duration ||
    !email ||
    !firstName ||
    !lastName ||
    !description
  ) {
    return res.status(400).json({
      message: &quot;Missing required fields&quot;
    });
  }

  // Continue with availability checks
  // and booking creation.
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a simplified illustration of the validation logic in my implementation, not a complete replacement for the original endpoint.&lt;/p&gt;
&lt;p&gt;The actual endpoint validates additional fields, including the selected date, day, time range, phone number, and referral information.&lt;/p&gt;
&lt;p&gt;The important principle is to validate the data required by the booking workflow before creating external resources or writing records.&lt;/p&gt;
&lt;p&gt;For a production system, request validation should also verify data types, valid email addresses, positive durations, valid dates, and acceptable appointment times. A field being present does not necessarily mean its value is valid.&lt;/p&gt;
&lt;h3 id=&quot;3-calculating-appointment-duration&quot;&gt;3. Calculating Appointment Duration&lt;/h3&gt;
&lt;p&gt;A booking needs both a start time and an end time. The end time determines the appointment's actual interval and makes it possible to detect overlapping bookings.&lt;/p&gt;
&lt;p&gt;In my implementation, the backend calculates the end time using the requested start time and duration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const startTime = new Date(time);
const endTime = new Date(startTime);

endTime.setMinutes(
  endTime.getMinutes() + Number(duration)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, an appointment starting at 10:00 AM with a duration of 60 minutes should end at 11:00 AM.&lt;/p&gt;
&lt;p&gt;Calculating the interval on the backend is important because scheduling decisions should not depend solely on values calculated by the browser.&lt;/p&gt;
&lt;p&gt;It is also important to handle time zones consistently. My Google Calendar integration sends ISO-formatted timestamps to the API. A more comprehensive implementation should explicitly define the application's scheduling time zone and validate incoming timestamps to prevent ambiguity around local times and daylight-saving transitions.&lt;/p&gt;
&lt;h3 id=&quot;4-preventing-overlapping-bookings&quot;&gt;4. Preventing Overlapping Bookings&lt;/h3&gt;
&lt;p&gt;One of the central problems in a booking system is preventing customers from selecting times that have already been reserved.&lt;/p&gt;
&lt;p&gt;Two appointments overlap when the first appointment starts before the second one ends and finishes after the second one starts.&lt;/p&gt;
&lt;p&gt;The logic can be expressed as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const overlaps =
  requestedStart &amp;lt; existingEnd &amp;amp;&amp;amp;
  requestedEnd &amp;gt; existingStart;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This condition also allows back-to-back appointments. For example, a booking ending at 11:00 AM does not conflict with another booking beginning at exactly 11:00 AM.&lt;/p&gt;
&lt;p&gt;In my implementation, I retrieve existing bookings from MongoDB, calculate each booking's end time from its start time and duration, and check for conflicts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const existingBookings = await Booking.find();

const conflict = existingBookings.some(booking =&amp;gt; {
  const existingStart = new Date(booking.startTime);
  const existingEnd = new Date(existingStart);

  existingEnd.setMinutes(
    existingEnd.getMinutes() +
      Number(booking.duration)
  );

  return (
    startTime &amp;lt; existingEnd &amp;amp;&amp;amp;
    endTime &amp;gt; existingStart
  );
});

if (conflict) {
  return res.status(409).json({
    message: &quot;This time slot has already been booked&quot;
  });
}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returning HTTP &lt;code&gt;&quot;409 Conflict&quot;&lt;/code&gt; communicates that the request conflicts with an existing booking.&lt;/p&gt;
&lt;p&gt;This approach is easy to understand, but it has a scalability limitation: the implementation loads all existing bookings before checking for an overlap. As the collection grows, a database query restricted to potentially overlapping intervals would be more efficient.&lt;/p&gt;
&lt;p&gt;There is also an important concurrency consideration. Two requests can check availability at almost the same time, both find the slot free, and both continue. The check alone does not guarantee that simultaneous requests cannot create overlapping bookings.&lt;/p&gt;
&lt;p&gt;A stronger production design would use an atomic reservation strategy, such as reserving discrete time slots with a unique database constraint or using another concurrency-control mechanism. That protection needs to be designed around the application's scheduling rules rather than relying only on an initial availability check.&lt;/p&gt;
&lt;h3 id=&quot;5-connecting-google-calendar-and-google-meet&quot;&gt;5. Connecting Google Calendar and Google Meet&lt;/h3&gt;
&lt;p&gt;After the backend confirms that a requested slot does not conflict with an existing booking, it calls a dedicated utility function to create the meeting.&lt;/p&gt;
&lt;p&gt;I separated this logic into a Google Calendar utility instead of placing the entire integration directly inside the Express route. This keeps the route focused on the booking workflow and makes the external integration easier to maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authenticating with Google&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The utility initializes an OAuth 2.0 client using environment variables and sets a refresh token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { google } = require(&quot;googleapis&quot;);

const oauth2Client = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  process.env.GOOGLE_REDIRECT_URI
);

oauth2Client.setCredentials({
  refresh_token: process.env.GOOGLE_REFRESH_TOKEN
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example uses the conventional environment-variable spelling for the client secret. In the project, the environment variable is named &lt;code&gt;&quot;GOOGLE_CLIENT_SECRETE&quot;&lt;/code&gt;, so the deployed configuration must match the actual code unless that naming is corrected.&lt;/p&gt;
&lt;p&gt;The refresh token allows the server-side client to obtain access tokens as needed without requiring the user to repeat the authorization flow for every booking.&lt;/p&gt;
&lt;p&gt;This requires the initial OAuth authorization to have been completed successfully and the refresh token to remain valid. The Google account must also have the necessary permissions and configuration for creating Calendar events and conference links.&lt;/p&gt;
&lt;p&gt;Credentials should be stored securely in environment variables or an appropriate secrets manager, never committed to a public repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating a Calendar event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The integration uses Google's Calendar API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const calendar = google.calendar({
  version: &quot;v3&quot;,
  auth: oauth2Client
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The meeting function calculates the event's end time and constructs a Calendar event containing the session title, description, start and end timestamps, and attendee information.&lt;/p&gt;
&lt;p&gt;The attendee is represented by the customer's email address:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;attendees: [
  {
    email,
    responseStatus: &quot;accepted&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event also includes settings that prevent guests from modifying the event, inviting other guests, or seeing other guests.&lt;/p&gt;
&lt;p&gt;These settings are part of the event configuration; they do not replace proper authorization and access control in the booking application itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requesting a Google Meet link&lt;br&gt;
&lt;/strong&gt;&lt;br&gt;
The event configuration includes a conference creation request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;conferenceData: {
  createRequest: {
    requestId: &quot;jopad-&quot; + Date.now(),
    conferenceSolutionKey: {
      type: &quot;hangoutsMeet&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event is then inserted into the primary Google Calendar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const response = await calendar.events.insert({
  calendarId: &quot;primary&quot;,
  resource: event,
  conferenceDataVersion: 1,
  sendUpdates: &quot;all&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &quot;conferenceDataVersion: 1&quot; option tells Google Calendar that conference data should be processed as part of the request. The &quot;sendUpdates: &quot;all&quot;&quot; option requests that Calendar send event updates to the relevant participants.&lt;/p&gt;
&lt;p&gt;After the API call succeeds, the implementation extracts the meeting URL from the returned conference data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const meetLink =
  response.data.conferenceData.entryPoints[0].uri;

return meetLink;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The booking route receives that link and includes it in the booking record.&lt;/p&gt;
&lt;p&gt;One important implementation detail is that the code assumes the response contains a conference entry point. A more defensive implementation would verify that the response contains a usable Meet URL before continuing.&lt;/p&gt;
&lt;p&gt;Google Calendar conference creation can also involve asynchronous processing. Production code should account for cases where the event exists but the conference details are not yet available, rather than assuming every successful event insertion immediately includes a meeting link.&lt;/p&gt;
&lt;h3 id=&quot;6-saving-bookings-in-mongodb&quot;&gt;6. Saving Bookings in MongoDB&lt;/h3&gt;
&lt;p&gt;Once the meeting link has been returned, the backend creates a Mongoose booking document.&lt;/p&gt;
&lt;p&gt;The model stores information such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Appointment start time and duration.&lt;/li&gt;
&lt;li&gt;Customer name and contact information.&lt;/li&gt;
&lt;li&gt;Session description.&lt;/li&gt;
&lt;li&gt;Date and time display values.&lt;/li&gt;
&lt;li&gt;Referral information.&lt;/li&gt;
&lt;li&gt;Google Meet link.&lt;/li&gt;
&lt;li&gt;Record creation timestamp.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The central fields include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const bookingSchema = new mongoose.Schema({
  startTime: {
    type: Date,
    required: true
  },
  duration: String,
  email: String,
  firstName: String,
  lastName: String,
  description: String,
  meetLink: String,
  createdAt: {
    type: Date,
    default: Date.now
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an excerpt of the schema; the complete project model contains additional fields.&lt;/p&gt;
&lt;p&gt;After constructing the document, the route saves it:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await newBooking.save();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Saving the booking after generating the meeting link ensures that the returned booking record can contain the link associated with the created event.&lt;/p&gt;
&lt;p&gt;However, the order of these operations creates an important failure scenario. If Google Calendar successfully creates the event but MongoDB subsequently fails to save the booking, a calendar event may remain without a corresponding booking record.&lt;/p&gt;
&lt;p&gt;This is a common challenge when coordinating operations across independent services. A more resilient design could track workflow states, retry recoverable operations, and provide a reconciliation or cleanup process for partially completed bookings.&lt;/p&gt;
&lt;h3 id=&quot;7-adding-an-airtable-backup&quot;&gt;7. Adding an Airtable Backup&lt;/h3&gt;
&lt;p&gt;The system also attempts to send booking information to Airtable after saving the MongoDB document.&lt;/p&gt;
&lt;p&gt;The request includes selected booking fields, such as the customer's name, email, phone number, session description, referral details, meeting link, and appointment date.&lt;/p&gt;
&lt;p&gt;The operation is wrapped in its own &quot;try...catch&quot; block. If the Airtable request fails, the error is logged, but the main booking workflow can still continue.&lt;/p&gt;
&lt;p&gt;This makes Airtable a secondary destination rather than the primary source of truth for the booking.&lt;/p&gt;
&lt;p&gt;That distinction matters: the implementation attempts a backup write, but it does not guarantee that the MongoDB and Airtable records are always synchronized. If Airtable is unavailable, the backup attempt fails without automatically retrying it.&lt;/p&gt;
&lt;p&gt;A more robust implementation could place failed backup operations into a retry queue or maintain a record of synchronization status.&lt;/p&gt;
&lt;h3 id=&quot;8-sending-booking-notifications&quot;&gt;8. Sending Booking Notifications&lt;/h3&gt;
&lt;p&gt;After the booking has been saved and the Airtable attempt has completed, the route triggers the email notification utility:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;sendBookingEmails({
  email,
  firstName,
  lastName,
  dayString,
  referral,
  dateString,
  number,
  description,
  timeString,
  timeRange,
  meetLink
}).catch(err =&amp;gt; {
  console.log(&quot;Email failed:&quot;, err.message);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The email operation is not awaited by the route. This allows the endpoint to return its successful booking response without waiting for the notification workflow to finish.&lt;/p&gt;
&lt;p&gt;It also means an email failure does not change the response after the booking has been saved.&lt;/p&gt;
&lt;p&gt;There is a trade-off: the current implementation logs email failures but does not automatically retry them or record a delivery status in the booking document. For a production booking service, a persistent background job or message queue would provide a stronger way to retry notifications and monitor delivery outcomes.&lt;/p&gt;
&lt;p&gt;The route eventually responds with HTTP &lt;code&gt;&quot;201 Created&quot;&lt;/code&gt;, along with a success message and the saved booking.&lt;/p&gt;
&lt;h3 id=&quot;9-lessons-from-the-implementation&quot;&gt;9. Lessons from the Implementation&lt;/h3&gt;
&lt;p&gt;Building this workflow brought several backend engineering considerations into focus.&lt;/p&gt;
&lt;p&gt;Validate before creating external resources. Checking the request and current bookings before calling Google reduces unnecessary API operations.&lt;/p&gt;
&lt;p&gt;Separate responsibilities. Keeping the Calendar integration in its own utility makes the Express route easier to understand and maintain.&lt;/p&gt;
&lt;p&gt;Handle partial failures. MongoDB, Google Calendar, Airtable, and an email provider are independent services. A successful operation in one does not guarantee success in the others.&lt;/p&gt;
&lt;p&gt;Treat concurrency as a separate problem. Checking for overlapping bookings is necessary, but it does not by itself protect against two simultaneous requests reserving the same slot.&lt;/p&gt;
&lt;p&gt;Design for observability and recovery. Logging an error helps with debugging, but production systems also benefit from structured logs, retry mechanisms, delivery tracking, and reconciliation workflows.&lt;/p&gt;
&lt;p&gt;These considerations extend beyond booking applications. Any backend that coordinates a database with multiple external APIs needs a plan for partial failures and retries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building the JOPAD Advisory booking system from scratch gave me practical experience connecting a Node.js backend to Google Calendar, generating Google Meet links, storing bookings in MongoDB, maintaining a secondary Airtable record, and triggering email notifications.&lt;/p&gt;
&lt;p&gt;The most important lesson is that integrating APIs is only one part of building a reliable application. The surrounding workflow—validation, conflict detection, persistence, failure handling, and recovery—is just as important.&lt;/p&gt;
&lt;p&gt;A booking endpoint can appear simple from the frontend, but it coordinates several systems that can succeed or fail independently. Understanding those interactions is what turns an API integration into a more complete engineering solution.&lt;/p&gt;
&lt;p&gt;I'm continuing to explore ways to make these workflows more resilient, especially around concurrent bookings, retries, and recovery from partial failures.&lt;/p&gt;
&lt;p&gt;If you're building a booking platform or integrating third-party services into a Node.js application, I'd be interested to hear how you approach these challenges.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32649/building-a-booking-system-with-node-js-google-calendar-and-google-meet</guid>
<pubDate>Tue, 27 Oct 2026 07:58:12 +0000</pubDate>
</item>
<item>
<title>is-kit Reached 100 Stars ⭐🎉 Here’s What Changed Since 50</title>
<link>https://coderlegion.com/32180/is-kit-reached-100-stars-heres-what-changed-since-50</link>
<description>&lt;p&gt;Hoi hoi!&lt;/p&gt;
&lt;p&gt;YAY! &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; just reached &lt;strong&gt;100 GitHub stars⭐&lt;/strong&gt; 🎉🎉🎉&lt;/p&gt;
&lt;p&gt;When it reached 50 stars, I wrote about how we use it in a production TypeScript application serving more than 100,000 users. 👇&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/nyaomaru/is-kit-reached-50-stars-heres-how-we-use-it-in-production-2i5b&quot;&gt;https://dev.to/nyaomaru/is-kit-reached-50-stars-heres-how-we-use-it-in-production-2i5b&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I could write the same story again. But that would be boring. 😹&lt;/p&gt;
&lt;p&gt;A lot has happened since then.&lt;/p&gt;
&lt;p&gt;The library gained &lt;strong&gt;new APIs&lt;/strong&gt;, some existing APIs &lt;strong&gt;became stricter&lt;/strong&gt;, I started &lt;strong&gt;using it in more of my own OSS projects&lt;/strong&gt;, and &lt;strong&gt;one real-world use case even exposed a type inference edge case in&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Effect-TS/effect&quot;&gt;&lt;code&gt;Effect&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So this time, instead of explaining the production adoption again, I want to look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What changed between 50 and 100 stars&lt;/li&gt;
&lt;li&gt;Some of the most important APIs added since then&lt;/li&gt;
&lt;li&gt;How I am actually using &lt;code&gt;is-kit&lt;/code&gt; outside the original production application&lt;/li&gt;
&lt;li&gt;A real bug report involving &lt;code&gt;Effect.catchIf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;What 100 stars means to me now&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's dive in! 🔧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/daotzurpsdocaj5u57pf.png&quot; alt=&quot;is-kit 100 stars&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-first-what-did-50-stars-mean-&quot;&gt;⭐ First, What Did 50 Stars Mean?&lt;/h3&gt;
&lt;p&gt;At 50 stars, the biggest thing I wanted to show was that &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; was not only a toy project.&lt;/p&gt;
&lt;p&gt;It was already being used in a real production application. 🚀&lt;/p&gt;
&lt;p&gt;The application had repeated runtime checks like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;typeof value === &quot;string&quot;;
typeof value === &quot;number&quot;;
value === null || value === undefined;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and several larger user-defined type guards for things like HTTP errors, literal unions, arrays, API values, and plain objects.&lt;/p&gt;
&lt;p&gt;The interesting part was &lt;strong&gt;not&lt;/strong&gt; that &lt;code&gt;is-kit&lt;/code&gt; made those checks shorter.&lt;br&gt;
The interesting part was that it gave them one &lt;strong&gt;reusable shape&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Small guards could be composed.&lt;/p&gt;
&lt;p&gt;Application-owned helpers could sit between the library and feature code. And the result could still participate in normal TypeScript control flow.&lt;/p&gt;
&lt;p&gt;That part has not changed.&lt;/p&gt;
&lt;p&gt;But since 50 stars, the library itself has moved quite a bit. 👀&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-property-refinement-became-a-first-class-use-case&quot;&gt;🔑 Property Refinement Became a First-Class Use Case&lt;/h3&gt;
&lt;p&gt;One thing I kept running into was this.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I already have a good type guard.&lt;br&gt;
I just want to apply it to one property.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;declare const input: unknown;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Maybe I already know how to validate a string&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString } from &quot;is-kit&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But now I want to express&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;This object has a &lt;code&gt;name&lt;/code&gt; property, and that property is a string.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds simple, but there are actually a few different cases.&lt;/p&gt;
&lt;p&gt;A required property is &lt;strong&gt;not the same as an optional property&lt;/strong&gt;.&lt;br&gt;
An array index is &lt;strong&gt;not the same as an object property&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And an optional property that becomes known to be defined should &lt;strong&gt;narrow differently from one that is still optional&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;is-kit&lt;/code&gt; added three property refinement helpers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;refineKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refineDefinedKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refineIndex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The idea is simple&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Take a type guard you already trust and lift it onto a property or array element.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString, refineKey } from &quot;is-kit&quot;;

const hasStringName = refineKey(&quot;name&quot;, isString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the string guard is reusable at a deeper level instead of being rewritten inside another custom predicate.&lt;/p&gt;
&lt;p&gt;This is a small API, but I like it because it follows the same design goal as the rest of &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Do not build one giant validator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build small checks that can be reused in normal TypeScript code&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-then-discriminatedunion-got-much-more-serious&quot;&gt;🔍 Then &lt;code&gt;discriminatedUnion&lt;/code&gt; Got Much More Serious&lt;/h3&gt;
&lt;p&gt;The biggest addition since the 50-star article is probably the exhaustive discriminated union API introduced in &lt;code&gt;v1.15.0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Consider a common TypeScript type&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Result = { ok: true; value: string } | { ok: false; error: string };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At runtime, validating this is not difficult.&lt;/p&gt;
&lt;p&gt;You can check &lt;code&gt;ok&lt;/code&gt;, then validate the corresponding fields.&lt;/p&gt;
&lt;p&gt;But there is another question&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can TypeScript verify that the validator itself covers the whole union?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is where &lt;code&gt;discriminatedUnion&lt;/code&gt; comes in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { discriminatedUnion, isString, oneOfValues, typedStruct } from &quot;is-kit&quot;;

type Result = { ok: true; value: string } | { ok: false; error: string };

const resultUnion = discriminatedUnion&amp;lt;Result&amp;gt;();

export const isResult = resultUnion(&quot;ok&quot;, [true, false], {
  true: typedStruct&amp;lt;Extract&amp;lt;Result, { ok: true }&amp;gt;&amp;gt;()({
    ok: oneOfValues(true),
    value: isString,
  }),
  false: typedStruct&amp;lt;Extract&amp;lt;Result, { ok: false }&amp;gt;&amp;gt;()({
    ok: oneOfValues(false),
    error: isString,
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runtime behavior is useful.&lt;/p&gt;
&lt;p&gt;But the more interesting part is the compile-time contract.&lt;/p&gt;
&lt;p&gt;The builder checks things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing discriminant branches&lt;/li&gt;
&lt;li&gt;Extra branches&lt;/li&gt;
&lt;li&gt;Incompatible branch guards&lt;/li&gt;
&lt;li&gt;Incomplete discriminant tuples&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So if the union changes, the guard cannot silently stay incomplete.&lt;/p&gt;
&lt;p&gt;That matters because one of the easiest ways for a hand-written type guard to become dangerous is drift.&lt;/p&gt;
&lt;p&gt;The TypeScript type evolves.&lt;br&gt;
The runtime guard does not.&lt;br&gt;
And the compiler cannot normally tell you that the two are no longer aligned.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;discriminatedUnion&lt;/code&gt; is one attempt to make that class of mistake harder.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-library-also-became-stricter&quot;&gt;💪 The Library Also Became Stricter&lt;/h3&gt;
&lt;p&gt;Not every improvement between 50 and 100 stars was a new feature.&lt;/p&gt;
&lt;p&gt;A lot of work was about making existing behavior more precise.&lt;/p&gt;
&lt;p&gt;For example, recent releases included fixes around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;recordOf&lt;/code&gt; literal-key narrowing&lt;/li&gt;
&lt;li&gt;native &lt;code&gt;Error&lt;/code&gt; brand checks&lt;/li&gt;
&lt;li&gt;array guards with overridden iterators&lt;/li&gt;
&lt;li&gt;exact &lt;code&gt;struct&lt;/code&gt; validation&lt;/li&gt;
&lt;li&gt;symbol-keyed struct schemas&lt;/li&gt;
&lt;li&gt;cross-realm struct schemas&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There was also more package-level testing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; now checks published-package behavior from both ESM and CommonJS TypeScript consumers, and also has &lt;code&gt;Deno&lt;/code&gt; / &lt;code&gt;JSR&lt;/code&gt; smoke coverage.&lt;/p&gt;
&lt;p&gt;This part is less exciting to show in a code snippet.&lt;/p&gt;
&lt;p&gt;But for a small library, I think it is important.&lt;/p&gt;
&lt;p&gt;Making sure they behave correctly in weird JavaScript and TypeScript environments is where things become real.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-i-started-using-is-kit-in-other-oss-too&quot;&gt;🤖 I Started Using &lt;code&gt;is-kit&lt;/code&gt; in Other OSS Too&lt;/h3&gt;
&lt;p&gt;Since reaching 50 stars, I've also started using &lt;code&gt;is-kit&lt;/code&gt; more naturally in other OSS projects I maintain.&lt;/p&gt;
&lt;p&gt;For example, in my changelog tooling of &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/changelog-bot&quot;&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/a&gt;, I use &lt;code&gt;define&lt;/code&gt; to keep small predicates reusable and composable instead of turning every runtime check into an isolated helper.&lt;/p&gt;
&lt;p&gt;A simplified example looks like this 👇&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { define, oneOfValues } from &quot;is-kit&quot;;

const CHANGELOG_SECTIONS = [&quot;Added&quot;, &quot;Changed&quot;, &quot;Fixed&quot;, &quot;Removed&quot;] as const;

const isChangelogSection = define&amp;lt;(typeof CHANGELOG_SECTIONS)[number]&amp;gt;(
  oneOfValues(CHANGELOG_SECTIONS),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact code is not the important part.&lt;/p&gt;
&lt;p&gt;What changed for me is that I no longer think&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I should use my library here.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I just reach for the guard I need.&lt;/p&gt;
&lt;p&gt;A library starts becoming useful to its maintainer when it stops feeling like a demo of itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-then-an-is-kit-guard-ran-into-an-effect-type-bug&quot;&gt;🚀 Then an &lt;code&gt;is-kit&lt;/code&gt; Guard Ran Into an Effect Type Bug&lt;/h3&gt;
&lt;p&gt;This was one of the most interesting things that happened recently.&lt;/p&gt;
&lt;p&gt;An &lt;code&gt;is-kit&lt;/code&gt; user was using a type guard with Effect's &lt;code&gt;catchIf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Conceptually, the refinement looked like this 👇&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;(error: unknown): error is SomeError =&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is completely normal for &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In fact, &lt;code&gt;unknown&lt;/code&gt; is exactly where the library is designed to start.&lt;/p&gt;
&lt;p&gt;But with &lt;code&gt;Effect.catchIf&lt;/code&gt;, that refinement could affect generic inference in an unexpected way.&lt;/p&gt;
&lt;p&gt;The error channel could become widened to &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The reported case resulted in this Effect PR 👏 And merged 🚀&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Effect-TS/effect/pull/9252&quot;&gt;https://github.com/Effect-TS/effect/pull/9252&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The relevant type originally looked roughly like this&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;refinement: Predicate.Refinement&amp;lt;E, EB&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the fix changed it to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;refinement: Predicate.Refinement&amp;lt;NoInfer&amp;lt;E&amp;gt;, EB&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important idea is that the &lt;strong&gt;refinement's input type should not be allowed to redefine the error type already established by the &lt;code&gt;Effect&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The PR also adds type tests for cases like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const result = Effect.catchIf(
  mixedEffect,
  (e: unknown): e is AiError =&amp;gt; e instanceof AiError,
  () =&amp;gt; Effect.succeed(&quot;ok&quot;),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the fix, the refinement input could participate in inference and widen the error channel.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;NoInfer&lt;/code&gt;, the original error type remains the source of truth.&lt;/p&gt;
&lt;p&gt;The same pattern was fixed for &lt;code&gt;Channel&lt;/code&gt; and &lt;code&gt;Stream&lt;/code&gt; too.&lt;/p&gt;
&lt;p&gt;There was also a &lt;code&gt;Bluesky&lt;/code&gt; post about the issue&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bsky.app/profile/evadeva.bsky.social/post/3mxwiqfeo3s2j&quot;&gt;https://bsky.app/profile/evadeva.bsky.social/post/3mxwiqfeo3s2j&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I really liked this bug because &lt;strong&gt;it came from an interaction I would probably never have discovered by testing &lt;code&gt;is-kit&lt;/code&gt; alone&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; did not &quot;break Effect&quot;. And &lt;code&gt;Effect&lt;/code&gt; was not doing something absurd either.&lt;/p&gt;
&lt;p&gt;It was a subtle TypeScript inference interaction between a generic API and a refinement whose input was intentionally broad.&lt;/p&gt;
&lt;p&gt;That is exactly the kind of edge case that appears when libraries start meeting each other in real code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-this-is-the-kind-of-feedback-i-wanted&quot;&gt;👍 This Is the Kind of Feedback I Wanted&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;is-kit&lt;/code&gt; reached 50 stars, I wrote&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Someone found this idea useful.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That was already enough to make me happy.&lt;/p&gt;
&lt;p&gt;100 stars still does not mean the library is huge.&lt;/p&gt;
&lt;p&gt;A star is still not a benchmark.&lt;br&gt;
It is still not proof of correctness.&lt;br&gt;
And it definitely does not mean every TypeScript project should install &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But something has changed.&lt;/p&gt;
&lt;p&gt;People are not only starring the repository anymore.&lt;/p&gt;
&lt;p&gt;They are&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using the library&lt;/li&gt;
&lt;li&gt;Asking questions&lt;/li&gt;
&lt;li&gt;Finding edge cases&lt;/li&gt;
&lt;li&gt;Reporting bugs&lt;/li&gt;
&lt;li&gt;Comparing it with other validation approaches&lt;/li&gt;
&lt;li&gt;Using its guards with other TypeScript libraries&lt;/li&gt;
&lt;li&gt;Making me reconsider APIs I thought were already finished&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is much more valuable than the number itself. 💖&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-project-is-also-becoming-easier-to-explain&quot;&gt;💬 The Project Is Also Becoming Easier to Explain&lt;/h3&gt;
&lt;p&gt;Another thing changed between 50 and 100 stars.&lt;/p&gt;
&lt;p&gt;I became much clearer about what &lt;code&gt;is-kit&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; trying to replace every schema library.&lt;br&gt;
It is &lt;strong&gt;not&lt;/strong&gt; trying to turn every runtime value into a schema-first workflow.&lt;br&gt;
And it is &lt;strong&gt;not&lt;/strong&gt; trying to hide normal TypeScript control flow.&lt;/p&gt;
&lt;p&gt;The core question is still very small&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Can this &lt;code&gt;unknown&lt;/code&gt; value safely enter this branch?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sometimes a schema library is the right answer.&lt;/p&gt;
&lt;p&gt;For example, if I need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rich validation errors&lt;/li&gt;
&lt;li&gt;Transformations&lt;/li&gt;
&lt;li&gt;Form validation&lt;/li&gt;
&lt;li&gt;A contract shared across systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I may want &lt;code&gt;Zod&lt;/code&gt;, &lt;code&gt;Valibot&lt;/code&gt;, &lt;code&gt;ArkType&lt;/code&gt;, or something else.&lt;/p&gt;
&lt;p&gt;But sometimes I already have normal TypeScript code and I only need a reusable narrowing function.&lt;/p&gt;
&lt;p&gt;That is where I want &lt;code&gt;is-kit&lt;/code&gt; to be useful.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (isUser(value)) {
  value.id;
}

if (isHttpClientError(error)) {
  // Handle HTTP client errors
}

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Small runtime decisions.&lt;br&gt;
Normal functions.&lt;br&gt;
Normal TypeScript.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-100-stars-means-to-me&quot;&gt;🐱 What 100 Stars Means to Me&lt;/h3&gt;
&lt;p&gt;100 stars is still small compared with the largest TypeScript OSS projects.&lt;/p&gt;
&lt;p&gt;No problem.&lt;/p&gt;
&lt;p&gt;What matters to me is that the project is becoming more real in ways that have nothing to do with the star counter.&lt;/p&gt;
&lt;p&gt;Since the 50-star milestone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New APIs were added&lt;/li&gt;
&lt;li&gt;Existing APIs became stricter&lt;/li&gt;
&lt;li&gt;Runtime edge cases were fixed&lt;/li&gt;
&lt;li&gt;Package compatibility testing improved&lt;/li&gt;
&lt;li&gt;I started using &lt;code&gt;is-kit&lt;/code&gt; naturally in other OSS&lt;/li&gt;
&lt;li&gt;External usage produced feedback I could not generate alone&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;unknown&lt;/code&gt;-based refinement even helped expose an inference edge case in Effect&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last part is probably my favorite. &lt;/p&gt;
&lt;p&gt;Because the goal was never to build a library that only looks nice in its own &lt;code&gt;README&lt;/code&gt;!!!&lt;/p&gt;
&lt;p&gt;The goal is &lt;strong&gt;to make reusable runtime narrowing work naturally inside real TypeScript code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And now I am starting to see that happen outside my own repository too.&lt;/p&gt;
&lt;p&gt;Thank you to everyone who starred the project, tried it, reported something, asked a question, opened an issue, or simply read the code. 🙏&lt;/p&gt;
&lt;p&gt;There is still a lot I want to improve.&lt;/p&gt;
&lt;p&gt;But 100 feels pretty nice. 😸⭐&lt;/p&gt;
&lt;p&gt;If small composable type guards fit your TypeScript style, give it a try!&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;https://github.com/nyaomaru/is-kit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for reading! 🤲&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32180/is-kit-reached-100-stars-heres-what-changed-since-50</guid>
<pubDate>Fri, 23 Oct 2026 12:24:38 +0000</pubDate>
</item>
<item>
<title>Demystifying MongoDB Write Concern and Read Concern in Node.js</title>
<link>https://coderlegion.com/31698/demystifying-mongodb-write-concern-and-read-concern-in-node-js</link>
<description>&lt;h3 id=&quot;what-are-write-concern-and-read-concern-&quot;&gt;What Are Write Concern and Read Concern?&lt;/h3&gt;
&lt;p&gt;In MongoDB, &lt;strong&gt;write concern&lt;/strong&gt; and &lt;strong&gt;read concern&lt;/strong&gt; are configuration settings that specify the level of guarantee you require when saving or retrieving data across a distributed cluster. A write concern determines how many database nodes must successfully record an update—and whether that data must be written to disk—before MongoDB reports success to your application. A read concern determines how fresh and consistent the data returned by a query must be, protecting your application from reading temporary data that might later be reversed.&lt;/p&gt;
&lt;p&gt;Together, these two settings let developers choose where their application sits on the classic spectrum between maximum execution speed and rock-solid data safety.&lt;/p&gt;
&lt;h3 id=&quot;the-real-world-analogy-certified-mail-vs-kitchen-whiteboards&quot;&gt;The Real-World Analogy: Certified Mail vs. Kitchen Whiteboards&lt;/h3&gt;
&lt;p&gt;Imagine you run a bustling catering kitchen with multiple head chefs.&lt;/p&gt;
&lt;p&gt;When taking an order, you have two choices for your &lt;strong&gt;write concern&lt;/strong&gt;. You could scribble the customer's allergy request on a loose scrap of paper and immediately yell &quot;Order placed!&quot; (a fast write, but if you drop the paper, the order is lost). Alternatively, you could walk the order ticket over to the main order board, watch two sous-chefs sign off on it, and wait for them to file it in the metal lockbox before confirming the order with the customer. The second route takes several seconds longer, but you are guaranteed that the kitchen will never forget the order even if one chef walks out.&lt;/p&gt;
&lt;p&gt;Now consider the &lt;strong&gt;read concern&lt;/strong&gt;. If a waiter needs to know the soup of the day, they can either glance at a whiteboard where someone started drafting ideas with an erasable marker, or they can refuse to serve anything until they see the official menu stamped and approved by the head kitchen manager. Glancing at the draft whiteboard is instantaneous, but you might serve an experimental soup that the manager ultimately rejects five minutes later.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-software-engineering&quot;&gt;Why It Matters in Daily Software Engineering&lt;/h3&gt;
&lt;p&gt;Modern web architectures rarely run on a single database server. Instead, they use replica sets—groups of connected servers where one &quot;primary&quot; node handles writes and multiple &quot;secondary&quot; nodes replicate that data for redundancy. &lt;/p&gt;
&lt;p&gt;If you leave MongoDB on default settings during high-traffic moments, subtle race conditions emerge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rollback Catastrophes:&lt;/strong&gt; If an API endpoint confirms an e-commerce order after writing only to the primary node, and that primary node suddenly experiences a hardware failure before syncing to secondary nodes, the cluster elects a new primary. Your customer's order is wiped out, yet your payment processor already charged their credit card.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dirty Reads:&lt;/strong&gt; A background worker updates a bank balance. A microservice queries the secondary replica before the primary finishes replicating. If that transaction aborts or fails over, the microservice has operated on data that never officially existed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By tuning write and read concerns, software engineers intentionally shield critical operations (payments, account transfers, credential updates) from data loss while letting low-risk operations (logging, view counters, user analytics) run with blazing speed.&lt;/p&gt;
&lt;h3 id=&quot;implementation-in-node-js&quot;&gt;Implementation in Node.js&lt;/h3&gt;
&lt;p&gt;Here is how you apply write and read concerns when working with the official MongoDB Node.js driver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { MongoClient } from 'mongodb';

const client = new MongoClient('mongodb://localhost:27017,localhost:27018/?replicaSet=rs0');

async function processPayment(orderId, paymentDetails) {
  await client.connect();
  const db = client.db('store');

  // Configure high-durability options for financial transactions
  const ordersCollection = db.collection('orders', {
    writeConcern: {
      w: 'majority',      // Must be acknowledged by more than half of the replica set nodes
      j: true,            // Must be written to the on-disk journal before confirming
      wtimeout: 5000      // Abort if replicas do not respond within 5 seconds
    },
    readConcern: {
      level: 'majority'   // Only read data committed by a majority of nodes
    }
  });

  // Step 1: Safely write with acknowledgment
  const writeResult = await ordersCollection.updateOne(
    { _id: orderId },
    { $set: { status: 'PAID', payment: paymentDetails } }
  );

  // Step 2: Read guaranteed, durable data
  const confirmedOrder = await ordersCollection.findOne({ _id: orderId });

  console.log('Payment processed safely:', confirmedOrder._id);
  return confirmedOrder;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, specifying &lt;code&gt;w: 'majority'&lt;/code&gt; and &lt;code&gt;j: true&lt;/code&gt; ensures that the write cannot be rolled back if the primary server abruptly crashes. Setting &lt;code&gt;level: 'majority'&lt;/code&gt; on the read ensures your application reads only finalized records.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Database speed and data reliability exist on a continuous slider: write concern dictates how hard your cluster works to preserve your updates, while read concern dictates how strictly your cluster verifies the stability of what you fetch. Mastering both allows you to protect financial transactions and vital data without unnecessarily throttling high-volume, low-risk endpoints across your stack.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/how-mongodb-write-concern-and-read.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31698/demystifying-mongodb-write-concern-and-read-concern-in-node-js</guid>
<pubDate>Mon, 19 Oct 2026 09:22:10 +0000</pubDate>
</item>
<item>
<title>A very Simple Rate Limiting App ( Node.JS )</title>
<link>https://coderlegion.com/31204/a-very-simple-rate-limiting-app-node-js</link>
<description>&lt;pre&gt;&lt;code&gt;enter code here

const express = require(&quot;express&quot;);

const app = express();

const requests = new Map();

const LIMIT = 5;
const WINDOW = 60 * 1000; // 1 minute

app.use((req, res, next) =&amp;gt; {
  const ip = req.ip;
  const now = Date.now();

  let data = requests.get(ip);

  // First request or expired time window
  if (!data || now - data.startTime &amp;gt;= WINDOW) {
    data = {
      count: 1,
      startTime: now,
    };

    requests.set(ip, data);
    return next();
  }

  // Rate limit exceeded
  if (data.count &amp;gt;= LIMIT) {
    return res.status(429).json({
      message: &quot;Too many requests. Try again later.&quot;,
    });
  }

  data.count++;
  next();
});

app.get(&quot;/&quot;, (req, res) =&amp;gt; {
  res.json({ message: &quot;Request successful!&quot; });
});

app.listen(3000, () =&amp;gt; {
  console.log(&quot;Server running on http://localhost:3000&quot;);
});

&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31204/a-very-simple-rate-limiting-app-node-js</guid>
<pubDate>Thu, 15 Oct 2026 13:40:19 +0000</pubDate>
</item>
<item>
<title>The Reconnect Bug Where Marking a User Online Erased the Data I Needed for Replay</title>
<link>https://coderlegion.com/31164/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay</link>
<description>&lt;p&gt;While testing a reconnect system for a deliverer-facing WebSocket gateway: when someone reconnects after losing their connection, they should get a snapshot of their current job plus any events they missed while offline. Two pieces, both of which I was confident were correct on their own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Marking presence:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async markOnline(userId: number) {
  await this.prisma.user.update({
    where: { id: userId },
    data: { isOnline: true, lastSeenAt: new Date() },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Computing what was missed:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async replayDelivererEvents(delivererId: number) {
  const deliverer = await this.prisma.delivererProfile.findUnique({
    where: { id: delivererId },
    select: { user: { select: { lastSeenAt: true } }, activeJobId: true },
  });
  // ...fetch events since deliverer.user.lastSeenAt
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;And the connection handler that used both:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async handleConnection(client: Socket) {
  const deliverer = await this.getDelivererByUserId(payload.sub);

  await this.sessionService.markOnline(payload.sub);           // step 1
  const missedEvents = await this.replayService.replayForDeliverer(deliverer.id); // step 2

  client.emit('events.replay', missedEvents);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tested it: disconnect, trigger a job assignment while offline, reconnect. &lt;strong&gt;events.replay&lt;/strong&gt; came back empty, every time. The event genuinely happened. The query genuinely should have found it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Walk through the actual timeline. A deliverer disconnects — markOffline correctly stamps lastSeenAt with the disconnect time. Later, they reconnect. handleConnection fires, and the first thing it does is call markOnline, which immediately overwrites lastSeenAt to right now. Only after that does it call replayForDeliverer, which reads lastSeenAt to compute &quot;what happened since they were last seen&quot;  except by then lastSeenAt no longer holds the disconnect time. It holds the current moment, because the line above just wrote over it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;getEventsSince({ since: lastSeenAt })&lt;/strong&gt; effectively became since: right now. The replay feature couldn't fail more quietly — no error, no exception, just a correctly-shaped empty array every single time.&lt;/p&gt;
&lt;p&gt;What makes this bug worth writing about isn't the fix — it's that neither piece of code was wrong in isolation. Marking someone online should update lastSeenAt. Replay should read lastSeenAt. The bug only exists in the order they ran in, and that kind of bug doesn't show up by staring at either function — only by tracing the actual sequence of operations across the whole request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Read before you overwrite:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async handleConnection(client: Socket) {
  const deliverer = await this.getDelivererByUserId(payload.sub);

  // Replay FIRST, using the lastSeenAt from before this connection
  const missedEvents = await this.replayService.replayForDeliverer(deliverer.id);

  // THEN mark online, which overwrites lastSeenAt for next time
  await this.sessionService.markOnline(payload.sub);

  client.emit('events.replay', missedEvents);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No schema change needed. Reordering two already-correct operations was the entire fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One field, two different jobs — &quot;when did we last hear a heartbeat&quot; and &quot;what's the anchor point for replay&quot; happened to share the same column, and that's exactly what made the collision possible. The one-line reorder fixes today's bug, but it leaves the deeper coupling in place: anything else that later wants to read lastSeenAt for a different purpose can walk into the same trap. The more robust version splits them. a dedicated &lt;strong&gt;lastDisconnectedAt&lt;/strong&gt;, set only on disconnect, read only by replay so two unrelated concerns stop sharing state that one of them mutates as a side effect. I haven't built that yet. It's on the list specifically because I know why it would help, not just that it might.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31164/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay</guid>
<pubDate>Thu, 15 Oct 2026 07:00:41 +0000</pubDate>
</item>
<item>
<title>Keeping a Legacy Cache Consistent During an Incremental Migration</title>
<link>https://coderlegion.com/31009/keeping-a-legacy-cache-consistent-during-an-incremental-migration</link>
<description>&lt;p&gt;During an incremental system migration, I ran into a subtle consistency problem: our new service could successfully update the database while the legacy service continued reading stale data from its own in-memory cache.&lt;/p&gt;
&lt;p&gt;The obvious solutions such as distributed locks, cache invalidation, or adding another piece of infrastructure didn't fit our constraints.&lt;/p&gt;
&lt;p&gt;The solution ended up being much simpler: &lt;strong&gt;the legacy system already had a lease mechanism that we could reuse.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the story of how I found it, how we used it, and how we handled the failure cases.&lt;/p&gt;
&lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Our system currently has three main components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Main Service:&lt;/strong&gt; The client-facing service that sends business requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy Service:&lt;/strong&gt; An older, high-performance engine that maintains an embedded in-memory cache. It loads a subset of database tables into its own memory for fast access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Service:&lt;/strong&gt; A modern service that is gradually replacing the Legacy Service using the Strangler Fig pattern.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The New Service currently acts as a proxy between the Main Service and the Legacy Service.&lt;/p&gt;
&lt;p&gt;If an endpoint has already been migrated, the New Service handles the request directly. Otherwise, it forwards the request to the Legacy Service.&lt;/p&gt;
&lt;p&gt;The architecture looks roughly like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/04m0jg5bf6d3qlo9ermn.png&quot; alt=&quot;System architecture diagram&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;The problem appeared when the New Service handled a migrated request.&lt;/p&gt;
&lt;p&gt;The New Service writes updates directly to the database. However, the Legacy Service has its own in-memory copy of the data and has no way of knowing that the database changed.&lt;/p&gt;
&lt;p&gt;That creates a dangerous sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Legacy Service caches &lt;code&gt;status = pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A request is migrated to the New Service.&lt;/li&gt;
&lt;li&gt;The New Service updates the database to &lt;code&gt;status = completed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A later request reaches the Legacy Service.&lt;/li&gt;
&lt;li&gt;The Legacy Service reads &lt;code&gt;status = pending&lt;/code&gt; from its local cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The database is correct, but the Legacy Service is operating on stale state.&lt;/p&gt;
&lt;p&gt;We discovered these issues during QA testing. As more endpoints moved to the New Service, the problem would become increasingly common.&lt;/p&gt;
&lt;p&gt;So we needed a way to guarantee that the Legacy Service could not continue operating on stale data while a migrated endpoint was modifying the same resource.&lt;/p&gt;
&lt;h3 id=&quot;the-constraints&quot;&gt;The Constraints&lt;/h3&gt;
&lt;p&gt;The textbook solutions weren't a good fit for this system.&lt;/p&gt;
&lt;p&gt;We had several constraints:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Legacy and Main Services should not be modified.&lt;/strong&gt; The Legacy Service is largely a black box, so modifying its behavior is risky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Legacy Service could not temporarily operate on stale data.&lt;/strong&gt; For these operations, we needed to prevent conflicting legacy operations rather than accept eventual consistency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Legacy Service had limited cache synchronization capabilities.&lt;/strong&gt; It automatically reloaded its cache only periodically, or through a manual refresh operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We wanted to avoid introducing new infrastructure.&lt;/strong&gt; Adding something like Redis purely for distributed locking would introduce another dependency to provision, operate, and maintain.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These constraints ruled out several conventional approaches.&lt;/p&gt;
&lt;h3 id=&quot;the-naive-solution&quot;&gt;The Naive Solution&lt;/h3&gt;
&lt;p&gt;My first thought was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What if the New Service acquires a distributed lock, performs the database update, and then triggers a cache refresh on the Legacy Service?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds reasonable, but there were two problems.&lt;/p&gt;
&lt;h4 id=&quot;1-the-legacy-service-wouldn-t-know-about-the-lock&quot;&gt;1. The Legacy Service wouldn't know about the lock&lt;/h4&gt;
&lt;p&gt;An external distributed lock only coordinates operations if the participating systems can use it.&lt;/p&gt;
&lt;p&gt;The Legacy Service was a black box and wouldn't check our new lock before accessing its local cache.&lt;/p&gt;
&lt;p&gt;We could add that behavior, but that violated one of our main constraints.&lt;/p&gt;
&lt;h4 id=&quot;2-refreshing-the-entire-cache-was-too-expensive&quot;&gt;2. Refreshing the entire cache was too expensive&lt;/h4&gt;
&lt;p&gt;The Legacy Service's manual refresh operation reloaded a large amount of data.&lt;/p&gt;
&lt;p&gt;Doing that after every migrated request would introduce significant latency and unnecessary work.&lt;/p&gt;
&lt;p&gt;We needed something more targeted.&lt;/p&gt;
&lt;h3 id=&quot;the-solution-reusing-an-existing-lease&quot;&gt;The Solution: Reusing an Existing Lease&lt;/h3&gt;
&lt;p&gt;While investigating the Legacy Service's existing APIs, I found something useful: it already had a built-in &lt;strong&gt;lease mechanism&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The Legacy Service exposed APIs to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Acquire a lease for a specific resource.&lt;/li&gt;
&lt;li&gt;Release the lease when the operation was complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;More importantly, releasing the lease had an existing side effect: &lt;strong&gt;the Legacy Service refreshed the cached data associated with that resource.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The lease mechanism was already part of how the Legacy Service protected its own critical sections.&lt;/p&gt;
&lt;p&gt;This changed the problem completely.&lt;/p&gt;
&lt;p&gt;Instead of introducing a new distributed lock, we could use the same coordination mechanism the Legacy Service already understood.&lt;/p&gt;
&lt;p&gt;The New Service could wrap a migrated operation like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Acquire lease for the resource
2. Update the database
3. Perform business logic
4. Release the lease
5. Legacy Service refreshes that resource's cached data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is that the lease wasn't merely an arbitrary lock exposed by the Legacy Service. The Legacy Service's own operations already align with the same lease mechanism.&lt;/p&gt;
&lt;p&gt;That gave us two properties we needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Conflicting Legacy Service operations could not proceed while the resource was leased.&lt;/li&gt;
&lt;li&gt;Releasing the lease caused the Legacy Service to refresh the affected resource rather than performing a full cache reload.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of adding Redis, Kafka, or another synchronization system, I reused a primitive that already existed in the system.&lt;/p&gt;
&lt;h3 id=&quot;what-if-releasing-the-lease-fails-&quot;&gt;What If Releasing the Lease Fails?&lt;/h3&gt;
&lt;p&gt;The happy path was straightforward.&lt;/p&gt;
&lt;p&gt;But distributed systems rarely fail only on the happy path.&lt;/p&gt;
&lt;p&gt;Consider this sequence:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;New Service
    |
    | acquire lease
    v
Legacy Service
    |
    | lease acquired
    |
    v
New Service
    |
    | update database
    |
    X---- network failure ----&amp;gt; release request never arrives
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database update succeeded, but the release request never reached the Legacy Service.&lt;/p&gt;
&lt;p&gt;If leases could remain indefinitely, the resource could become permanently locked.&lt;/p&gt;
&lt;p&gt;So I looked at how the Legacy Service handled abandoned leases.&lt;/p&gt;
&lt;p&gt;It already had two mechanisms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TTL (Time-To-Live):&lt;/strong&gt; Every lease automatically expires after a configured duration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background reaper:&lt;/strong&gt; A background process periodically detects expired leases and cleans them up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better, lease cleanup also triggered the same cache refresh behavior.&lt;/p&gt;
&lt;p&gt;That meant the failure path became:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Acquire lease
     |
     v
Update database
     |
     X  release request lost
     |
     v
Lease expires
     |
     v
Background reaper
     |
     v
Lease cleared + cache refreshed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We therefore didn't need to build a separate retry system just to recover from a lost release request.&lt;/p&gt;
&lt;p&gt;The existing lease lifecycle already provided a recovery mechanism.&lt;/p&gt;
&lt;h4 id=&quot;an-important-invariant&quot;&gt;An Important Invariant&lt;/h4&gt;
&lt;p&gt;The key correctness property here is that the resource remains protected by the lease until the Legacy Service releases or expires it.&lt;/p&gt;
&lt;p&gt;As long as the database update completes before the lease is released or expires, the Legacy Service cannot resume normal access to the resource using its old cached state during that protected window.&lt;/p&gt;
&lt;p&gt;When the lease is eventually cleaned up, the cache refresh brings the Legacy Service back in sync with the database.&lt;/p&gt;
&lt;p&gt;This is an important distinction: &lt;strong&gt;the TTL isn't what provides consistency by itself.&lt;/strong&gt; The consistency comes from the lease preventing conflicting access; the TTL and reaper provide failure recovery when the normal release path doesn't happen.&lt;/p&gt;
&lt;h3 id=&quot;the-tradeoff&quot;&gt;The Tradeoff&lt;/h3&gt;
&lt;p&gt;The solution wasn't free.&lt;/p&gt;
&lt;p&gt;The main cost was &lt;strong&gt;latency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If a resource has many related records that need to be refreshed, releasing the lease and synchronizing the cache can take noticeable time and I verified this through tests.&lt;/p&gt;
&lt;p&gt;So I didn't view this as a permanent architecture.&lt;/p&gt;
&lt;p&gt;It's a &lt;strong&gt;migration bridge&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;During the migration, migrated endpoints use the lease mechanism to safely coordinate with the Legacy Service.&lt;/p&gt;
&lt;p&gt;As more functionality moves to the New Service, individual resources eventually become fully owned by the New Service.&lt;/p&gt;
&lt;p&gt;At that point, the Legacy Service no longer needs to read or modify those resources.&lt;/p&gt;
&lt;p&gt;That's our exit condition.&lt;/p&gt;
&lt;p&gt;Once a resource is completely owned by the New Service, we can remove the lease and cache-synchronization logic for it.&lt;/p&gt;
&lt;p&gt;This is important because migration code has a tendency to become permanent if nobody defines when it can be deleted. So I documented this as well.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;h4 id=&quot;1-start-with-invariants-not-technologies&quot;&gt;1. Start with invariants, not technologies&lt;/h4&gt;
&lt;p&gt;My first instinct was to think about distributed locks and infrastructure such as Redis.&lt;/p&gt;
&lt;p&gt;But the more useful question was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What must never happen?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this case, the important invariant was that the Legacy Service must not perform conflicting operations against a resource while the New Service is modifying it.&lt;/p&gt;
&lt;p&gt;Once that was clear, the technology choice became much easier.&lt;/p&gt;
&lt;h4 id=&quot;2-existing-systems-often-contain-useful-primitives&quot;&gt;2. Existing systems often contain useful primitives&lt;/h4&gt;
&lt;p&gt;At first, I thought of the Legacy Service as something we couldn't modify and therefore mostly had to work around.&lt;/p&gt;
&lt;p&gt;But &quot;don't modify the legacy system&quot; doesn't mean &quot;don't understand the legacy system.&quot;&lt;/p&gt;
&lt;p&gt;By digging into its existing APIs and lease lifecycle, I found a mechanism that already provided most of the coordination and recovery behavior we needed.&lt;/p&gt;
&lt;p&gt;Sometimes the best solution isn't introducing another system. It's discovering what the current system can already do.&lt;/p&gt;
&lt;h4 id=&quot;3-temporary-migration-code-needs-an-exit-condition&quot;&gt;3. Temporary migration code needs an exit condition&lt;/h4&gt;
&lt;p&gt;A migration introduces a lot of code that ideally won't exist forever.&lt;/p&gt;
&lt;p&gt;The lease wrapper is useful while ownership is split between the Legacy and New Services. But once the New Service completely owns a resource, keeping the wrapper would only add unnecessary latency and complexity.&lt;/p&gt;
&lt;p&gt;Defining the exact conditions under which the lease logic can be removed makes it much less likely that temporary migration infrastructure becomes permanent technical debt.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This experience changed how I think about solving distributed systems problems.&lt;/p&gt;
&lt;p&gt;It's easy to reach for familiar tools like Redis for locking, Kafka for asynchronous communication, an outbox for reliable delivery.&lt;/p&gt;
&lt;p&gt;Sometimes those are exactly the right tools.&lt;/p&gt;
&lt;p&gt;But before introducing another component, it's worth understanding the guarantees and primitives that already exist in the system.&lt;/p&gt;
&lt;p&gt;In this case, the legacy system already had a resource-level lease, automatic expiration, a background reaper, and targeted cache synchronization.&lt;/p&gt;
&lt;p&gt;The challenge wasn't building a new distributed system.&lt;/p&gt;
&lt;p&gt;The best solution wasn't to build another system around the legacy one. It was to understand the system we already had well enough to make it work for us.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31009/keeping-a-legacy-cache-consistent-during-an-incremental-migration</guid>
<pubDate>Wed, 14 Oct 2026 04:04:39 +0000</pubDate>
</item>
<item>
<title>Calling a Service Inside prisma.$transaction Doesn't Make It Atomic — Here's the Bug That Taught Me</title>
<link>https://coderlegion.com/30791/calling-service-inside-prisma-transaction-doesnt-make-atomic-heres-the-bug-that-taught</link>
<description>&lt;p&gt;I was fixing a real bug in a delivery backend I'm building: a customer's order could get created in Postgres even when something downstream failed, leaving an order with no event history behind it. The fix looked simple. Wrap the whole thing in &lt;br&gt;
&lt;strong&gt;prisma.$transaction:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const order = await this.prisma.$transaction(async (tx) =&amp;gt; {
  const newOrder = await tx.order.create({ data: { ... } });

  await this.eventLogService.recordEvent({
    entityType: 'ORDER',
    entityId: newOrder.id,
    eventType: EVENTS.ORDER.CREATED,
    payload: { orderId: newOrder.id, status: OrderStatus.PENDING_VENDOR_CONFIRMATION },
  });

  return newOrder;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran it, broke the event-logging step on purpose to test the rollback, and the order still got committed. The transaction did nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$transaction(async (tx) =&amp;gt; {...})&lt;/strong&gt; opens one database connection and starts a transaction on it. The tx object you get inside the callback is bound to that specific connection. Every query run through tx participates in the same transaction.&lt;/p&gt;
&lt;p&gt;My &lt;strong&gt;EventLogService.recordEvent&lt;/strong&gt; method didn't use &lt;strong&gt;tx&lt;/strong&gt;. It used &lt;strong&gt;this.prisma&lt;/strong&gt; — its own injected Prisma client, a completely different connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async recordEvent(params) {
  const event = await this.prisma.eventLog.create({ ... }); // not tx — its own connection
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nesting the call inside &lt;strong&gt;$transaction&lt;/strong&gt; changes nothing here. Prisma has no way to know &quot;we're inside a transaction right now, please route all queries through it.&quot; The only thing that determines whether a query participates in a transaction is which client object actually executed it. Since &lt;strong&gt;recordEvent&lt;/strong&gt; ran on its own connection, its insert committed instantly and independently, with no awareness that anything else was &quot;inside&quot; a transaction around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
Every method that needs to be transaction-capable has to accept an optional transaction client and fall back to the default when none is passed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async recordEvent(params, tx?: Prisma.TransactionClient) {
  const client = tx ?? this.prisma;
  const event = await client.eventLog.create({ data: params });
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the caller passes its own &lt;strong&gt;tx&lt;/strong&gt; through explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;await this.eventLogService.recordEvent(params, tx);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;strong&gt;recordEvent's&lt;/strong&gt; insert runs on the same connection, inside the same transaction, as everything else. If anything later in that transaction throws, Postgres rolls back the whole connection's uncommitted work — including this insert — because it genuinely was part of the same unit of work, not just nested inside it syntactically.&lt;/p&gt;
&lt;p&gt;I retested the same failure scenario. This time, the order didn't get created either. Rollback worked as a single unit, order and event together.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;br&gt;
Code structure and transaction membership are two different things. A function call being lexically inside a &lt;strong&gt;$transaction&lt;/strong&gt; block tells you nothing about which database connection it actually used. If you're composing transactional logic across services in Prisma — or any ORM with a similar explicit-client model — every service in that chain needs to accept and thread through the transaction client deliberately. There's no implicit &quot;ambient transaction&quot; you can rely on.&lt;/p&gt;
&lt;p&gt;The honest cost: this makes service signatures a bit noisier — an extra optional parameter on every method that might need to participate in a caller's transaction. I think that's a fair trade for not silently losing atomicity on exactly the kind of multi-step write where you'd assume you had it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30791/calling-service-inside-prisma-transaction-doesnt-make-atomic-heres-the-bug-that-taught</guid>
<pubDate>Mon, 12 Oct 2026 09:22:43 +0000</pubDate>
</item>
<item>
<title>The Reliability Boundary: What AI Agents Need Before They Touch Production APIs</title>
<link>https://coderlegion.com/30693/the-reliability-boundary-what-ai-agents-need-before-they-touch-production-apis</link>
<description>&lt;p&gt;AI agents are moving from chat interfaces into real business workflows. They can search customer records, update accounts, trigger deployments, and initiate financial operations. That creates a practical engineering question: &lt;strong&gt;what should happen between an agent's decision and a production API call?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In a conventional backend, a request passes through authentication, authorization, validation, rate limits, transaction boundaries, observability, and error handling. Agents need those controls too. They can also misunderstand intent, follow malicious instructions embedded in retrieved content, retry an operation after a timeout, or select the wrong tool.&lt;/p&gt;
&lt;p&gt;The answer is a reliability boundary: a layer that treats the agent's output as a proposal and makes the final decision through deterministic policy and backend services.&lt;/p&gt;
&lt;h3 id=&quot;separate-reasoning-from-execution&quot;&gt;Separate reasoning from execution&lt;/h3&gt;
&lt;p&gt;An early architecture often looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User → AI agent → MCP tool → Production API
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That path assumes the agent can decide when and how a business action should happen. For sensitive operations, use a more explicit path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User request
    ↓
AI agent proposes an action
    ↓
MCP tool or tool gateway
    ↓
Policy, risk, and reliability boundary
    ↓
Validated command → Production API
    ↓
Audit events and reconciliation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The boundary checks identity, permissions, business rules, duplicate requests, state transitions, and the eventual outcome. This is familiar territory for payment and distributed systems: a syntactically valid request is only the start of the decision.&lt;/p&gt;
&lt;h3 id=&quot;why-an-api-gateway-is-not-enough&quot;&gt;Why an API gateway is not enough&lt;/h3&gt;
&lt;p&gt;An API gateway can authenticate a caller, route traffic, enforce rate limits, and record access. It usually cannot determine whether an agent misunderstood the user's instruction, whether a retrieved document contained prompt injection, whether a selected tool fits the task, or whether the requested change is consistent with the current business state.&lt;/p&gt;
&lt;p&gt;Those checks require application policy. The backend that owns the data or action must remain the final authority.&lt;/p&gt;
&lt;h3 id=&quot;a-production-lesson-from-banking-modernization&quot;&gt;A production lesson from banking modernization&lt;/h3&gt;
&lt;p&gt;At UST, I worked on a Capital One modernization effort as SAS-based operational and business reporting was being moved toward Salesforce. The people using those reports were service agents, not AI agents. My part of the work included designing Spring Boot GET APIs for fraud/security, Section 78, and exception reporting, integrating the services with Snowflake, and exposing the endpoints to the Salesforce team.&lt;/p&gt;
&lt;p&gt;The production question was broader than whether an endpoint returned the right JSON. A service agent had to see only the reports allowed by their role; the API had to apply that rule even when Salesforce was the caller. We worked with architects and product owners on the service design, used role-based access and business rules, and tested the contracts and behavior. JMeter and Gatling performance tests helped us check whether the services could meet SLA expectations under load. Docker, Kubernetes, and the DevOps workflow made deployment and operation part of the engineering responsibility, not an afterthought.&lt;/p&gt;
&lt;p&gt;This was a read-oriented integration, not an AI agent making autonomous changes. Still, it taught a useful boundary lesson: moving a capability from one platform to another does not transfer the authority to decide who may use it. If an AI assistant were later allowed to retrieve one of these reports, its tool call would need to carry the human user's identity and scope. The backend would still enforce role access, validate the request, and record the outcome. An AI tool is another caller of a business capability; it is not a substitute for the controls around that capability.&lt;/p&gt;
&lt;h3 id=&quot;a-refund-example&quot;&gt;A refund example&lt;/h3&gt;
&lt;p&gt;Imagine a customer tells an assistant, “Please refund my last order.” A basic agent may find the latest order and call a refund endpoint. A production workflow needs more answers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which customer is authenticated, and which order do they mean?&lt;/li&gt;
&lt;li&gt;Is the order eligible under the current policy?&lt;/li&gt;
&lt;li&gt;Has a refund already been requested or completed?&lt;/li&gt;
&lt;li&gt;Is the amount within the caller's authority?&lt;/li&gt;
&lt;li&gt;Does this amount or account require human approval?&lt;/li&gt;
&lt;li&gt;If a tool call times out, how will the system discover its actual state?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A language model can interpret the request and prepare a proposed command. It should not make these policy decisions on its own.&lt;/p&gt;
&lt;h3 id=&quot;six-controls-for-sensitive-agent-actions&quot;&gt;Six controls for sensitive agent actions&lt;/h3&gt;
&lt;h4 id=&quot;1-identity-and-narrow-authorization&quot;&gt;1. Identity and narrow authorization&lt;/h4&gt;
&lt;p&gt;Do not give an agent a broad administrative token. Resolve the authenticated user's identity and grant the tool only the capability required for this workflow. Reading an order and issuing a refund are different permissions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;orders.read
refunds.preview
refunds.request
refunds.approve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enforce authorization again in the backend. An MCP server is an integration layer; its presence does not remove the need for service-level authorization.&lt;/p&gt;
&lt;h4 id=&quot;2-structured-commands-and-validation&quot;&gt;2. Structured commands and validation&lt;/h4&gt;
&lt;p&gt;Do not pass unrestricted prose to a state-changing API. Define a typed command with required fields and constraints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public record RefundCommand(
    String customerId,
    String orderId,
    BigDecimal amount,
    String currency,
    String reason,
    String idempotencyKey
) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent may suggest initial arguments. The backend must check the authenticated identity, ownership, amount, currency, eligibility, and current state before accepting the command. Schema validation catches malformed inputs; business validation catches unsafe actions.&lt;/p&gt;
&lt;h4 id=&quot;3-idempotency-and-duplicate-protection&quot;&gt;3. Idempotency and duplicate protection&lt;/h4&gt;
&lt;p&gt;Agents retry after timeouts, uncertain tool results, and orchestration failures. A retry must not create a second refund. Assign a stable idempotency key to the &lt;em&gt;business operation&lt;/em&gt;, and persist the result associated with it. If the same operation is requested again, return the existing result.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;First request: refund-order-7842 → REFUND_ACCEPTED
Retry:         refund-order-7842 → same recorded result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key must remain stable across retries. Generating a fresh key on every attempt defeats duplicate protection.&lt;/p&gt;
&lt;h4 id=&quot;4-risk-based-human-approval&quot;&gt;4. Risk-based human approval&lt;/h4&gt;
&lt;p&gt;Approval should follow a policy, rather than being added after an incident. For example, reading an order may proceed automatically; a refund preview may be automatic after validation; a high-value refund or a change to bank details may require a person to review the exact payload.&lt;/p&gt;
&lt;p&gt;The agent can prepare the action and explain it. Policy decides what is allowed. The human approves actions above the risk threshold. The backend validates and executes the final command.&lt;/p&gt;
&lt;h4 id=&quot;5-auditability&quot;&gt;5. Auditability&lt;/h4&gt;
&lt;p&gt;A production workflow should leave more than a chat transcript. Record the authenticated actor, workflow and tool identifiers, input arguments, policy decision, approval, correlation ID, idempotency key, backend response, and final business outcome. Exclude secrets and unnecessary personal data from logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;eventType&quot;: &quot;REFUND_REQUESTED&quot;,
  &quot;workflowId&quot;: &quot;wf-8271&quot;,
  &quot;actor&quot;: &quot;user-442&quot;,
  &quot;resource&quot;: &quot;order-7842&quot;,
  &quot;policyDecision&quot;: &quot;APPROVED&quot;,
  &quot;idempotencyKey&quot;: &quot;refund-order-7842&quot;,
  &quot;correlationId&quot;: &quot;corr-91a7&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These records support incident investigation, customer disputes, and reconciliation.&lt;/p&gt;
&lt;h4 id=&quot;6-reconciliation&quot;&gt;6. Reconciliation&lt;/h4&gt;
&lt;p&gt;A timeout does not prove failure. The backend may have accepted the action while the agent missed the response. Before retrying, look up the operation by its stable identifier and ask the system of record for its actual state.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;What is the current state of refund-order-7842?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final business outcome should come from the system of record, not the model's interpretation of a partial tool response.&lt;/p&gt;
&lt;h3 id=&quot;where-mcp-fits&quot;&gt;Where MCP fits&lt;/h3&gt;
&lt;p&gt;Model Context Protocol (MCP) offers a standard interface for exposing tools and resources to AI applications. That can simplify integration, but it does not replace identity management, authorization, input validation, transaction handling, audit logging, approval, or reconciliation.&lt;/p&gt;
&lt;p&gt;Expose controlled business capabilities, such as &lt;code&gt;preview_refund(order_id, reason)&lt;/code&gt;. Avoid tools that give an agent unrestricted database queries or administrative operations. Tool descriptions help an agent choose an action; backend policy determines whether the action may run.&lt;/p&gt;
&lt;h3 id=&quot;reference-architecture&quot;&gt;Reference architecture&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;User request
   │
   ▼
AI agent ── proposes tool call ──► MCP tool server
                                      │
                                      ▼
                               Policy and risk engine
                                      │
                       ┌──────────────┴──────────────┐
                       ▼                             ▼
                Human approval                 Automatic path
                       └──────────────┬──────────────┘
                                      ▼
                          Validation + idempotency
                                      │
                                      ▼
                              Backend API
                                      │
                                      ▼
                          Audit + reconciliation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key component is the policy and reliability layer between the model and the business operation. It keeps the model from becoming the final authority over sensitive actions.&lt;/p&gt;
&lt;h3 id=&quot;what-teams-should-measure&quot;&gt;What teams should measure&lt;/h3&gt;
&lt;p&gt;Response accuracy is only one measure of agent quality. Also track invalid tool calls, policy rejections, duplicate prevention, retry frequency, approval rates, time to reconcile uncertain operations, audit completeness, and recovery time after failures.&lt;/p&gt;
&lt;p&gt;An agent that answers fluently but cannot explain or reconcile its actions is not ready for production.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The most useful enterprise agents will have bounded, observable, and recoverable autonomy. They can move quickly while the system verifies identity, permissions, business state, and outcomes. MCP helps connect agents to tools; a reliability boundary makes those connections safe enough for real work.&lt;/p&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://modelcontextprotocol.io/specification/latest&quot;&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nist.gov/itl/ai-risk-management-framework&quot;&gt;NIST AI Risk Management Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://owasp.org/www-project-top-10-for-large-language-model-applications/&quot;&gt;OWASP Top 10 for Large Language Model Applications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30693/the-reliability-boundary-what-ai-agents-need-before-they-touch-production-apis</guid>
<pubDate>Sun, 11 Oct 2026 08:43:40 +0000</pubDate>
</item>
<item>
<title>Half-Open Circuit Breaker Probes in Serverless Workers</title>
<link>https://coderlegion.com/30668/half-open-circuit-breaker-probes-in-serverless-workers</link>
<description>&lt;p&gt;Managing dependency recovery in serverless architectures requires coordinating multiple concurrent invocations to prevent a thundering-herd problem when a service heals. &lt;strong&gt;Quick solution:&lt;/strong&gt; Coordinate the half-open lease through a single SQLite-backed Durable Object per dependency, keep the network probe outside the transaction, and defer competing callers. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function acquireProbeLease(storage: DurableObjectStorage, now: number): Promise&amp;lt;boolean&amp;gt; {
  return await storage.transaction(async (txn) =&amp;gt; {
    const state = await txn.get(&quot;breaker_state&quot;);
    if (state &amp;amp;&amp;amp; state.status === &quot;open&quot; &amp;amp;&amp;amp; state.cooldownUntil &amp;gt; now) {
      return false;
    }
    const lease = await txn.get(&quot;probe_lease&quot;);
    if (lease &amp;amp;&amp;amp; lease.expiresAt &amp;gt; now) {
      return false;
    }
    await txn.put(&quot;probe_lease&quot;, { expiresAt: now + 5000 });
    return true;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A circuit breaker protects downstream dependencies by failing fast while an upstream service is unhealthy. The recovery window introduces a new risk when independent request handlers attempt recovery simultaneously. This race condition happens whenever multiple concurrent invocations share a dependency and coordinate through &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/shared-git-state-in-parallel-agent-worktrees/&quot;&gt;shared state&lt;/a&gt;. A scheduled invocation runs at its configured time rather than automatically fanning out into multiple triggers, but a sudden influx of traffic can create the same competitive pressure.&lt;/p&gt;
&lt;h3 id=&quot;understanding-the-circuit-breaker-states&quot;&gt;Understanding the Circuit Breaker States&lt;/h3&gt;
&lt;p&gt;A reliable serverless circuit breaker relies on three distinct operational states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closed: normal traffic passes directly to a healthy dependency.&lt;/li&gt;
&lt;li&gt;Open: calls fail fast or defer while the dependency remains unhealthy.&lt;/li&gt;
&lt;li&gt;Half-open: after a cooldown period, the system admits a controlled probe instead of immediately restoring all traffic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A successful probe closes the circuit breaker. A failed or timed-out probe reopens it and schedules another recovery attempt. A cooldown period represents a probe opportunity rather than a guarantee of full recovery.&lt;/p&gt;
&lt;h3 id=&quot;choosing-a-coordination-boundary&quot;&gt;Choosing a Coordination Boundary&lt;/h3&gt;
&lt;p&gt;Choosing the right coordination backend is essential for safe probe admission. Eventually consistent storage layers cannot provide the atomic guarantees required for single-probe coordination.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Option &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;When to Use It &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Trade-off or Failure Mode &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Recommendation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Workers KV &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Global read-heavy configurations &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Eventually consistent, lacks atomic read-modify-write &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Do not use for transactional lease locks.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;SQLite-backed Durable Object &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Single-region state consistency and transactions &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Bound to a single location, requires routing &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Recommended for single coordination points.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;External Relational Database &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Existing enterprise database clusters &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Adds network latency to serverless invocations &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Avoid if serverless cold-start and latency matter.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A SQLite-backed Durable Object provides strong consistency and transactional guarantees for a single coordination point. Use a stable Durable Object identity for each protected dependency to manage lease admission in a short transaction.&lt;/p&gt;
&lt;h3 id=&quot;managing-recovery-and-failure-handling&quot;&gt;Managing Recovery and Failure Handling&lt;/h3&gt;
&lt;p&gt;Leases must include an expiration timestamp so a crashed or timed-out claimant does not permanently block recovery. Callers that do not receive the lease should return a retryable or deferred outcome rather than spinning in a tight retry loop.&lt;/p&gt;
&lt;p&gt;On a successful probe, close the breaker and clear the active lease. On a failure or timeout, reopen the breaker with an appropriate delay and jitter. For further reading on operational safety mechanisms, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/self-expiring-kill-switches-and-human-readable-alerts/&quot;&gt;Self-Expiring Kill Switches and Human-Readable Alerts&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;verification-checklist&quot;&gt;Verification Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Issue five simultaneous lease requests against the Durable Object and verify that exactly one caller receives permission to probe.&lt;/li&gt;
&lt;li&gt;Confirm that expired leases allow a new claimant to acquire the probe slot after the timeout window elapses.&lt;/li&gt;
&lt;li&gt;Verify that late probe results from an expired lease do not overwrite newer breaker states in storage.&lt;/li&gt;
&lt;li&gt;Check that callers without a lease receive a deferred response rather than executing unauthorized network traffic.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30668/half-open-circuit-breaker-probes-in-serverless-workers</guid>
<pubDate>Sun, 11 Oct 2026 02:44:43 +0000</pubDate>
</item>
<item>
<title>Testing LLM Fallback Tiers Before Outages</title>
<link>https://coderlegion.com/30571/testing-llm-fallback-tiers-before-outages</link>
<description>&lt;p&gt;When a primary language model provider rejects requests due to location or transient overloads, many teams rely on an in-platform fallback tier. Logging response bodies and separating deterministic blocks from transient failures solves the diagnosis layer, but it leaves a critical question unanswered. A fallback that never runs in production is merely a hope. If you do not test the backup path continuously, your first time seeing it fire will be during an outage, which is the worst possible time to discover a configuration bug.&lt;/p&gt;
&lt;p&gt;Running a reliable secondary tier requires more than basic routing rules. You need a reliable warming pattern that does not inflate latency or cost, a strict response-shape contract so consumers never know which provider answered, and a scheduled drill cadence with a clear readiness metric. This guide covers how to implement these mechanisms in a serverless environment without breaking your budget.&lt;/p&gt;
&lt;h3 id=&quot;fractional-shadow-traffic-with-diff-logging&quot;&gt;Fractional Shadow Traffic With Diff Logging&lt;/h3&gt;
&lt;p&gt;To ensure a fallback model is ready, you need to send it realistic payloads. However, routing user traffic blindly to an unproven secondary provider risks increased latency and unexpected error rates. The solution is fractional shadow traffic.&lt;/p&gt;
&lt;p&gt;In a serverless worker, you can mirror a small, sampled percentage of live requests to the fallback tier asynchronously. By using asynchronous execution primitives like &lt;code&gt;waitUntil&lt;/code&gt;, the shadow request fires in the background without affecting the user-visible critical path. Both the primary response and the shadow response are logged, and their normalized shapes are compared.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;lt;Response&amp;gt; {
  const primaryResponse = await callPrimaryProvider(request, env);
  
  // Sample 2 percent of traffic for shadow warming
  if (Math.random() &amp;lt; 0.02) {
    ctx.waitUntil(runShadowFallback(request, primaryResponse, env));
  }
  
  return primaryResponse;
}

async function runShadowFallback(request: Request, primary: Response, env: Env): Promise&amp;lt;void&amp;gt; {
  try {
    const fallbackRaw = await callFallbackProvider(request, env);
    const diff = compareResponseShapes(primary, fallbackRaw);
    if (diff.hasMismatch) {
      await logShapeMismatch(env, diff);
    }
  } catch (error) {
    await logShadowError(env, error);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach gives you continuous coverage. If the fallback provider alters its error format or response structure, your diff logs catch the mismatch on a quiet Tuesday rather than during a traffic spike.&lt;/p&gt;
&lt;h3 id=&quot;response-shape-adapter-contracts&quot;&gt;Response-Shape Adapter Contracts&lt;/h3&gt;
&lt;p&gt;Different language model providers return unique JSON envelopes, usage metadata, and finish reasons. If your application code directly handles provider-specific payloads, switching to a fallback tier requires branching your business logic everywhere.&lt;/p&gt;
&lt;p&gt;To eliminate this complexity, introduce a response-shape adapter. Neither provider's raw envelope should ever reach your core application. Instead, every provider output passes through a normalization layer that maps data to a single internal structure containing only the generated text, token counts, finish reason, and standardized error classes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface NormalizedResponse {
  text: string;
  inputTokens: number;
  outputTokens: number;
  finishReason: 'stop' | 'length' | 'error';
  errorClass?: string;
}

function adaptProviderResponse(raw: any, provider: string): NormalizedResponse {
  if (provider === 'primary') {
    return {
      text: raw.candidates[0].output,
      inputTokens: raw.usage.prompt_tokens,
      outputTokens: raw.usage.completion_tokens,
      finishReason: raw.candidates[0].finish_reason === 'MAX_TOKENS' ? 'length' : 'stop'
    };
  } else {
    return {
      text: raw.choices[0].message.content,
      inputTokens: raw.usage.input_tokens,
      outputTokens: raw.usage.output_tokens,
      finishReason: raw.choices[0].finish_reason
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By forcing both primary and fallback responses through this adapter, failover becomes a simple routing decision. The rest of your application remains entirely agnostic of which provider fulfilled the prompt.&lt;/p&gt;
&lt;h3 id=&quot;scheduled-failover-drills-and-readiness-metrics&quot;&gt;Scheduled Failover Drills and Readiness Metrics&lt;/h3&gt;
&lt;p&gt;Asynchronous shadow traffic proves that responses match, but it does not verify that your routing logic actually triggers when the primary provider fails. For that, you need scheduled failover drills.&lt;/p&gt;
&lt;p&gt;A cron-triggered background worker can force a small batch of synthetic probe traffic through the fallback tier for a bounded window. The pass bar for these drills is binary. Every probe must succeed, and every response must conform to the normalized shape contract. If a single probe fails, the drill fails. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/bounded-llm-fallback-chains/&quot;&gt;Bounded Llm Fallback Chains&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To give the on-call engineer immediate confidence during an incident, combine your telemetry into a single readiness score. This score factors in three underlying data series:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Shadow success rate over the last twenty-four hours.&lt;/li&gt;
&lt;li&gt;Shape-conformance rate from differential logging.&lt;/li&gt;
&lt;li&gt;Days elapsed since the last successful green drill.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the drills are skipped or if the shadow diff rate degrades, the readiness score drops automatically. Your incident runbook should require the on-call engineer to verify that this readiness score meets a minimum threshold before manually forcing a failover switch, removing guesswork from high-stress moments. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/audit-macos-system-data-before-deleting-developer-caches/&quot;&gt;Audit Macos System Data Before Deleting&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;bounding-cost-and-complexity&quot;&gt;Bounding Cost and Complexity&lt;/h3&gt;
&lt;p&gt;Warming a fallback tier and running regular synthetic drills consumes billable API requests. Without cost controls, your reliability engineering efforts can inadvertently trigger a billing incident.&lt;/p&gt;
&lt;p&gt;Keep shadow traffic tightly sampled, typically between one and five percent of total production volume. Cap the number of synthetic probes executed during weekly drills to the minimum necessary for statistical confidence. By bounding these operations and scheduling them during off-peak windows, you maintain a continuously verified backup system while keeping expenses entirely predictable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30571/testing-llm-fallback-tiers-before-outages</guid>
<pubDate>Sat, 10 Oct 2026 09:03:31 +0000</pubDate>
</item>
<item>
<title>⚡️ Caching Strategies for Web Apps: From Browser to Backend</title>
<link>https://coderlegion.com/30544/caching-strategies-for-web-apps-from-browser-to-backend</link>
<description>&lt;p&gt;Caching is one of the &lt;strong&gt;most powerful ways to improve application performance&lt;/strong&gt;—reducing load times, server costs, and user frustration. Yet many developers treat caching as a “black box” or skip it due to fear of cache-related bugs.&lt;/p&gt;
&lt;p&gt;In this blog, we will:&lt;br&gt;
✅ Demystify caching: what it is and why it matters&lt;br&gt;
✅ Explore &lt;strong&gt;frontend (browser) caching strategies&lt;/strong&gt;&lt;br&gt;
✅ Dive into &lt;strong&gt;backend and distributed caching&lt;/strong&gt; with Go examples&lt;br&gt;
✅ Cover &lt;strong&gt;Cache-Control headers, ETag, stale-while-revalidate&lt;/strong&gt;&lt;br&gt;
✅ Discuss &lt;strong&gt;common pitfalls and best practices&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By the end, you’ll know how to confidently implement caching in your React frontend and Go backend to build &lt;strong&gt;scalable, fast, and efficient applications&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-is-caching-&quot;&gt;🚀 What is Caching?&lt;/h3&gt;
&lt;p&gt;Caching means &lt;strong&gt;storing data temporarily&lt;/strong&gt; so future requests can be served faster without recomputing or refetching the same data repeatedly.&lt;/p&gt;
&lt;p&gt;Think of it like &lt;strong&gt;preparing tea and storing it in a flask&lt;/strong&gt;—you don’t need to reboil water each time you want to drink.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-why-caching-matters&quot;&gt;📈 Why Caching Matters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Return data faster to users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce Server Load:&lt;/strong&gt; Avoid redundant computation or DB queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve Reliability:&lt;/strong&gt; Serve cached responses when backend services are down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce Costs:&lt;/strong&gt; Less compute = lower cloud bills.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-caching-in-the-frontend-browser-&quot;&gt;🧩 Caching in the Frontend (Browser)&lt;/h3&gt;
&lt;h4 id=&quot;1-browser-cache-http-caching-&quot;&gt;1️⃣ Browser Cache (HTTP Caching)&lt;/h4&gt;
&lt;p&gt;Browsers automatically cache assets (JS, CSS, images) and API responses based on HTTP headers.&lt;/p&gt;
&lt;h5 id=&quot;key-headers-&quot;&gt;Key Headers:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt;: Defines how long and under what conditions the browser can reuse the response.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt;: Helps validate cached data; server sends a hash, and browser sends it back to check if the data changed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expires&lt;/code&gt;: Sets a hard expiration time for cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stale-while-revalidate&lt;/code&gt;: Allows serving stale content while revalidating in the background for a seamless UX.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cache-Control: public, max-age=3600, stale-while-revalidate=600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Use Case:&lt;/strong&gt; Cache API GET responses for non-user-specific data (e.g., blog posts, product catalogs).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-client-side-data-fetching-libraries&quot;&gt;2️⃣ Client-Side Data Fetching Libraries&lt;/h4&gt;
&lt;p&gt;Libraries like &lt;strong&gt;React Query, SWR&lt;/strong&gt; provide built-in caching for API calls.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache API responses in memory.&lt;/li&gt;
&lt;li&gt;Background revalidation for fresh data.&lt;/li&gt;
&lt;li&gt;Avoids unnecessary re-fetching on component re-mounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Use for &lt;strong&gt;per-session data caching&lt;/strong&gt; in React apps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-caching-in-the-backend&quot;&gt;🗂️ Caching in the Backend&lt;/h3&gt;
&lt;h4 id=&quot;1-in-memory-caching&quot;&gt;1️⃣ In-Memory Caching&lt;/h4&gt;
&lt;p&gt;Store frequently accessed data in memory for fast retrieval.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Golang’s &lt;code&gt;sync.Map&lt;/code&gt;, &lt;code&gt;groupcache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fastest but limited by server RAM and is not shared across instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Use for small, frequently requested data (e.g., config, auth tokens).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-distributed-caching&quot;&gt;2️⃣ Distributed Caching&lt;/h4&gt;
&lt;p&gt;Tools like &lt;strong&gt;Redis and Memcached&lt;/strong&gt; provide a shared cache layer accessible across multiple servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Centralized cache for load-balanced apps.&lt;/li&gt;
&lt;li&gt;Supports expiration policies.&lt;/li&gt;
&lt;li&gt;Can handle large datasets.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-go-example-using-redis-for-caching&quot;&gt;🔹 Go Example: Using Redis for Caching&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Install Redis client:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go get github.com/go-redis/redis/v8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Basic Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;rdb := redis.NewClient(&amp;amp;redis.Options{
    Addr: &quot;localhost:6379&quot;,
})

ctx := context.Background()

// Set cache
rdb.Set(ctx, &quot;product_123&quot;, jsonData, time.Hour)

// Get cache
val, err := rdb.Get(ctx, &quot;product_123&quot;).Result()
if err == redis.Nil {
    // Cache miss, fetch from DB
} else {
    // Cache hit, use val
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cache-invalidation-the-hard-part&quot;&gt;⚖️ Cache Invalidation: The Hard Part&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;“There are only two hard things in Computer Science: cache invalidation and naming things.” – Phil Karlton&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Whenever the underlying data changes, you need to ensure the cache is updated to avoid stale data.&lt;/p&gt;
&lt;h4 id=&quot;strategies-&quot;&gt;Strategies:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;✅ Time-based expiration (TTL):&lt;/strong&gt;&lt;br&gt;
Set a cache expiry time so data auto-expires after a fixed duration, ensuring old data clears automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Manual invalidation:&lt;/strong&gt;&lt;br&gt;
Manually clear or update cache entries whenever the underlying data changes to keep data fresh.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Write-through caching:&lt;/strong&gt;&lt;br&gt;
Write data to both the cache and the database at the same time, ensuring consistency during reads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Cache busting:&lt;/strong&gt;&lt;br&gt;
Change cache keys (e.g., &lt;code&gt;v1:product_123&lt;/code&gt; → &lt;code&gt;v2:product_123&lt;/code&gt;) when your data structure changes to avoid serving stale or incompatible data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-best-practices-for-caching&quot;&gt;✅ Best Practices for Caching&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cache &lt;strong&gt;only GET requests&lt;/strong&gt;; avoid caching POST unless explicitly designed.&lt;/li&gt;
&lt;li&gt;Set reasonable TTLs to avoid stale data.&lt;/li&gt;
&lt;li&gt;Monitor cache hit/miss rates to tune cache policies.&lt;/li&gt;
&lt;li&gt;Be cautious caching user-specific or sensitive data.&lt;/li&gt;
&lt;li&gt;Use cache versioning (&lt;code&gt;v1:product_123&lt;/code&gt;) to manage structure changes.&lt;/li&gt;
&lt;li&gt;Document what is cached, where, and why.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-common-pitfalls&quot;&gt;⚠️ Common Pitfalls&lt;/h3&gt;
&lt;p&gt;🚫 Caching sensitive user data publicly.&lt;br&gt;
🚫 Forgetting to invalidate cache on data updates.&lt;br&gt;
🚫 Over-caching dynamic endpoints, leading to stale or incorrect UI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-future-of-caching-cdn-amp-edge-caching&quot;&gt;🔮 Future of Caching: CDN &amp;amp; Edge Caching&lt;/h3&gt;
&lt;p&gt;Content Delivery Networks (CDNs) like &lt;strong&gt;Cloudflare, AWS CloudFront, Vercel Edge&lt;/strong&gt; cache data closer to the user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;br&gt;
✅ Lower latency.&lt;br&gt;
✅ Offload origin server traffic.&lt;br&gt;
✅ Built-in stale-while-revalidate strategies.&lt;/p&gt;
&lt;p&gt;Using edge caching in modern React + Go apps can drastically improve performance globally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-final-thoughts&quot;&gt;🎯 Final Thoughts&lt;/h3&gt;
&lt;p&gt;Caching isn’t just an optimization; it’s essential for building &lt;strong&gt;scalable, reliable, and fast applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Whether it’s &lt;strong&gt;browser caching, React Query in the frontend, or Redis in the backend&lt;/strong&gt;, knowing when and how to cache makes you a stronger developer.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;💬 &lt;strong&gt;Have you implemented caching in your apps? What caching challenges have you faced? Let’s discuss in the comments!&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30544/caching-strategies-for-web-apps-from-browser-to-backend</guid>
<pubDate>Sat, 10 Oct 2026 03:33:24 +0000</pubDate>
</item>
<item>
<title>Nightly Analyst Digest from Slot Activity Logs</title>
<link>https://coderlegion.com/30528/nightly-analyst-digest-from-slot-activity-logs</link>
<description>&lt;p&gt;Running a high-frequency automation system that triggers dozens of times per day creates a difficult reporting dilemma. If every run sends an immediate notification, your inbox fills with noise. If you turn off notifications entirely, regressions go unnoticed until a user complains. The natural solution is a nightly digest, but building one reliably on serverless infrastructure introduces subtle failure modes. A naive implementation often results in missing reports when the underlying language model fails, misaligned days due to UTC offset handling, or raw log dumps that require too much manual effort to parse.&lt;/p&gt;
&lt;p&gt;This article examines how to build a reliable scheduled digest email from activity log data. You will learn how to design a logging contract specifically for downstream aggregation, handle timezone boundaries without drifting, implement a graceful fallback when the analyst model is unavailable, and keep recommendations strictly advisory.&lt;/p&gt;
&lt;h3 id=&quot;sixty-emails-versus-one-digest&quot;&gt;Sixty Emails versus One Digest&lt;/h3&gt;
&lt;p&gt;When a system executes tasks tens of times a day, individual alerts quickly lose their value. Developers stop reading them, and important failures hide inside a flood of routine success notices. A single daily email solves this fatigue by aggregating metrics, engagement statistics, and notable events into one readable summary. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/email-and-account-safety-for-developers/&quot;&gt;Email Account Safety Developers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, building this digest introduces three architectural challenges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data Availability&lt;/strong&gt;: Slots that only execute tasks do not inherently preserve the context needed for a narrative summary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fragile Dependencies&lt;/strong&gt;: If the nightly summary relies entirely on a third-party language model to generate text, an API outage will prevent the email from sending entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timezone Drift&lt;/strong&gt;: Standardizing cron jobs on UTC while the human reader operates in a local time zone splits calendar days incorrectly, producing reports that mix evening events from the wrong day.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Solving these issues requires treating the nightly digest not as an afterthought, but as a core pipeline with its own data contract and error boundaries.&lt;/p&gt;
&lt;h3 id=&quot;logging-for-the-analyst&quot;&gt;Logging for the Analyst&lt;/h3&gt;
&lt;p&gt;To produce a meaningful summary, individual execution slots must leave behind structured data designed for aggregation rather than raw debugging. Instead of unstructured console logs, every slot should append a compact JSON line to a date-keyed store with a multi-day time-to-live (TTL). For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/debugging-silent-skips-in-poll-based-reply-bots/&quot;&gt;Debugging Silent Skips In Poll Based&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Below is an example of a structured log entry recorded by a worker slot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;timestamp&quot;: &quot;2023-10-25T08:30:00Z&quot;,
  &quot;slot_id&quot;: &quot;hourly-sync-04&quot;,
  &quot;status&quot;: &quot;success&quot;,
  &quot;model&quot;: &quot;gpt-4o-mini&quot;,
  &quot;provider&quot;: &quot;openai&quot;,
  &quot;material_count&quot;: 12,
  &quot;reply_count&quot;: 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Writing these entries rides on existing key-value storage writes, avoiding the overhead of a dedicated database. To prevent timezone ambiguity, the storage key incorporates the audience local date directly rather than relying on raw UTC timestamps. For example, a key format of &lt;code&gt;digest:2023-10-25&lt;/code&gt; ensures all runs belonging to that calendar day in the target region are grouped together, regardless of UTC shift.&lt;/p&gt;
&lt;h3 id=&quot;the-degraded-digest-pattern&quot;&gt;The Degraded Digest Pattern&lt;/h3&gt;
&lt;p&gt;Relying on a language model to synthesize raw activity logs into human-readable insights introduces a single point of failure. If the provider experiences downtime or rate limits at the cron trigger time, the entire digest fails to send, leaving the team blind.&lt;/p&gt;
&lt;p&gt;To make the system resilient, the analyst call must be treated as best-effort. If the model call fails or times out, the service must catch the error, fall back to displaying the computed raw statistics block directly, and label the email accordingly.&lt;/p&gt;
&lt;p&gt;Here is a conceptual TypeScript implementation of the execution handler with fallback logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function generateDigestReport(rawLogs: LogEntry[]): Promise&amp;lt;DigestResult&amp;gt; {
  const stats = computeAggregates(rawLogs);
  
  try {
    const analysis = await callAnalystModel(stats);
    return {
      mode: &quot;ai-enhanced&quot;,
      summary: analysis.text,
      recommendations: analysis.recommendations,
      stats
    };
  } catch (error) {
    return {
      mode: &quot;degraded-raw-stats&quot;,
      summary: &quot;Analyst model unavailable. Displaying raw computed metrics.&quot;,
      recommendations: [],
      stats
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern ensures that an upstream outage degrades the quality of the formatting rather than breaking delivery. The recipient still receives their morning metrics on schedule.&lt;/p&gt;
&lt;h3 id=&quot;questions-not-actions&quot;&gt;Questions, Not Actions&lt;/h3&gt;
&lt;p&gt;Another risk in automated reporting is allowing the analyst model to modify system behavior based on its own findings. If an LLM decides that a prompt or execution cadence should change and automatically applies that change, the reporting pipeline transforms into an unsupervised and potentially destructive deployment system.&lt;/p&gt;
&lt;p&gt;To maintain safety, recommendations generated by the digest must be phrased strictly as questions for human review. For instance, rather than updating configuration files, the output should suggest adjustments like &quot;Consider increasing the timeout limit for morning sync runs?&quot; or &quot;Cadence for slot B generated high error rates; review prompt constraints.&quot; An explicit human decision remains required to apply any configuration change.&lt;/p&gt;
&lt;h3 id=&quot;summary-of-architectural-patterns&quot;&gt;Summary of Architectural Patterns&lt;/h3&gt;
&lt;p&gt;Building a dependable nightly analyst digest requires more than wiring a cron trigger to an AI model. By designing a dedicated logging contract, anchoring date keys to the audience timezone, implementing a non-LLM fallback for the summary step, and keeping recommendations advisory, you can achieve the best of both worlds. You get the readability of an AI-assisted narrative without sacrificing the reliability of traditional serverless infrastructure.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30528/nightly-analyst-digest-from-slot-activity-logs</guid>
<pubDate>Sat, 10 Oct 2026 03:03:48 +0000</pubDate>
</item>
<item>
<title>Debugging Silent Skips in Poll-Based Reply Bots</title>
<link>https://coderlegion.com/30521/debugging-silent-skips-in-poll-based-reply-bots</link>
<description>&lt;p&gt;When a poll-based reply bot runs in a staging environment, it usually behaves predictably. It reads recent posts, checks for mentions, and generates answers. In production, however, a user report reveals that a valid reply sat unanswered for hours while every execution log reported complete success with zero errors. The telemetry shows nothing unusual because the system never encountered an unhandled exception. It simply decided that there was nothing to process. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-viewmodel-partial-success-refresh-failure/&quot;&gt;Viewmodel Partial Success Refresh Failure&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This silent failure mode typically stems from two intertwined design choices: bare continue statements that swallow item-level decisions and fixed lookback windows that slide past older content as new posts arrive. Understanding why these patterns cause silent failures helps make background workers transparent and robust.&lt;/p&gt;
&lt;h3 id=&quot;the-silent-sieve-of-bare-continue-statements&quot;&gt;The Silent Sieve of Bare Continue Statements&lt;/h3&gt;
&lt;p&gt;Most poll loops rely on a sequence of guards to decide whether an incoming post requires a response. These guards check if the bot already replied, whether the post originates from an authorized user, or if the content matches specific criteria. When a guard decides to skip an item, the standard implementation often uses a bare control flow statement.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for item in fetched_items:
    if already_replied(item):
        continue
    if not is_author_valid(item):
        continue
    process_reply(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern creates an observability blind spot. Every skipped item vanishes into the loop structure. The function completes successfully, returns an empty result, and logs an ordinary run completion. To the monitoring system, a run that skipped every available item looks identical to a run where no items needed attention.&lt;/p&gt;
&lt;p&gt;Replacing bare control flow with structured skip tracking changes the diagnostic profile of the worker. Instead of hiding the decision, each guard can record why an item was passed over.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;skipped = []
for item in fetched_items:
    if already_replied(item):
        skipped.append({&quot;id&quot;: item[&quot;id&quot;], &quot;reason&quot;: &quot;already_replied&quot;})
        continue
    if not is_author_valid(item):
        skipped.append({&quot;id&quot;: item[&quot;id&quot;], &quot;reason&quot;: &quot;invalid_author&quot;})
        continue
    process_reply(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returning this structured list alongside successful actions transforms log-diving into direct inspection. When an item remains unanswered, the output explicitly states whether the system saw it and why it chose to ignore it.&lt;/p&gt;
&lt;h3 id=&quot;the-sliding-window-horizon-problem&quot;&gt;The Sliding Window Horizon Problem&lt;/h3&gt;
&lt;p&gt;Beyond guard logic, the query window itself often introduces silent omissions. To avoid scanning an entire history, bots typically request only the N most recent posts. If a bot reads the five most recent posts every ten minutes, it operates under the assumption that all relevant interactions happen within that horizon.&lt;/p&gt;
&lt;p&gt;This assumption breaks down when post velocity increases. If a burst of activity pushes an unanswered mention beyond the fifth position, the sliding window moves past it. The post remains active on the platform, but the polling mechanism no longer includes it in the fetch payload. Manual testing often masks this issue because running a manual test posts fresh content to the timeline, pushing the neglected item even further out of bounds.&lt;/p&gt;
&lt;p&gt;Fixing this requires sizing the lookback window against actual content velocity rather than relying on a static magic constant. If human replies typically arrive within a multi-hour window, the fetch limit must scale to cover that timeframe based on the average rate of incoming posts. Bounding the window remains necessary to control API usage, but the boundary should reflect operational reality instead of an arbitrary small number.&lt;/p&gt;
&lt;h3 id=&quot;testing-the-negative&quot;&gt;Testing the Negative&lt;/h3&gt;
&lt;p&gt;Because these bugs manifest as silence rather than crashes, standard test suites often fail to catch them. A test that only asserts on successful replies will pass even if the bot is silently skipping every valid input.&lt;/p&gt;
&lt;p&gt;Comprehensive test coverage for a poll-based worker must assert on the negative state. A proper test verifies that when no work is required, the skip list explicitly populates with expected reasons rather than returning an ambiguous empty response. By asserting on both sent actions and structured skip records, engineers can prove that the worker is actively evaluating items rather than bypassing them.&lt;/p&gt;
&lt;h3 id=&quot;resolving-poll-based-failures&quot;&gt;Resolving Poll-Based Failures&lt;/h3&gt;
&lt;p&gt;Poll-based reply bots fail quietly when their control flow obscures item-level decisions and their fetch windows are too narrow for the surrounding content velocity. Replacing bare continue statements with structured skip records and tying lookback limits to real-world posting rates turns invisible failures into inspectable state, ensuring that background workers handle every interaction predictably.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30521/debugging-silent-skips-in-poll-based-reply-bots</guid>
<pubDate>Fri, 09 Oct 2026 21:01:58 +0000</pubDate>
</item>
<item>
<title>Safe Public Demo Environments for Android Apps</title>
<link>https://coderlegion.com/30332/safe-public-demo-environments-for-android-apps</link>
<description>&lt;p&gt;Offering a public demo of an Android application presents a unique architectural challenge. A demo build is exposed to strangers and needs frequent, destructive data resets, yet it must never be able to target or corrupt the real customer database by accident. Relying on a simple runtime UI flag or environment toggle inside the production codebase creates an unacceptable risk of accidental data leakage. This article examines how to build a robust isolation boundary using separate build variants, dedicated demo backends, and fail-closed identity validation.&lt;/p&gt;
&lt;h3 id=&quot;why-a-demo-toggle-fails-as-an-isolation-boundary&quot;&gt;Why a Demo Toggle Fails as an Isolation Boundary&lt;/h3&gt;
&lt;p&gt;Many development teams initially attempt to support public demonstrations by adding a toggle within the primary application and backend. The logic often checks whether a demo mode flag is active before skipping certain write operations or routing requests to mock data. This approach fails because a single code path or misconfigured deployment can easily bypass the flag. If a build script accidentally packages the production credentials or connects the demo backend to the primary database, a public reset operation can wipe out real customer records.&lt;/p&gt;
&lt;p&gt;True isolation requires separating the trust boundary at every layer of the architecture. The application package name, the backend service instance, and the database project reference must be distinct. If any component in the chain does not match the expected demo configuration, the service should refuse to start entirely rather than attempting to recover or fallback.&lt;/p&gt;
&lt;h3 id=&quot;separate-android-build-variants-and-app-identity&quot;&gt;Separate Android Build Variants and App Identity&lt;/h3&gt;
&lt;p&gt;The first layer of defense begins in the Android client. Using build variants allows developers to maintain different application IDs, source sets, and configuration values within the same repository. By modifying the application ID for the demo variant, the operating system treats the demo build and the production build as entirely separate apps on the test device.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-groovy&quot;&gt;android {
    defaultConfig {
        applicationId &quot;com.example.app&quot;
    }
    buildTypes {
        release {
            minifyEnabled true
        }
    }
    flavorDimensions &quot;mode&quot;
    productFlavors {
        production {
            dimension &quot;mode&quot;
            applicationIdSuffix &quot;&quot;
        }
        demo {
            dimension &quot;mode&quot;
            applicationIdSuffix &quot;.demo&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration ensures that installation artifacts are distinct. More importantly, keeping the choice fixed in the build artifact prevents users or automated scripts from accidentally activating a demo mode inside a production binary.&lt;/p&gt;
&lt;h3 id=&quot;verify-backend-and-database-identity&quot;&gt;Verify Backend and Database Identity&lt;/h3&gt;
&lt;p&gt;Client-side separation alone is insufficient if the backend can connect to any database via environment variables. A dedicated demo backend must require explicit configuration flags and perform strict startup validation. During initialization, the backend should compare the database project reference derived from its active connection string against an expected demo project reference.&lt;/p&gt;
&lt;p&gt;If the strings do not match, the application must immediately throw an unhandled exception and shut down. This fail-closed mechanism ensures that a server misconfiguration becomes immediately visible during deployment rather than remaining a latent vulnerability.&lt;/p&gt;
&lt;h3 id=&quot;seed-and-reset-guarded-disposable-data&quot;&gt;Seed and Reset Guarded Disposable Data&lt;/h3&gt;
&lt;p&gt;Public demo environments require a mechanism to restore clean state after users alter or delete records. However, this reset mechanism must be guarded against accidental execution against live systems. Implementation evidence suggests a two-step validation model for data lifecycle management:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Initial seeding should only proceed if every demo table is entirely empty. If existing records are detected, the seed routine halts.&lt;/li&gt;
&lt;li&gt;Subsequent resets require a persisted security sentinel string to match the configured sentinel, and the database identity must be re-verified immediately before any deletion takes place.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Furthermore, developers should strip all non-essential integrations from the demo service. Production push notification credentials, third-party payment hooks, and external spreadsheet integrations should be omitted so that a compromised or heavily tested demo environment cannot trigger external side effects.&lt;/p&gt;
&lt;h3 id=&quot;what-process-local-locks-do-not-protect&quot;&gt;What Process-Local Locks Do Not Protect&lt;/h3&gt;
&lt;p&gt;When implementing automated reset endpoints, developers frequently reach for concurrency primitives such as a language-specific mutex to prevent overlapping reset requests. While a process-local mutex successfully serializes concurrent coroutines or threads within a single backend instance, it offers no protection in a horizontally scaled architecture. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/managing-concurrent-git-commits-during-automated-publishing/&quot;&gt;Managing Concurrent Git Commits During Automated&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If the demo backend is deployed behind a load balancer with multiple active instances, each instance maintains its own memory space and its own mutex. Two distinct server processes could execute a database reset simultaneously. Protecting a multi-instance demo environment requires database-level locking, a dedicated single-instance worker queue, or another distributed coordination mechanism. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/safe-multi-environment-database-orchestration/&quot;&gt;Safe Multi Environment Database Orchestration&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;release-checklist-for-a-public-demo&quot;&gt;Release Checklist for a Public Demo&lt;/h3&gt;
&lt;p&gt;Before launching a public demo environment, verify the following architectural controls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The demo application variant uses a distinct application ID suffix.&lt;/li&gt;
&lt;li&gt;The backend enforces explicit demo mode flags and fails startup if the database project reference does not match.&lt;/li&gt;
&lt;li&gt;Initial data seeding refuses to run unless all target tables are empty.&lt;/li&gt;
&lt;li&gt;Destructive reset operations require a valid persisted sentinel check and a pre-execution database identity re-validation.&lt;/li&gt;
&lt;li&gt;Session secrets and sentinels meet minimum length requirements.&lt;/li&gt;
&lt;li&gt;Non-essential production integrations and credentials are completely removed from the demo service deployment.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30332/safe-public-demo-environments-for-android-apps</guid>
<pubDate>Thu, 08 Oct 2026 23:37:04 +0000</pubDate>
</item>
<item>
<title>Self-Expiring Kill Switches and Human-Readable Alerts</title>
<link>https://coderlegion.com/30345/self-expiring-kill-switches-and-human-readable-alerts</link>
<description>&lt;p&gt;When an unattended cron system encounters a total provider failure, engineers often reach for a manual kill switch. While effective at stopping runaway requests, the manual-only reset pattern introduces hidden operational costs. If an outage lasts twenty minutes, the system stays dark until an operator manually intervenes. When multiple transient interruptions occur within a single day, teams find themselves repeatedly waking up or context-switching to clear flags for zero new information. Furthermore, when these systems fail, they frequently dump raw JSON status codes into email inboxes. Over time, operators learn to ignore these notifications entirely, missing the genuine incidents hidden beneath the noise. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/ai-agent-handoff-fallback-context/&quot;&gt;Ai Agent Handoff Fallback Context&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Designing resilient background workflows requires solving two distinct problems. First, the system needs an automated mechanism to halt execution during widespread failures without trapping the infrastructure in a permanent locked state. Second, notifications must translate low-level errors into actionable prose so that operators can assess the situation at a glance. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/fixing-malformed-json-errors-in-android-and-ci-pipelines/&quot;&gt;Fixing Malformed Json Errors In Ci&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-manual-only-resets&quot;&gt;The Problem with Manual-Only Resets&lt;/h3&gt;
&lt;p&gt;Traditional architectural patterns treat a kill switch as a binary toggle. An operator sets a flag, the background jobs stop, and someone must later run a command or update a database record to resume execution. In practice, this creates a mismatch between system recovery and human intervention. Providers frequently recover on their own within minutes. If the kill switch lacks an expiration mechanism, the recovery is bottlenecked by human availability. The operational burden shifts from fixing the infrastructure to administrative housekeeping.&lt;/p&gt;
&lt;h3 id=&quot;designing-the-expiring-kill-flag&quot;&gt;Designing the Expiring Kill Flag&lt;/h3&gt;
&lt;p&gt;To balance safety with automation, the kill flag can be upgraded from a simple boolean to a timestamped window. When a total provider failure is detected, the system writes a kill key containing the exact engagement time. Subsequent execution cycles check this key before running. If the current time is within a bounded window, such as three hours, execution is suppressed.&lt;/p&gt;
&lt;p&gt;Once the timestamp exceeds the expiration threshold, the system automatically clears the flag and resumes normal operations. This ensures that the infrastructure never remains silenced indefinitely due to an forgotten flag. &lt;/p&gt;
&lt;p&gt;Security and failure modes require strict handling. If the kill key value is unreadable, malformed, or corrupted, the system must fail closed rather than assuming everything is fine. Expiry logic applies exclusively to well-formed timestamps.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;kill_switch&quot;: {
    &quot;status&quot;: &quot;active&quot;,
    &quot;engaged_at&quot;: &quot;2023-10-25T14:00:00Z&quot;,
    &quot;expires_at&quot;: &quot;2023-10-25T17:00:00Z&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When a cron worker evaluates this structure, it compares the current clock against the expiration boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def is_kill_switch_active(flag_data):
    if not flag_data or &quot;expires_at&quot; not in flag_data:
        # Fail closed on corrupted or missing flags
        return True
    
    current_time = get_current_utc_time()
    if current_time &amp;lt; flag_data[&quot;expires_at&quot;]:
        return True
        
    return False
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;writing-alerts-humans-actually-read&quot;&gt;Writing Alerts Humans Actually Read&lt;/h3&gt;
&lt;p&gt;Infrastructure notifications often fail because they optimize for machine parsing rather than human comprehension. Sending raw JSON dumps or raw status codes trains engineers to filter out alerts. A robust fallback reporting system replaces machine payloads with deterministic prose templates.&lt;/p&gt;
&lt;p&gt;Instead of dispatching stack traces, the notification service maps known error shapes to human causes. For instance, a connection timeout combined with a specific gateway response gets translated into a clear sentence explaining that the upstream provider is experiencing degraded performance. &lt;/p&gt;
&lt;p&gt;Furthermore, auto-clearing events do not require a dedicated notification. When the three-hour window expires and execution resumes, the transition is recorded as ordinary data in the next scheduled summary digest. This keeps communication channels quiet during self-healing incidents while preserving historical visibility.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Reliable background automation relies on bounding manual intervention and respecting human attention limits. By combining self-expiring timestamps with plain-language fallback reports, engineering teams can stop bleeding during provider outages without trapping their systems in permanent manual locks or drowning in unreadable alerts.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30345/self-expiring-kill-switches-and-human-readable-alerts</guid>
<pubDate>Thu, 08 Oct 2026 22:16:58 +0000</pubDate>
</item>
<item>
<title>Publishing Threads Carousels from Worker-Hosted Images</title>
<link>https://coderlegion.com/30314/publishing-threads-carousels-from-worker-hosted-images</link>
<description>&lt;p&gt;When you want to publish multi-image carousels to Threads through its API, every item requires a publicly downloadable image URL. Traditional architectures solve this by uploading generated or processed assets to an object storage service like Amazon S3 or Cloudflare R2 before making API requests. If you are running your application entirely within a serverless worker, adding a separate storage bucket introduces extra infrastructure overhead, credentials management, and recurring billing for assets that an external platform downloads only once. This guide explores how to host carousel images directly inside a Cloudflare Worker using KV storage, bypass traditional object storage, and manage the multi-step publishing pipeline successfully. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/running-a-zero-cost-social-auto-poster-on-cloudflare-workers/&quot;&gt;Running A Zero Cost Social Auto&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-missing-image-problem-in-serverless&quot;&gt;The Missing Image Problem in Serverless&lt;/h3&gt;
&lt;p&gt;Serverless workers excel at handling stateless API requests, executing webhook logic, and routing web traffic on the edge. However, they lack a persistent filesystem or a static file server. You cannot simply drop generated images into a public folder inside your worker project because the runtime environment evaluates code on demand rather than serving static assets from disk.&lt;/p&gt;
&lt;p&gt;To bridge this gap, you need a mechanism to store binary data temporarily or semi-permanently within your existing serverless ecosystem and expose it via a public URL that Meta can reach. While external buckets are the default recommendation, small-scale or ephemeral workloads often do not justify the added configuration overhead.&lt;/p&gt;
&lt;h3 id=&quot;kv-as-a-one-download-cdn&quot;&gt;KV as a One-Download CDN&lt;/h3&gt;
&lt;p&gt;Cloudflare KV allows you to store key-value pairs at the edge. By treating KV as a rudimentary object store, you can write binary image bytes directly into a key and serve them through a custom worker route such as &lt;code&gt;/img/&amp;lt;asset-name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When storing images in KV, keep in mind the platform limits regarding value sizes. For typical compressed JPEGs or WebP thumbnails, individual images easily fit well within the free-tier value cap. When a request hits your public route, the worker fetches the binary payload from KV and responds with the appropriate MIME type and caching headers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  async fetch(request: Request, env: Env): Promise&amp;lt;Response&amp;gt; {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/img/')) {
      const imageName = url.pathname.split('/')[2];
      const imageBuffer = await env.IMAGE_KV.get(imageName, 'arrayBuffer');
      
      if (!imageBuffer) {
        return new Response('Not found', { status: 404 });
      }
      
      return new Response(imageBuffer, {
        headers: {
          'Content-Type': 'image/jpeg',
          'Cache-Control': 'public, max-age=86400'
        }
      });
    }
    return new Response('Endpoint not found', { status: 404 });
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before initiating any interactions with the Threads API, verify your route manually. Run a curl command to ensure the endpoint returns an HTTP 200 status, the correct content type header, and a non-zero byte count. If your server cannot download the image reliably, Meta servers will fail as well.&lt;/p&gt;
&lt;h3 id=&quot;the-carousel-pipeline-and-processing-wait&quot;&gt;The Carousel Pipeline and Processing Wait&lt;/h3&gt;
&lt;p&gt;Publishing a carousel on Threads is not a single API call. It requires a strict sequence of container creation steps, server-side processing, and final publication. Skipping the required waiting period between container creation and publishing is the most frequent point of failure.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/designing-a-reliable-serverless-ai-publishing-workflow/&quot;&gt;publishing workflow&lt;/a&gt; consists of four distinct phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Item Containers&lt;/strong&gt;: Create an individual media container for each image in your carousel, passing the public worker URL in the request parameters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processing Gap&lt;/strong&gt;: Wait for Meta to download, validate, and process each image server-side. Publishing immediately after container creation results in processing errors because the remote servers have not finished ingesting the assets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Carousel Parent&lt;/strong&gt;: Create a parent container that references the child item container IDs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publishing&lt;/strong&gt;: Trigger the final publish action on the parent container to push the carousel live to your profile.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because status field names and readiness indicators can vary across API versions, implementing a reliable time-based buffer of roughly sixty seconds after container creation provides a pragmatic safety margin before executing the final publish request.&lt;/p&gt;
&lt;h3 id=&quot;managing-generative-visual-quality&quot;&gt;Managing Generative Visual Quality&lt;/h3&gt;
&lt;p&gt;When your workflow involves generating visuals programmatically before publishing them in a social media carousel, text rendering remains an ongoing challenge. Modern image generation models frequently attempt to render unintended words, letters, or gibberish characters into visuals unless explicitly instructed otherwise.&lt;/p&gt;
&lt;p&gt;Even when prompt instructions explicitly state to avoid text entirely, stochastic models occasionally leak artifacts. Incorporating a human review gate into your publishing loop ensures that every generated asset is visually inspected before it enters a carousel container. Automated pipelines should handle byte storage, container creation, and API orchestration, while human verification safeguards final quality.&lt;/p&gt;
&lt;h3 id=&quot;summary-of-worker-hosted-carousel-publishing&quot;&gt;Summary of Worker-Hosted Carousel Publishing&lt;/h3&gt;
&lt;p&gt;By leveraging Cloudflare KV as a lightweight storage layer and enforcing a strict processing delay between container creation steps, you can publish Threads carousels entirely from within a serverless worker. This approach removes the need for external object storage buckets while maintaining full control over asset delivery and visual quality.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30314/publishing-threads-carousels-from-worker-hosted-images</guid>
<pubDate>Thu, 08 Oct 2026 18:41:04 +0000</pubDate>
</item>
<item>
<title>Preview Cross-System Data Changes Before Sync</title>
<link>https://coderlegion.com/30272/preview-cross-system-data-changes-before-sync</link>
<description>&lt;p&gt;A sync button can hide a destructive decision. When a spreadsheet and a backend database disagree because rows were added, removed, edited, duplicated, or marked for deletion, starting an automatic write without checking the shape of those differences risks corrupting data. Users need to know whether the selected source of truth is safe before any changes are applied.&lt;/p&gt;
&lt;p&gt;To solve this, a preflight check must calculate a read-only diff first, require one explicit source of truth, block ambiguous keys, and verify the sync by recalculating the diff afterward. This guide explores how to build a reliable preflight check for cross-system data integration using aggregate difference counts and directional guards.&lt;/p&gt;
&lt;h3 id=&quot;why-a-sync-button-needs-a-preflight&quot;&gt;Why a Sync Button Needs a Preflight&lt;/h3&gt;
&lt;p&gt;Automatic synchronization assumes that both sides agree on identifiers and state. In practice, networks fail, users edit spreadsheets offline, and backend records are updated concurrently. If an application blindly pushes or pulls without an inspection phase, it can overwrite valid updates or propagate duplicate keys.&lt;/p&gt;
&lt;p&gt;A preflight stage separates observation from mutation. By inspecting both sides without writing, the system computes an aggregate preview of the state gap. This gives operators visibility into what will change before they commit to an action.&lt;/p&gt;
&lt;h3 id=&quot;stable-keys-and-row-signatures&quot;&gt;Stable Keys and Row Signatures&lt;/h3&gt;
&lt;p&gt;Comparing data across systems requires reliable identification. Row positions in a spreadsheet are notoriously fragile because sorting or inserting a row shifts every subsequent index. Integrations must rely on stable business keys rather than physical row numbers.&lt;/p&gt;
&lt;p&gt;In addition to stable keys, systems need canonical signatures to detect when shared rows have changed. A signature can be a cryptographic hash or a concatenated string of critical fields. If the key matches between &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/google-sheets-android-backend/&quot;&gt;Google Sheets&lt;/a&gt; and the database, but the signature differs, the record has been modified.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;stable_key&quot;: &quot;user_10293&quot;,
  &quot;sheets_signature&quot;: &quot;a1b2c3d4&quot;,
  &quot;db_signature&quot;: &quot;e5f6g7h8&quot;,
  &quot;status&quot;: &quot;changed&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;classifying-missing-changed-duplicate-and-deleted-rows&quot;&gt;Classifying Missing, Changed, Duplicate, and Deleted Rows&lt;/h3&gt;
&lt;p&gt;A comprehensive preview categorizes discrepancies into distinct buckets so users can assess the scope of a sync operation. The underlying service should evaluate several entity groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keys that exist only in the spreadsheet.&lt;/li&gt;
&lt;li&gt;Keys that exist only in the database.&lt;/li&gt;
&lt;li&gt;Shared keys whose canonical signatures differ.&lt;/li&gt;
&lt;li&gt;Duplicate keys appearing on either side.&lt;/li&gt;
&lt;li&gt;Pending deletions flagged by business logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This implementation evidence relies on aggregate counts rather than a field-by-field diff. The preview informs the user how many records are affected, but it does not attempt to display every mutated attribute in a complex UI.&lt;/p&gt;
&lt;h3 id=&quot;choosing-a-single-source-of-truth&quot;&gt;Choosing a Single Source of Truth&lt;/h3&gt;
&lt;p&gt;When systems diverge, automatic two-way merging often fails because conflict resolution requires domain context that automated scripts lack. If duplicate keys or changed common rows are present, a two-way mode becomes ambiguous and should be rejected.&lt;/p&gt;
&lt;p&gt;Instead, the integration must force an explicit choice. The user or operator selects either the spreadsheet or the application database as the authoritative source of truth for that run. Once an authority is chosen, the sync operation flows in a single direction, eliminating merge conflicts.&lt;/p&gt;
&lt;h3 id=&quot;confirming-safely-and-detecting-stale-previews&quot;&gt;Confirming Safely and Detecting Stale Previews&lt;/h3&gt;
&lt;p&gt;A safe run requires an active preview and refuses concurrent execution. If another sync is already running, or if the preview has not been generated, the system blocks the write request.&lt;/p&gt;
&lt;p&gt;Process memory often holds these preview objects for simplicity. However, keeping previews in memory introduces a race condition if the underlying data changes between the time the preview is generated and the time the user clicks confirm. A production integration must expire previews after a short duration or bind them to source fingerprints, rejecting stale plans to prevent unintended overwrites.&lt;/p&gt;
&lt;h3 id=&quot;post-sync-convergence-checks&quot;&gt;Post-Sync Convergence Checks&lt;/h3&gt;
&lt;p&gt;Applying a sync direction is only half the workflow. To ensure the operation succeeded, the service must immediately recompute the differences after the write completes.&lt;/p&gt;
&lt;p&gt;If the new preview shows zero discrepancies, the system reports full convergence. If differences remain due to validation errors or partial failures, the service reports a partial result rather than assuming success.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Cross-system synchronization requires discipline to prevent data loss. By inspecting changes in a read-only preflight, relying on stable keys, blocking ambiguous states, and verifying convergence after execution, applications can bridge external tools like spreadsheets and internal databases safely.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30272/preview-cross-system-data-changes-before-sync</guid>
<pubDate>Thu, 08 Oct 2026 12:04:12 +0000</pubDate>
</item>
<item>
<title>Measuring Scheduled Job Reliability</title>
<link>https://coderlegion.com/30245/measuring-scheduled-job-reliability</link>
<description>&lt;p&gt;A scheduled workflow can start successfully, finish with no eligible work, or commit an article that is not yet live. Counting all three as successes turns a reliability percentage into a trigger counter. Long-running and manually triggered executions also distort the denominator unless they are labeled separately. The useful question is not simply whether the scheduler fired. It is whether the promised user-visible result became available, and how often that happened across scheduled opportunities.&lt;/p&gt;
&lt;h3 id=&quot;defining-the-denominator-and-success-event&quot;&gt;Defining the Denominator and Success Event&lt;/h3&gt;
&lt;p&gt;To calculate an honest reliability rate, you must first define what constitutes an opportunity. If a cron trigger fires every hour, every one of those scheduled slots is a denominator entry. A slot where the system finds no work to process is still an opportunity. Omitting empty slots from the denominator quietly inflates your success percentage. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-viewmodel-partial-success-refresh-failure/&quot;&gt;Viewmodel Partial Success Refresh Failure&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, tie success to the final user-visible outcome rather than an intermediate API response. If a serverless function initiates a workflow that commits data to a database, success requires the record to be queryable at its canonical public endpoint. If the database write succeeds but a subsequent webhook notification fails, the core publishing action still succeeded. Separating core delivery from secondary notifications prevents false failures from dragging down your reliability metric. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/designing-a-reliable-serverless-ai-publishing-workflow/&quot;&gt;Designing A Serverless Ai Publishing Workflow&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;handling-manual-triggers-and-dry-runs&quot;&gt;Handling Manual Triggers and Dry Runs&lt;/h3&gt;
&lt;p&gt;Operational workflows often require manual intervention, testing, or dry runs. These paths must be isolated from automated metrics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automated scheduled runs populate the primary reliability denominator.&lt;/li&gt;
&lt;li&gt;Manual live runs are tracked separately to measure operator-initiated actions without polluting automated cron statistics.&lt;/li&gt;
&lt;li&gt;Dry runs are excluded entirely because they make no production attempt and produce no live outcome.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Failing to separate these paths leads to misleading reports where a heavy day of manual testing masks an underlying cron failure or vice versa.&lt;/p&gt;
&lt;h3 id=&quot;idempotent-ledger-schema-and-state-tracking&quot;&gt;Idempotent Ledger Schema and State Tracking&lt;/h3&gt;
&lt;p&gt;Using a versioned SQLite database like Cloudflare D1 allows you to record execution states reliably. Each scheduled slot receives a deterministic, idempotent run identifier. When the scheduler triggers, the application writes an initial RUNNING marker before starting any asynchronous work. If starting the workflow fails outright, that same row is finalized as a miss. A marker that remains open past a bounded timeout is also classified as a failed run.&lt;/p&gt;
&lt;p&gt;Below is an example schema migration demonstrating how to store these execution states while keeping records compact and sanitized.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE job_reliability_ledger (
    run_id TEXT PRIMARY KEY,
    scheduled_time TEXT NOT NULL,
    execution_type TEXT NOT NULL,
    status TEXT NOT NULL,
    blocker_category TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_scheduled_time ON job_reliability_ledger(scheduled_time);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When writing updates, use an upsert strategy or check the existing status to ensure replaying a terminal update does not create duplicate rows or alter a verified success timestamp.&lt;/p&gt;
&lt;h3 id=&quot;operational-reporting-considerations&quot;&gt;Operational Reporting Considerations&lt;/h3&gt;
&lt;p&gt;When displaying reliability metrics to an engineering team, context matters as much as the raw percentage. Always state the data-collection start date next to every rate so partial history is not mistaken for lifetime performance. Calculate summaries using the audience local timezone rather than forcing UTC conversion on operations teams. Store only sanitized blocker categories, such as timeout or validation error, instead of raw provider replies or secrets.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-on-measuring-reliability&quot;&gt;Conclusion on Measuring Reliability&lt;/h3&gt;
&lt;p&gt;Accurate reliability measurement requires discipline around what you count as an attempt and what you verify as a success. By separating scheduled opportunities from manual tests, anchoring your denominator to every cron slot, and verifying real user-visible outcomes, you build an operational view that reflects actual system health rather than optimistic trigger counts.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30245/measuring-scheduled-job-reliability</guid>
<pubDate>Thu, 08 Oct 2026 09:04:08 +0000</pubDate>
</item>
<item>
<title>📊 Can Kafka Handle Every Integration Use Case? Understanding the Architectural Trade-offs</title>
<link>https://coderlegion.com/30143/can-kafka-handle-every-integration-use-case-understanding-the-architectural-trade-offs</link>
<description>&lt;p&gt;Compared with traditional queue-based messaging platforms, Kafka is primarily designed for event streaming. It excels at distributing data from producers to one or many consumers at massive scale. However, unlike many traditional messaging platforms, Kafka intentionally minimizes broker-side routing and content-based filtering capabilities. Message selection is typically achieved through topic design, partitioning strategies, stream processing, or consumer-side filtering rather than broker-managed routing.&lt;/p&gt;
&lt;p&gt;This raises an important architectural question:&lt;/p&gt;
&lt;p&gt;❓ Can Kafka be used for all integration and messaging use cases?&lt;/p&gt;
&lt;p&gt;The answer is yes, but organizations should understand and address several architectural and operational challenges before adopting Kafka as a universal messaging platform.&lt;/p&gt;
&lt;p&gt; 📈 1. Scaling Considerations&lt;/p&gt;
&lt;p&gt;Kafka's distributed architecture allows horizontal scaling by adding additional brokers to support increasing workloads and client connections. However, consumer scalability is fundamentally tied to the number of partitions within a topic.&lt;/p&gt;
&lt;p&gt;Key considerations:&lt;/p&gt;
&lt;p&gt;✅ One partition can be consumed by only one consumer within the same consumer group.&lt;br&gt;
✅ One consumer can consume multiple partitions.&lt;/p&gt;
&lt;p&gt; ⚠️ The maximum parallelism of a consumer group is limited by the partition count.&lt;/p&gt;
&lt;p&gt;As a result, partitioning strategy becomes a critical design decision. Under-partitioning can limit future scalability, while over-partitioning may introduce unnecessary operational overhead, metadata overhead, replication traffic, and recovery times, making partition planning a long-term architectural decision.&lt;/p&gt;
&lt;p&gt;Partition planning should be treated as a long-term architectural decision, with future scalability requirements carefully considered during initial design.&lt;/p&gt;
&lt;p&gt;🛡️ 2. Reliability and Data Durability&lt;/p&gt;
&lt;p&gt;Kafka provides flexible durability options through replication and acknowledgment settings.&lt;/p&gt;
&lt;p&gt;Publishers can choose different acknowledgment modes, balancing performance against durability requirements.&lt;/p&gt;
&lt;p&gt;This flexibility offers advantages, but it also introduces governance challenges:&lt;/p&gt;
&lt;p&gt;⚠️ Different applications may adopt different reliability settings.&lt;br&gt;
⚠️ Organizations need governance and standards to ensure mission-critical applications meet reliability requirements.&lt;/p&gt;
&lt;p&gt;In addition, replication significantly reduces the risk of data loss, but durability guarantees ultimately depend on replication factors, acknowledgment settings, in-sync replica requirements, and storage configuration. Kafka's design prioritizes minimizing the probability of data loss while maintaining high throughput. Kafka achieves durable storage through persistent logs and replication. While Kafka leverages the operating system page cache to maximize throughput, durability ultimately depends on replication, acknowledgment settings, storage configuration, and broker availability. These mechanisms provide strong durability characteristics, although architects should carefully evaluate configurations based on business requirements.&lt;/p&gt;
&lt;p&gt;Architects should therefore carefully assess durability requirements and select appropriate replication, acknowledgment, and storage configurations.&lt;/p&gt;
&lt;p&gt; 🔄 3. Consumer Group Rebalancing&lt;/p&gt;
&lt;p&gt;Consumer groups provide scalability and high availability, but they come with an operational cost.&lt;/p&gt;
&lt;p&gt;Whenever a consumer joins, leaves, fails, or recovers within a consumer group, Kafka may initiate a rebalance process.&lt;/p&gt;
&lt;p&gt;During rebalancing:&lt;/p&gt;
&lt;p&gt;Partition ownership is reassigned&lt;br&gt;
Consumer-to-partition mappings are recalculated.&lt;br&gt;
Message consumption may be temporarily paused.&lt;/p&gt;
&lt;p&gt;For dynamic cloud-native applications that frequently scale in and out, excessive consumer group rebalancing can disrupt message processing and increase latency. By default, Kafka does not guarantee partition ownership stability during rebalances. To minimize disruption, consider using the Cooperative Sticky Assignor or Static Membership.&lt;/p&gt;
&lt;p&gt;⚙️ 4. Group Coordinator Bottlenecks&lt;/p&gt;
&lt;p&gt;Kafka's data plane is highly scalable and typically handles large volumes of producers and consumers efficiently.&lt;/p&gt;
&lt;p&gt;However, consumer groups rely on a control-plane component known as the Group Coordinator.&lt;/p&gt;
&lt;p&gt;The Group Coordinator is responsible for:&lt;/p&gt;
&lt;p&gt;📌 Managing group membership&lt;br&gt;
📌 Handling consumer rebalancing&lt;br&gt;
📌 Processing heartbeats&lt;br&gt;
📌 Managing offset commits and retrievals&lt;br&gt;
📌 Enforcing group protocols and coordination logic&lt;/p&gt;
&lt;p&gt;In very large deployments with many consumer groups and frequent rebalances, Group Coordinator load can become an important operational consideration. When many consumer groups rebalance simultaneously, Group Coordinator load may increase significantly, potentially leading to longer rebalance times and higher consumer recovery latency.&lt;/p&gt;
&lt;p&gt;🔀 5. Message Distribution Models&lt;/p&gt;
&lt;p&gt;Traditional message brokers and Kafka use fundamentally different message distribution approaches.&lt;/p&gt;
&lt;p&gt;Message Bus Model&lt;/p&gt;
&lt;p&gt;✅ Multiple consumers can subscribe to the same non-exclusive queue.&lt;br&gt;
✅ The broker distributes messages among available consumers.&lt;br&gt;
✅ Consumer scaling can be automated using tools such as Kubernetes Event-Driven Autoscaling (KEDA) based on queue depth.&lt;/p&gt;
&lt;p&gt; Kafka Model&lt;/p&gt;
&lt;p&gt;✅ Messages are distributed across partitions.&lt;br&gt;
✅ Each partition is processed by only one consumer within a consumer group.&lt;br&gt;
✅ Scaling is achieved through adding partitions and consumers.&lt;/p&gt;
&lt;p&gt;However:&lt;/p&gt;
&lt;p&gt;⚠️ Message distribution occurs at the partition level.&lt;br&gt;
⚠️ New messages continue to be sent to assigned partitions regardless of individual consumer performance.&lt;/p&gt;
&lt;p&gt;If one consumer becomes significantly slower than others, backlog accumulation can occur on specific partitions, resulting in uneven processing rates across the consumer group.&lt;/p&gt;
&lt;p&gt;Kafka has introduced the Share Group model (KIP-932), which aims to provide queue-like work-distribution semantics similar to traditional non-exclusive queues. However, the feature is still evolving and has not yet reached the same maturity level as established enterprise messaging platforms.&lt;/p&gt;
&lt;p&gt;🌐 6. Network Bandwidth Consumption&lt;/p&gt;
&lt;p&gt;One of the key differences between Kafka and many traditional message buses is the absence of native broker-side routing and filtering.&lt;/p&gt;
&lt;p&gt;In many messaging platforms, consumers receive only messages that match specific routing rules.&lt;/p&gt;
&lt;p&gt;With Kafka:&lt;/p&gt;
&lt;p&gt;📥 Consumers typically subscribe to topics.&lt;br&gt;
📥 Kafka favors topic-based distribution. As consumer requirements diverge, additional topics, stream processing, or filtering may be needed, increasing operational overhead.&lt;br&gt;
📥 When consumer requirements diverge significantly, organizations may need additional topics, stream processing, or consumer-side filtering, potentially increasing operational complexity and network traffic.&lt;/p&gt;
&lt;p&gt;For environments with large event volumes and diverse consumer requirements, this approach may increase:&lt;/p&gt;
&lt;p&gt;📡 Network bandwidth usage&lt;br&gt;
🖥️ Consumer processing overhead&lt;/p&gt;
&lt;p&gt;Increasing the number of Kafka topics is one way to address bandwidth and scalability challenges. However, this approach can significantly increase operational overhead and application complexity. Architects should carefully evaluate topic design and event segmentation strategies to minimize unnecessary data movement and avoid excessive topic proliferation.&lt;/p&gt;
&lt;p&gt;📋7. Message Ordering vs Scalability&lt;/p&gt;
&lt;p&gt;One of Kafka's key strengths is its ability to preserve message order, but this comes with an important architectural trade-off.&lt;/p&gt;
&lt;p&gt;Kafka guarantees message ordering only within a partition.&lt;/p&gt;
&lt;p&gt;To increase processing throughput and consumer parallelism, organizations often increase the number of partitions. However, as partitions increase, maintaining a single global order across all messages becomes more difficult.&lt;/p&gt;
&lt;p&gt;📦 8. Topic and Metadata Management&lt;/p&gt;
&lt;p&gt;In very large Kafka deployments, metadata size and frequent metadata updates can increase client startup time, subscription management overhead, and cluster coordination activity. While Kafka is designed to scale efficiently, excessive topic and partition growth can create additional operational and management challenges.&lt;/p&gt;
&lt;p&gt;🎯 Final Thoughts&lt;/p&gt;
&lt;p&gt;Kafka is an exceptional event-streaming platform and has become a popular foundation for modern event-driven architecture. However, &quot;one platform for every use case&quot; is not always the optimal architectural approach.&lt;/p&gt;
&lt;p&gt;The key question is not whether Kafka can support a use case, but whether it is the most appropriate tool for that use case.&lt;/p&gt;
&lt;p&gt;Successful architecture decisions require balancing:&lt;/p&gt;
&lt;p&gt;📈 Scalability&lt;br&gt;
🛡️ Reliability&lt;br&gt;
⚙️ Operational Complexity&lt;br&gt;
🔄 Consumer Distribution Requirements&lt;br&gt;
🌐 Network Efficiency&lt;/p&gt;
&lt;p&gt;High-scale Kafka environments require strong operational observability. Consumer lag, partition imbalance, replication health, broker utilization, and rebalancing behavior must be continuously monitored to ensure reliable service operation.&lt;/p&gt;
&lt;p&gt;However, Kafka is intentionally designed with relatively simple architecture to deliver high-performance event streaming at scale. As a result, it does not provide the rich routing and message filtering capabilities commonly found in traditional message buses.&lt;/p&gt;
&lt;p&gt;Understanding these trade-offs enables architects to select the right messaging paradigm for the right business problem rather than forcing every workload into a single technology pattern.&lt;/p&gt;
&lt;p&gt;💡 Kafka can support most integration workloads, but its architectural model introduces trade-offs that architects must understand. The most successful implementations are those that align Kafka's strengths with business requirements, rather than assuming that a streaming platform is the optimal solution for every messaging scenario.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30143/can-kafka-handle-every-integration-use-case-understanding-the-architectural-trade-offs</guid>
<pubDate>Wed, 07 Oct 2026 13:39:23 +0000</pubDate>
</item>
<item>
<title>Fixing KV Propagation Delay in Media Publishing</title>
<link>https://coderlegion.com/30080/fixing-kv-propagation-delay-in-media-publishing</link>
<description>&lt;p&gt;When you build a serverless media pipeline that stores generated images in a key-value store and immediately sends those public URLs to a third-party publishing API, you will likely encounter a frustrating failure mode. Your local tests pass, your terminal curl requests return the correct image bytes with a 200 OK status, yet the publishing API responds with a cryptic media not found error. This happens because your own worker reads from the exact edge location that just wrote the key, while the platform fetcher hits a different edge node that has not received the replicated data yet. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-viewmodel-partial-success-refresh-failure/&quot;&gt;Viewmodel Partial Success Refresh Failure&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;green-locally-dead-remotely&quot;&gt;Green Locally, Dead Remotely&lt;/h3&gt;
&lt;p&gt;The most deceptive part of this issue is its misleading diagnostic feedback. When publishing fails, the natural instinct is to examine the container ordering, parameter payloads, or authentication tokens. Developers often curl the generated public URL from their own workstation or test runner, receive the correct MIME type and image bytes, and conclude that the serving layer works fine. This self-verification is a trap. Your edge succeeding proves only that one specific node has the data. It provides zero visibility into whether the global key-value fabric has synchronized across all regional datacenters.&lt;/p&gt;
&lt;h3 id=&quot;whose-edge-is-it-anyway&quot;&gt;Whose Edge Is It Anyway&lt;/h3&gt;
&lt;p&gt;Global key-value stores prioritize low-latency writes and eventual consistency. When your worker executes a put operation, the runtime confirms success once the local write succeeds. Replication to other regions happens asynchronously in the background. If a third-party platform attempts to download the asset immediately after receiving the URL, its request routes to an edge server near its own infrastructure. If that specific server has not received the update, the fetch fails. Because distributed platforms do not expose their internal replication state or routing tables, you cannot poll their edge directly to check availability. You must instead build a defensive timing model into your application code.&lt;/p&gt;
&lt;h3 id=&quot;two-waits-two-handoffs&quot;&gt;Two Waits, Two Handoffs&lt;/h3&gt;
&lt;p&gt;Solving this problem requires separating your publishing pipeline into distinct phases, each guarded by its own deliberate delay. Conflating these phases leads to brittle workarounds.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Propagation Wait&lt;/strong&gt;: After storing the asset in key-value storage, introduce a measured pause to allow global replication. This gives remote edge locations time to catch up before any external system attempts a download.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Processing Wait&lt;/strong&gt;: After the third-party platform initiates its import or container creation process, allow an additional window for the platform to process the file before you invoke the final publish command.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hardcoding these delays into your core logic makes your test suite painfully slow. Instead, make your wait durations configurable by injecting milliseconds during testing while using production-appropriate values for live deployments. Keeping these constants clearly named in your source code prevents future maintainers from accidentally optimizing them away.&lt;/p&gt;
&lt;h3 id=&quot;implementing-the-fix&quot;&gt;Implementing the Fix&lt;/h3&gt;
&lt;p&gt;To manage these handoffs safely, structure your asynchronous worker code to wait explicitly between storage and publishing steps. Here is an example configuration pattern in TypeScript that uses injected wait durations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface PublishConfig {
  propagationDelayMs: number;
  processingDelayMs: number;
  maxRetries: number;
}

async function storeAndPublishMedia(
  kv: KVNamespace,
  key: string,
  data: ArrayBuffer,
  config: PublishConfig
): Promise&amp;lt;string&amp;gt; {
  await kv.put(key, data);
  
  // Guard against global edge replication lag
  await new Promise((resolve) =&amp;gt; setTimeout(resolve, config.propagationDelayMs));
  
  const mediaId = await initiatePlatformUpload(key);
  
  // Guard against remote processing lag
  await new Promise((resolve) =&amp;gt; setTimeout(resolve, config.processingDelayMs));
  
  return await finalizePublish(mediaId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;verifying-the-publish-object&quot;&gt;Verifying the Publish Object&lt;/h3&gt;
&lt;p&gt;Never rely on your own curl command as proof of a successful publish operation. True success is defined solely by the response of the downstream publishing API. When the platform successfully fetches the asset from its local edge, processes it, and returns a valid media object with attached children or confirmation IDs, your pipeline has succeeded. If the call fails, adjust your propagation budget upward or inspect the platform error logs for download timeouts.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Eventual consistency models introduce hidden timing assumptions into modern serverless architectures. When your application hands off assets to external consumers, assuming instant global visibility will cause intermittent failures. By acknowledging edge propagation realities, separating your storage waits from your processing waits, and verifying platform objects rather than local URLs, you can make your media publishing pipelines resilient to distributed latency.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30080/fixing-kv-propagation-delay-in-media-publishing</guid>
<pubDate>Wed, 07 Oct 2026 06:02:44 +0000</pubDate>
</item>
<item>
<title>Building a High-Speed School Marketplace (Part 2): Multi-Role Security with Supabase RLS</title>
<link>https://coderlegion.com/30070/building-a-high-speed-school-marketplace-part-2-multi-role-security-with-supabase-rls</link>
<description>&lt;p&gt;&lt;em&gt;This is Part 2 of our technical series on building a high-speed, lightweight school marketplace. In Part 1: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl&quot;&gt;\[Why We Ditched Frameworks for Vanilla JS &amp;amp; PWA,\]&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl&quot;&gt;1&lt;/a&gt; we discussed how we optimized the frontend for budget mobile devices. Today, we look at the backend architecture.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When planning the architecture for &lt;strong&gt;Aksara-Mart&lt;/strong&gt;—our digital marketplace for vocational school canteens and cooperatives—we faced a fundamental infrastructure question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Do we really need to build, maintain, and scale a traditional custom backend server (like Node.js/Express, Python, or Go) just to shuttle data between the browser and the database?&lt;/strong&gt;&lt;br&gt;
For a school environment, maintaining a traditional backend comes with real trade-offs:&lt;br&gt;
1.&lt;strong&gt;Infrastructure &amp;amp; Hosting Overhead&lt;/strong&gt;: A dedicated server needs 24/7 uptime monitoring, memory management, and scaling configuration for peak recess traffic.&lt;br&gt;
2.&lt;strong&gt;Maintenance Burden&lt;/strong&gt;: API routes, middleware controllers, and ORM abstractions require continuous patching and maintenance.&lt;br&gt;
3.&lt;strong&gt;Unnecessary Latency&lt;/strong&gt;: Every simple query (browsing the lunch menu, checking an order status) has to make an extra hop through an intermediate API server layer.&lt;br&gt;
We wanted an architecture that could &lt;strong&gt;minimize backend server workload&lt;/strong&gt; while keeping running costs close to zero.&lt;br&gt;
That led us to &lt;strong&gt;Supabase&lt;/strong&gt;. By allowing our frontend JavaScript to communicate directly with PostgreSQL via auto-generated APIs and WebSockets, we could offload the bulk of routine read queries directly to the client.&lt;br&gt;
However, letting client-side JavaScript query a database directly sounds like a security nightmare—unless you have a bulletproof authorization layer built into the database itself.&lt;br&gt;
That is where &lt;strong&gt;PostgreSQL Row Level Security (RLS)&lt;/strong&gt; becomes the cornerstone of the entire system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Architecture: Direct Client Queries Without the Middleman&lt;br&gt;
In a traditional web application, every user request passes through three layers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[ Browser (Client) ] ──▶ [ Custom API Server (Express/Node) ] ──▶ [ Database ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The API server spends most of its CPU cycles doing mundane tasks: authenticating tokens, verifying if a user is allowed to read a row, running the query, and formatting JSON.&lt;br&gt;
With Supabase and PostgreSQL RLS, we simplified the architecture into a direct, high-performance two-tier model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[ Browser (Vanilla JS Client) ] ──────────────▶ [ Supabase PostgreSQL + RLS ]
•Direct catalog SELECT                         - Kernel-level row authorization
•Direct order history lookup                   - Instant query evaluation (&amp;lt; 3ms)
•Real-time WebSocket subscriptions             - Atomic RPC stored procedures
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Advantages:&lt;br&gt;
•&lt;strong&gt;Zero Backend Server Maintenance&lt;/strong&gt;: No intermediate server processes to crash, restart, or run out of memory during a rush.&lt;br&gt;
•&lt;strong&gt;Minimal Compute Overhead&lt;/strong&gt;: PostgreSQL handles filtering and JSON serialization natively in C, which is vastly faster than parsing objects in Node.js.&lt;br&gt;
•&lt;strong&gt;Direct Client Queries&lt;/strong&gt;: Frontend JavaScript can fetch exactly the columns it needs, when it needs them, directly from the client.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Core Problem: Securing Direct Database Access&lt;br&gt;
If frontend JavaScript talks directly to the database using a public API key, what prevents a student from querying other users' private orders or inspecting canteen cost margins?&lt;br&gt;
This is where &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; comes in.&lt;br&gt;
Instead of writing authorization logic in backend middleware (&lt;code&gt;if (req.user.id !== order.user_id) return res.status(403)&lt;/code&gt;), the authorization rules are compiled directly into PostgreSQL table definitions.&lt;br&gt;
•Without RLS: A client query &lt;code&gt;supabase.from('orders').select('*')&lt;/code&gt; returns every order in the database.&lt;br&gt;
•With RLS: PostgreSQL automatically injects an internal filter into the query plan:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Automatically enforced by PostgreSQL on every query
WHERE email_customers = auth.jwt() -&amp;gt;&amp;gt; 'email'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even if a user inspects the network tab and writes a custom script asking for all records, the database engine guarantees they only receive the rows they are explicitly permitted to see.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing Roles Across the School Ecosystem&lt;br&gt;
A school marketplace serves several distinct groups with different permissions:&lt;br&gt;
| Role | Access Scope | How Queries Are Handled |&lt;br&gt;
| :--- | :--- | :--- |&lt;br&gt;
| &lt;strong&gt;&lt;code&gt;siswa&lt;/code&gt; (Students)&lt;/strong&gt; | Browse active products, view own order history. | Direct client &lt;code&gt;SELECT&lt;/code&gt; constrained by RLS. |&lt;br&gt;
| &lt;strong&gt;&lt;code&gt;guru&lt;/code&gt; (Teachers)&lt;/strong&gt; | Same as students, with priority fulfillment flags. | Direct client &lt;code&gt;SELECT&lt;/code&gt; constrained by RLS. |&lt;br&gt;
| &lt;strong&gt;&lt;code&gt;kasir&lt;/code&gt; (Cashiers)&lt;/strong&gt; | View all active incoming orders, print receipts. | Direct client &lt;code&gt;SELECT&lt;/code&gt; with staff privileges; no direct price/stock editing. |&lt;br&gt;
| &lt;strong&gt;&lt;code&gt;manajer&lt;/code&gt; (Managers)&lt;/strong&gt; | Manage catalog items, update stock, view revenue. | Full CRUD permissions on product tables via RLS. |&lt;br&gt;
| &lt;strong&gt;&lt;code&gt;admin&lt;/code&gt; (System Admins)&lt;/strong&gt; | System-wide configuration and role management. | Full administrative access. |&lt;br&gt;
The &lt;code&gt;admin_role&lt;/code&gt; Table&lt;br&gt;
To keep role checks clean and reliable, we maintain a dedicated role table in PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE public.admin_role (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
);
ALTER TABLE public.admin_role ENABLE ROW LEVEL SECURITY;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Practical Gotchas We Addressed:&lt;br&gt;
4.&lt;strong&gt;Case-Insensitive Emails&lt;/strong&gt;: Mobile keyboards often capitalize the first letter of an email (&lt;code&gt;*Emails are not allowed*&lt;/code&gt;). Always use &lt;code&gt;lower(trim(email))&lt;/code&gt; in policies to prevent login lockouts.&lt;br&gt;
5.&lt;strong&gt;Dual-Spelling Tolerance&lt;/strong&gt;: In bilingual environments, staff may enter roles in English or Indonesian (&lt;code&gt;manager&lt;/code&gt; vs &lt;code&gt;manajer&lt;/code&gt;, &lt;code&gt;cashier&lt;/code&gt; vs &lt;code&gt;kasir&lt;/code&gt;). We check role aliases with &lt;code&gt;IN ('manager', 'manajer', 'cashier', 'kasir')&lt;/code&gt; so minor spelling differences don't break authentication.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Separation of Concerns: Direct Reads vs Server-Side Writes&lt;br&gt;
Allowing frontend JavaScript to query the database directly works brilliantly for &lt;strong&gt;reads&lt;/strong&gt;, but transactional &lt;strong&gt;writes&lt;/strong&gt; require a different boundary.&lt;br&gt;
A. Direct Client Reads (Safe via RLS)&lt;br&gt;
Frontend JavaScript directly reads from the database for:&lt;br&gt;
•&lt;strong&gt;The product catalog&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Direct client query in skripmart.js
const { data: products } = await supabase
.from('product_list')
.select('id, name, price, stock, image_url')
.eq('status', 'active');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RLS ensures that wholesale costs (&lt;code&gt;modal_price&lt;/code&gt;) and archived items are filtered out before data leaves PostgreSQL.&lt;br&gt;
•&lt;strong&gt;Order tracking&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Customers only receive their own orders
const { data: myOrders } = await supabase
.from('orders')
.select('*')
.order('created_at', { ascending: false });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;B. Controlled Server-Side Writes (Via Database RPC)&lt;br&gt;
While reading data directly from JavaScript is safe, &lt;strong&gt;creating orders and modifying stock should never happen via direct client &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ❌ ANTI-PATTERN: Never allow direct client inserts for financial transactions
await supabase.from('orders').insert({
total_price: clientCalculatedTotal, // Client could tamper with this value!
items: cartItems
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we allowed direct client inserts on the &lt;code&gt;orders&lt;/code&gt; table, a user could theoretically alter prices or bypass stock limits.&lt;br&gt;
Instead, we follow a simple architectural rule:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Frontend JavaScript reads data directly using RLS.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Frontend JavaScript writes transactions via PostgreSQL Stored Procedures (RPC).&lt;/strong&gt;&lt;br&gt;
When a student checks out, the client invokes an RPC procedure (&lt;code&gt;v2_create_order&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ✅ SECURE: Transaction logic runs atomically inside PostgreSQL
const { data, error } = await supabase.rpc('v2_create_order', {
p_customer_name: userProfile.name,
p_delivery_type: deliveryOption,
p_items: cartPayload
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside this procedure, the database recalculates prices from the official catalog, checks remaining stock, decrements inventory, and creates the order records in a single atomic transaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What We Gained from This Architecture&lt;br&gt;
By offloading query processing directly to the client and database engine:&lt;br&gt;
6.&lt;strong&gt;Zero Backend Maintenance&lt;/strong&gt;: We do not maintain or pay for a custom Node.js/Express server layer.&lt;br&gt;
7.&lt;strong&gt;Sub-3ms Authorization Overhead&lt;/strong&gt;: RLS policy checks happen natively within PostgreSQL's query optimizer with virtually no latency penalty.&lt;br&gt;
8.&lt;strong&gt;High Reliability During Traffic Spikes&lt;/strong&gt;: When hundreds of students browse the menu simultaneously during recess, Supabase and Cloudflare serve the requests directly without choking an intermediate web server.&lt;br&gt;
9.&lt;strong&gt;Complete Data Privacy&lt;/strong&gt;: Every customer and staff member only receives data that belongs to their assigned role.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What's Next in Part 3?&lt;br&gt;
With direct reads secured by RLS and client-side queries running smoothly, we still had to solve one critical concurrency problem:&lt;br&gt;
During the 15-minute school break, &lt;strong&gt;dozens of students click &quot;Checkout&quot; at the exact same millisecond for the last few items in the canteen&lt;/strong&gt;.&lt;br&gt;
How do we prevent &lt;strong&gt;race conditions and overselling&lt;/strong&gt; without a heavy application server?&lt;br&gt;
&lt;strong&gt;In Part 3: Atomic Checkouts &amp;amp; Zero Overselling with PostgreSQL RPC&lt;/strong&gt;, we will break down how we implemented &lt;strong&gt;pessimistic row locking (&lt;code&gt;FOR UPDATE&lt;/code&gt;)&lt;/strong&gt; inside PostgreSQL to handle high-concurrency checkout spikes in &lt;strong&gt;under 42 milliseconds&lt;/strong&gt;!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30070/building-a-high-speed-school-marketplace-part-2-multi-role-security-with-supabase-rls</guid>
<pubDate>Wed, 07 Oct 2026 04:15:37 +0000</pubDate>
</item>
<item>
<title>Bounded LLM Fallback Chains</title>
<link>https://coderlegion.com/30065/bounded-llm-fallback-chains</link>
<description>&lt;p&gt;When a primary AI provider experiences an outage or a rate limit in the middle of the night, applications often suffer from unexpected downtime. The common reaction is to implement a multi-provider failover chain. However, standard fallback loops create a different kind of emergency: runaway billing events. Unbounded retries across multiple models can exhaust available budgets in minutes during a regional outage.&lt;/p&gt;
&lt;p&gt;To prevent failover mechanisms from becoming cost incidents, systems need strict boundaries. A production-ready fallback tier must exhaust the primary provider completely before switching, enforce a strict per-run call cap, treat billing errors as permanent stop signs, and exclude fallbacks from routine dry runs.&lt;/p&gt;
&lt;h3 id=&quot;the-cost-of-unbounded-retries&quot;&gt;The Cost of Unbounded Retries&lt;/h3&gt;
&lt;p&gt;Chaining multiple language model providers without constraints introduces several hidden failure modes. Without a defined architecture, an outage triggers cascading problems.&lt;/p&gt;
&lt;p&gt;Unbounded failover burns capital rapidly. Each fallback call consumes tokens or credits. If a system cascades through four expensive models without a cap, a single failing background job can drain a monthly budget. Furthermore, applications often retry quota-exceeded and billing-restricted errors. These responses are permanent verdicts from the provider, not transient network glitches. Retrying them only multiplies the failure rate and increases error logs.&lt;/p&gt;
&lt;p&gt;Another subtle issue involves testing and dry runs. If health checks or dry runs execute the entire fallback path, they consume the scarce fallback quota that production systems rely on during actual emergencies. Finally, silent provider drift occurs when a primary model fails permanently, but the system quietly runs on the expensive fallback tier for weeks without alerting the engineering team.&lt;/p&gt;
&lt;h3 id=&quot;designing-the-provider-tier-contract&quot;&gt;Designing the Provider Tier Contract&lt;/h3&gt;
&lt;p&gt;To solve these challenges, an LLM routing layer must enforce specific operational constraints. &lt;/p&gt;
&lt;p&gt;First, the system must exhaust tier one completely before initiating tier two. Fallback models should never run in parallel or preemptively. Second, total fallback calls per execution run require a hard cap, such as a maximum of four attempts. This bounds the worst-case financial exposure regardless of how many models exist in the registry. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/audit-macos-system-data-before-deleting-developer-caches/&quot;&gt;Audit Macos System Data Before Deleting&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Third, error classification dictates the next step. Infrastructure issues like 503 service unavailable or gateway timeouts advance the chain. Conversely, quota exhaustion, invalid API keys, and paid-only model restrictions act as immediate stop signs. The runner halts execution and records the exact reason rather than proceeding to the next provider.&lt;/p&gt;
&lt;p&gt;Fourth, dry runs must execute against the primary path only. Fallback bindings remain dormant during routine checks unless an explicit integration test flag is provided. Fifth, every successful response must record the provider and model name in the output metadata. This attribution makes silent fallback immediately visible in monitoring dashboards.&lt;/p&gt;
&lt;h3 id=&quot;implementing-a-bounded-fallback-runner&quot;&gt;Implementing a Bounded Fallback Runner&lt;/h3&gt;
&lt;p&gt;Below is a TypeScript example demonstrating a bounded fallback executor with error classification and call limits.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type ProviderResult = {
  content: string;
  provider: string;
  model: string;
};

enum ErrorType {
  Transient,
  PermanentBilling,
}

interface Provider {
  name: string;
  model: string;
  execute(prompt: string): Promise&amp;lt;string&amp;gt;;
}

function classifyError(err: any): ErrorType {
  if (err.status === 429 || err.code === 'QUOTA_EXCEEDED') {
    return ErrorType.PermanentBilling;
  }
  return ErrorType.Transient;
}

async.executeWithFallback = async function(
  primary: Provider,
  fallbacks: Provider[],
  prompt: string,
  maxFallbackCalls: number = 2,

): Promise&amp;lt;ProviderResult&amp;gt; {
  try {
    const content = await primary.execute(prompt);
    return { content, provider: primary.name, model: primary.model };
  } catch (primaryError) {
    if (classifyError(primaryError) === ErrorType.PermanentBilling) {
      throw new Error('Primary failed with billing error; halting chain.');
    }

    let callsMade = 0;
    for (const fb of fallbacks) {
      if (callsMade &amp;gt;= maxFallbackCalls) {
        break;
      }
      callsMade++;

      try {
        const content = await fb.execute(prompt);
        return { content, provider: fb.name, model: fb.model };
      } catch (fbError) {
        if (classifyError(fbError) === ErrorType.PermanentBilling) {
          throw new Error('Fallback failed with billing error; halting chain.');
        }
      }
    }

    throw new Error('All providers exhausted without successful execution.');
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;verifying-fallback-behavior-safely&quot;&gt;Verifying Fallback Behavior Safely&lt;/h3&gt;
&lt;p&gt;Proving that a fallback chain works without triggering a real production outage requires targeted unit and integration tests. Tests should simulate a complete primary failure and assert that the fallback tier engages exactly once. Additional test cases must verify that dry runs never invoke the fallback binding unless explicitly enabled, that hitting the per-run call cap throws an exception instead of executing unauthorized calls, and that billing-class errors halt the chain immediately with a documented stop reason.&lt;/p&gt;
&lt;h3 id=&quot;architectural-conclusion&quot;&gt;Architectural Conclusion&lt;/h3&gt;
&lt;p&gt;Reliable AI infrastructure requires treating failover as a finite budget rather than an open-ended loop. By classifying provider errors, enforcing hard call limits, and maintaining strict attribution, engineering teams can build resilient multi-provider runtimes that withstand outages without risking financial exposure.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30065/bounded-llm-fallback-chains</guid>
<pubDate>Wed, 07 Oct 2026 03:03:52 +0000</pubDate>
</item>
<item>
<title>Backend Architecture Starts With Workflows, Not Frameworks</title>
<link>https://coderlegion.com/29972/backend-architecture-starts-with-workflows-not-frameworks</link>
<description>&lt;p&gt;When teams begin designing a backend, technology discussions often arrive first.&lt;br&gt;
Node.js or Python?&lt;br&gt;
PostgreSQL or MongoDB?&lt;br&gt;
Monolith or microservices?&lt;/p&gt;
&lt;p&gt;Those choices matter, but the architecture usually becomes clearer if you start somewhere else:&lt;br&gt;
the workflow.&lt;/p&gt;
&lt;h3 id=&quot;follow-one-operation-from-beginning-to-end&quot;&gt;Follow One Operation From Beginning To End&lt;/h3&gt;
&lt;p&gt;Suppose an e-commerce customer places an order.&lt;/p&gt;
&lt;p&gt;The backend may need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticate the customer&lt;/li&gt;
&lt;li&gt;Check inventory&lt;/li&gt;
&lt;li&gt;Validate pricing&lt;/li&gt;
&lt;li&gt;Create an order&lt;/li&gt;
&lt;li&gt;Contact the payment provider&lt;/li&gt;
&lt;li&gt;Update inventory&lt;/li&gt;
&lt;li&gt;Trigger fulfillment&lt;/li&gt;
&lt;li&gt;Send confirmation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The architecture question is not simply: &lt;br&gt;
Which framework should execute this?&lt;/p&gt;
&lt;p&gt;A better question is:&lt;br&gt;
Which steps must succeed together, which can happen later, and what happens when one fails?&lt;/p&gt;
&lt;p&gt;That immediately exposes important design requirements.&lt;/p&gt;
&lt;h3 id=&quot;define-data-ownership&quot;&gt;Define Data Ownership&lt;/h3&gt;
&lt;p&gt;Modern applications frequently use multiple external platforms.&lt;/p&gt;
&lt;p&gt;Customer information might appear in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The main application database&lt;/li&gt;
&lt;li&gt;A CRM&lt;/li&gt;
&lt;li&gt;A payment platform&lt;/li&gt;
&lt;li&gt;An analytics system&lt;/li&gt;
&lt;li&gt;A support platform&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without explicit ownership rules, synchronization becomes unpredictable.&lt;br&gt;
Backend design should identify which system owns each important data type and how changes move between systems.&lt;/p&gt;
&lt;h3 id=&quot;design-integration-failure-paths&quot;&gt;Design Integration Failure Paths&lt;/h3&gt;
&lt;p&gt;Third-party APIs will eventually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Time out&lt;/li&gt;
&lt;li&gt;Reject requests&lt;/li&gt;
&lt;li&gt;Apply rate limits&lt;/li&gt;
&lt;li&gt;Return unexpected responses&lt;/li&gt;
&lt;li&gt;Experience downtime&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A production backend needs a strategy for those situations.&lt;br&gt;
That may include retries, queues, idempotency, fallback behavior, error logging, or manual recovery.&lt;br&gt;
The exact solution depends on the workflow, but ignoring the failure path is rarely a good solution.&lt;/p&gt;
&lt;h3 id=&quot;treat-authorization-as-architecture&quot;&gt;Treat Authorization As Architecture&lt;/h3&gt;
&lt;p&gt;Authentication tells us who made the request.&lt;br&gt;
Authorization decides what that person can do.&lt;br&gt;
As applications grow, the second problem often becomes harder.&lt;/p&gt;
&lt;p&gt;A SaaS application, for example, may have organization owners, administrators, managers, ordinary members, billing users, and support staff.&lt;/p&gt;
&lt;p&gt;If permission rules are embedded randomly throughout controllers and endpoints, they become difficult to reason about.&lt;/p&gt;
&lt;p&gt;Defining authorization boundaries early makes the rest of the system easier to maintain.&lt;/p&gt;
&lt;h3 id=&quot;choose-architecture-based-on-operational-needs&quot;&gt;Choose Architecture Based On Operational Needs&lt;/h3&gt;
&lt;p&gt;Microservices are useful when independently managed services genuinely solve an organizational or operational problem.&lt;/p&gt;
&lt;p&gt;They are not automatically an upgrade from a monolith.&lt;/p&gt;
&lt;p&gt;A modular application with clearly separated responsibilities can be a strong choice for many products.&lt;/p&gt;
&lt;p&gt;Architecture should match:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Product complexity&lt;/li&gt;
&lt;li&gt;Team structure&lt;/li&gt;
&lt;li&gt;Deployment requirements&lt;/li&gt;
&lt;li&gt;Operational overhead&lt;/li&gt;
&lt;li&gt;Expected change&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not architectural fashion.&lt;/p&gt;
&lt;h3 id=&quot;think-about-production-before-production&quot;&gt;Think About Production Before Production&lt;/h3&gt;
&lt;p&gt;Backend design should also answer:&lt;br&gt;
How will we know when something is wrong?&lt;/p&gt;
&lt;p&gt;Logs, error tracking, metrics, alerts, database visibility, and integration monitoring are part of the system, not extras to think about after launch.&lt;/p&gt;
&lt;p&gt;The framework can change.&lt;/p&gt;
&lt;p&gt;A poorly defined workflow is much harder to fix.&lt;/p&gt;
&lt;p&gt;For teams currently mapping these decisions, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.budventure.technology/backend-development&quot;&gt;backend development services&lt;/a&gt;&lt;/strong&gt; provide a broader view of how APIs, databases, integrations, authentication, architecture, and maintenance fit together.&lt;/p&gt;
&lt;p&gt;What do you define first when starting a new backend: domain boundaries, data model, API contracts, or infrastructure?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29972/backend-architecture-starts-with-workflows-not-frameworks</guid>
<pubDate>Tue, 06 Oct 2026 14:30:00 +0000</pubDate>
</item>
<item>
<title>Reads That Survive the Kill Switch</title>
<link>https://coderlegion.com/29829/reads-that-survive-the-kill-switch</link>
<description>&lt;p&gt;When an unattended cron worker misbehaves, operators typically engage a kill switch to halt all execution. This stops harmful mutations, but it frequently blinds the operations team as well. While the switch is active, nobody can see whether requests are piling up, and the team discovers the backlog only after the system recovers. This occurs because writes and reads often share a single gate. The disabled flag blocks publishing and the demand check together, incorrectly conflating the instruction to stop acting with the need to observe system state.&lt;/p&gt;
&lt;h3 id=&quot;blind-while-stopped&quot;&gt;Blind While Stopped&lt;/h3&gt;
&lt;p&gt;A kill switch that stops a misbehaving cron worker also blinds it. While engaged, nobody can see whether customers are waiting, so the team discovers a backlog only after recovery, or never. Writes and reads share one gate, meaning the disabled flag blocks publishing and the demand check together. Silent slots hide demand because a disabled result with no context reads identically whether zero or fifty replies wait. Recovery starts blind when the flag clears, as the first healthy slot discovers the backlog by accident instead of by report. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/schema-first-gates-for-ai-publishing-pipelines/&quot;&gt;Schema First Gates Ai Publishing Pipelines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-peek-pattern&quot;&gt;The Peek Pattern&lt;/h3&gt;
&lt;p&gt;To keep visibility intact during an outage, systems need a separation of concerns between looking at the queue and modifying it. The peek pattern addresses this by placing read operations ahead of any mutation gates. The workflow executes three distinct phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Token validation: Load credentials first. If authentication fails, the run stops immediately.&lt;/li&gt;
&lt;li&gt;The peek: Run the read-only demand check to count pending items without altering state.&lt;/li&gt;
&lt;li&gt;Gate evaluation: Check the kill switch status. If active, skip writing while preserving the read data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The read operation must never throw, write data, or trigger external notifications. Even if the underlying data store experiences intermittent pressure, the worst-case return value should be an empty count rather than a failed execution slot. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-duplicate-push-notifications-fcm/&quot;&gt;Duplicate Push Notifications Fcm&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;demand-on-every-result&quot;&gt;Demand on Every Result&lt;/h3&gt;
&lt;p&gt;A common monitoring flaw is reporting demand only when tasks succeed. When a kill switch is active, disabled slots often return a silent empty response with no context. Operators cannot tell whether zero tasks are waiting or fifty items are sitting in a blocked queue.&lt;/p&gt;
&lt;p&gt;The solution is to attach the pending count to every possible execution outcome. Whether a slot is disabled, skipped, failed, or successfully published, the log entry or audit digest should carry the exact pending count. This guarantees that observability logs show continuous pressure trends even when the system is entirely fenced off from writing.&lt;/p&gt;
&lt;h3 id=&quot;answering-stays-gated&quot;&gt;Answering Stays Gated&lt;/h3&gt;
&lt;p&gt;Visibility into demand must never weaken the security boundary of the kill switch. Observing the queue does not grant permission to process it. Publishing answers and executing outbound calls must remain safely behind every existing guard, including duplicate detection entries, budget caps, and audit requirements.&lt;/p&gt;
&lt;p&gt;Consider this simplified TypeScript configuration pattern showing how a worker separates the read peek from the write action:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type RunResult = { status: string; pendingCount: number };

async function handleCronJob(isKillSwitchActive: boolean): Promise&amp;lt;RunResult&amp;gt; {
  const pendingCount = await fetchPendingDemandSafely();
  
  if (isKillSwitchActive) {
    return { status: &quot;fenced&quot;, pendingCount };
  }

  await executeWrites();
  return { status: &quot;published&quot;, pendingCount };
}

async function fetchPendingDemandSafely(): Promise&amp;lt;number&amp;gt; {
  try {
    return await getQueueDepth();
  } catch {
    return 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this model, the &lt;code&gt;fetchPendingDemandSafely&lt;/code&gt; function wraps calls in a try-catch block to guarantee it never throws an error that could abort the inspection. The pending count is captured regardless of whether the kill switch is active.&lt;/p&gt;
&lt;h3 id=&quot;maintaining-visibility-during-outages&quot;&gt;Maintaining Visibility During Outages&lt;/h3&gt;
&lt;p&gt;Separating read operations from write mutations prevents the blind spots that typically accompany emergency interventions. By ensuring that demand checks execute independently of write gates, and by attaching pending counts to every outcome, engineering teams can size their recovery efforts accurately the moment restrictions are lifted.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29829/reads-that-survive-the-kill-switch</guid>
<pubDate>Mon, 05 Oct 2026 09:02:25 +0000</pubDate>
</item>
<item>
<title>Why I Chose a Simple Capability Token for Waitless — and When I Would Harden It</title>
<link>https://coderlegion.com/29812/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it</link>
<description>&lt;p&gt;While working on &lt;strong&gt;Waitless&lt;/strong&gt;, I needed customers to be able to track their position in a queue and leave the queue without creating an account.&lt;/p&gt;
&lt;p&gt;That led to a small but interesting security design question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How can I authorize an anonymous user without introducing a full authentication system?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My current solution is deliberately simple: each queue entry receives a random UUID token, and that token is used as a capability for the tracking link.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;uuid-token-instead-of-a-sequential-id&quot;&gt;UUID token instead of a sequential ID&lt;/h3&gt;
&lt;p&gt;A sequential identifier could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/track/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that IDs such as &lt;code&gt;123&lt;/code&gt;, &lt;code&gt;124&lt;/code&gt;, and &lt;code&gt;125&lt;/code&gt; are predictable and enumerable.&lt;/p&gt;
&lt;p&gt;For an anonymous tracking system, I don't want a user to be able to guess another customer's resource simply by changing the ID.&lt;/p&gt;
&lt;p&gt;Instead, an entry has a separate token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;model Entry {
  id      String @id @default(uuid())
  token   String @unique @default(uuid())

  name     String
  email    String?
  position Int
  status   EntryStatus @default(WAITING)

  queueId String
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The public URL therefore looks more like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/track/550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important point is that the token is not intended to be an ordinary identifier.&lt;/p&gt;
&lt;p&gt;It acts as a &lt;strong&gt;capability&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-a-capability-url-&quot;&gt;What is a capability URL?&lt;/h3&gt;
&lt;p&gt;A capability can be thought of as:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;An unguessable secret that grants access to a specific resource.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In my case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;possess valid token
        ↓
access this queue entry
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The capability is deliberately designed to be difficult to guess.&lt;/p&gt;
&lt;p&gt;That is different from simply hiding a sequential ID and hoping nobody discovers it.&lt;/p&gt;
&lt;p&gt;This distinction matters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Security by obscurity
→ &quot;Nobody knows the ID.&quot;

Capability-based design
→ &quot;Access requires possession of a deliberately unguessable credential.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second is a much more intentional security model.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-i-kept-the-token-simple-for-now&quot;&gt;Why I kept the token simple for now&lt;/h3&gt;
&lt;p&gt;Waitless is an anonymous queueing system.&lt;/p&gt;
&lt;p&gt;A customer does not need an account, password, or traditional login just to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;see their position&lt;/li&gt;
&lt;li&gt;receive queue updates&lt;/li&gt;
&lt;li&gt;leave the queue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Introducing a complete authentication system would add considerable complexity to the product and to the user experience.&lt;/p&gt;
&lt;p&gt;For the current use case, a capability link gives me a simple model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Join queue
    ↓
Generate random token
    ↓
Give customer tracking link
    ↓
Customer presents token
    ↓
Server identifies the queue entry
    ↓
Allow limited actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a conscious trade-off.&lt;/p&gt;
&lt;p&gt;I'm choosing &lt;strong&gt;simplicity and frictionless anonymous access&lt;/strong&gt; because the current resource is relatively limited in scope.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;but-the-token-is-also-a-bearer-credential&quot;&gt;But the token is also a bearer credential&lt;/h3&gt;
&lt;p&gt;There is an important consequence.&lt;/p&gt;
&lt;p&gt;If possession of the token grants access, then:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;whoever possesses the token can use it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That makes the token a bearer credential.&lt;/p&gt;
&lt;p&gt;For example, the current &lt;code&gt;leave()&lt;/code&gt; operation relies on the same tracking capability.&lt;/p&gt;
&lt;p&gt;So if the token is leaked, copied, logged, or shared, another person could potentially use it.&lt;/p&gt;
&lt;p&gt;That means the design is intentionally simple, but it is not the strongest possible security model.&lt;/p&gt;
&lt;h4 id=&quot;known-limitation&quot;&gt;Known limitation&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;The tracking token currently also functions as a bearer credential for &lt;code&gt;leave()&lt;/code&gt;, with no rate-limiting or one-time consumption.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I consider this an accepted limitation of the current design rather than something I want to hide.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-stronger-design-capability-url-session-cookie&quot;&gt;A stronger design: capability URL → session cookie&lt;/h3&gt;
&lt;p&gt;One way to harden the system would be to use the tracking link only as an initial capability.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/claim/&amp;lt;secret&amp;gt;
      ↓
validate capability
      ↓
create anonymous session
      ↓
HttpOnly + Secure cookie
      ↓
redirect to /track
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, the token would no longer need to remain in the URL.&lt;/p&gt;
&lt;p&gt;The browser would use the session cookie for future requests.&lt;/p&gt;
&lt;p&gt;This would reduce some of the risks associated with repeatedly carrying a secret in a URL and would give me more control over session lifecycle, expiration and revocation.&lt;/p&gt;
&lt;p&gt;But it introduces another trade-off.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-trade-off-security-vs-convenience&quot;&gt;The trade-off: security vs. convenience&lt;/h3&gt;
&lt;p&gt;The current capability link has an important usability property:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The link itself works across devices.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A customer can open it on their phone, copy it to another device, bookmark it, or reopen it later.&lt;/p&gt;
&lt;p&gt;With a one-time capability-to-session exchange, that behavior becomes more complicated.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Phone
  ↓
claim token
  ↓
session created
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the same one-time token may no longer be usable on another device.&lt;/p&gt;
&lt;p&gt;Supporting multiple devices would therefore require additional identity/session handling, which increases complexity.&lt;/p&gt;
&lt;p&gt;For a simple anonymous queue, I decided that this trade-off is not currently justified.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-would-this-design-change-&quot;&gt;When would this design change?&lt;/h2&gt;
&lt;p&gt;The answer is: &lt;strong&gt;when the product starts having identity requirements.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine Waitless later introduces a reservation system.&lt;/p&gt;
&lt;p&gt;Now a customer might have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Account
   ↓
Reservations
   ↓
Queue entries
   ↓
Payment history
   ↓
Cancellation rules
   ↓
Personal information
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The security requirements become much stronger.&lt;/p&gt;
&lt;p&gt;At that point, a random tracking capability may no longer be enough as the main authorization mechanism.&lt;/p&gt;
&lt;p&gt;I would probably move toward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User authentication
        ↓
Authenticated session
        ↓
Authorization
        ↓
Reservation / queue access
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The capability token could still exist for specific use cases, but it would no longer be responsible for the entire identity model.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;uuidv4-vs-uuidv7&quot;&gt;UUIDv4 vs. UUIDv7&lt;/h2&gt;
&lt;p&gt;This also made me think more carefully about what an identifier is supposed to do.&lt;/p&gt;
&lt;p&gt;For database identifiers, UUIDv7 is attractive because it is time-ordered and therefore provides better index locality than completely random UUIDv4 values.&lt;/p&gt;
&lt;p&gt;For a public capability, however, my primary requirement is different:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;I care more about unpredictability than database ordering.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's why I would keep the concepts separate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Database ID
    ↓
UUIDv7 can be a good choice

Public capability token
    ↓
Cryptographically random token / UUIDv4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an example of a broader engineering principle:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The best identifier for the database is not necessarily the best credential for the client.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-about-storing-the-token-itself-&quot;&gt;What about storing the token itself?&lt;/h2&gt;
&lt;p&gt;Another improvement would be to store only a hash of the capability:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;raw token
    ↓
SHA-256
    ↓
tokenHash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the database contains the verifier rather than the original bearer secret.&lt;/p&gt;
&lt;p&gt;The application can hash the presented token and compare the result.&lt;/p&gt;
&lt;p&gt;This gives additional protection if the database contents are exposed.&lt;/p&gt;
&lt;p&gt;Again, it adds some implementation complexity, so it is a hardening step rather than something I needed to introduce simply to make the current Waitless flow work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-current-decision&quot;&gt;My current decision&lt;/h2&gt;
&lt;p&gt;For the current version of Waitless:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Random capability token
        ↓
Tracking URL
        ↓
Anonymous access
        ↓
Limited actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I deliberately chose this because it provides a very small and understandable authorization mechanism without forcing customers through registration and login.&lt;/p&gt;
&lt;p&gt;The trade-off is that the token is a bearer credential, so the system needs to treat it as sensitive.&lt;/p&gt;
&lt;p&gt;I have therefore documented the limitation rather than pretending the design is perfect.&lt;/p&gt;
&lt;p&gt;My planned evolution is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Current
Random capability URL
        ↓
Simple anonymous access


Later
Rate limiting
+ expiry / revocation
+ stronger token handling


When identity becomes important
        ↓
Capability URL
        ↓
Session / authentication
        ↓
Fine-grained authorization
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting lesson for me was not simply “use UUIDs.”&lt;/p&gt;
&lt;p&gt;It was learning to ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What is this value actually supposed to represent?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;An identifier, a capability, and an authenticated identity are three different concepts.&lt;/p&gt;
&lt;p&gt;Once that distinction is clear, the architecture becomes much easier to reason about.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29812/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it</guid>
<pubDate>Mon, 05 Oct 2026 07:20:50 +0000</pubDate>
</item>
<item>
<title>When Gemini Rejects Cloudflare Workers by Location</title>
<link>https://coderlegion.com/29736/when-gemini-rejects-cloudflare-workers-by-location</link>
<description>&lt;p&gt;When a scheduled worker calls the Gemini API around the clock, an unexpected failure can disrupt pipelines for hours. During an outage, error monitoring dashboards often show a mix of failures that appear to stem from a single overarching problem, yet they require opposite responses. Distinguishing transient overloads from static geo-blocking when using serverless egress IPs is essential to keeping automated workflows running without unnecessary provider migrations or wasted retry cycles. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/normalize-cloudflare-workflows-trigger-payloads/&quot;&gt;Normalize Cloudflare Workflows Trigger Payloads&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;everything-fails-at-once&quot;&gt;Everything Fails at Once&lt;/h3&gt;
&lt;p&gt;In a recent debugging session with the Morgans provider integration, scheduled workers executing on serverless edges began encountering widespread errors. The dashboard showed a wall of failed requests with status codes that looked like a standard capacity outage. However, closer inspection of the raw error payloads revealed that two entirely distinct failure modes were occurring simultaneously within the same incident window.&lt;/p&gt;
&lt;p&gt;Understanding this behavior requires looking at how edge platforms route traffic. Serverless execution environments route requests through rotating egress pools. When an upstream provider updates its regional enforcement rules or experiences capacity shifts, certain egress IPs may trigger geographic restrictions while others continue to pass through normally. Without inspecting the specific error message returned by the provider, operations teams risk misdiagnosing the root cause entirely.&lt;/p&gt;
&lt;h3 id=&quot;two-errors-opposite-cures&quot;&gt;Two Errors, Opposite Cures&lt;/h3&gt;
&lt;p&gt;During the incident, two specific error signatures appeared in the logs. The first was a transient capacity issue, while the second was a deterministic regional rejection.&lt;/p&gt;
&lt;p&gt;The first signature returned a standard HTTP 503 status code with a payload message indicating high demand. This error is temporary. It clears with time, and standard exponential backoff retries are the correct operational response. Forcing a migration away from the primary provider for a temporary demand spike introduces unnecessary complexity.&lt;/p&gt;
&lt;p&gt;The second signature returned an HTTP 400 status code accompanied by a message stating that the user location is not supported for the API use. This error is deterministic per egress IP. It never clears with retries, and treating it as an overload wastes the entire incident window waiting for a recovery that will not happen on that IP address.&lt;/p&gt;
&lt;p&gt;Treating a geo-block as an overload wastes valuable time, while treating an overload as a geo-block triggers an unnecessary provider migration. Correlating failures solely by clock time or status code makes two different problems look like one.&lt;/p&gt;
&lt;h3 id=&quot;routing-around-geography&quot;&gt;Routing Around Geography&lt;/h3&gt;
&lt;p&gt;When a geo-block is confirmed, waiting for it to resolve is not a viable architecture. The structural answer involves routing around the geographic restriction by promoting an in-platform fallback tier. For example, routing execution to a serverless model inference engine running on the same edge network avoids external region-gating rules entirely.&lt;/p&gt;
&lt;p&gt;To implement this safely, the fallback tier must be exercised against live traffic regularly. A fallback that never runs is merely a hope rather than a reliable tier. Verifying response-shape handling across both the primary provider and the fallback ensures that switching over during an incident does not introduce secondary parsing failures.&lt;/p&gt;
&lt;h3 id=&quot;log-bodies-not-codes&quot;&gt;Log Bodies, Not Codes&lt;/h3&gt;
&lt;p&gt;Status-only logging is the primary reason why teams misdiagnose edge routing failures. Relying solely on HTTP status codes obscures the difference between a demand spike and a regional policy rejection. Capturing a short snippet of the response body, such as the first 160 characters of an error payload, provides the exact context needed to automate routing decisions.&lt;/p&gt;
&lt;p&gt;Consider a minimal JavaScript implementation running in an edge worker that inspects the provider response before deciding whether to retry or fail over:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function callGeminiWithFallback(requestData) {
  try {
    const response = await fetch(&quot;https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent&quot;, {
      method: &quot;POST&quot;,
      headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
      body: JSON.stringify(requestData)
    });

    if (!response.ok) {
      const errorBody = await response.text();
      const snippet = errorBody.substring(0, 160);

      if (response.status === 400 &amp;amp;&amp;amp; snippet.includes(&quot;user location is not supported&quot;)) {
        return await executeInPlatformFallback(requestData);
      }

      if (response.status === 503) {
        throw new TransientDemandError(&quot;Model experiencing high demand: &quot; + snippet);
      }

      throw new Error(&quot;Unhandled provider error: &quot; + snippet);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TransientDemandError) {
      return executeWithBackoff(requestData);
    }
    throw error;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern ensures that geographic rejections trigger an immediate switch to the secondary tier, while transient capacity limits correctly trigger bounded retries.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Managing external model providers from serverless edge environments requires looking past simple status codes. By capturing error response bodies, distinguishing static geo-blocking from temporary capacity overloads, and maintaining a warm fallback tier, engineering teams can build resilient integrations that survive unexpected regional policy shifts without manual intervention. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/managing-gemini-overload-with-intelligent-fallback-patterns/&quot;&gt;Managing Gemini Overload Intelligent Fallback Patterns&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29736/when-gemini-rejects-cloudflare-workers-by-location</guid>
<pubDate>Sun, 04 Oct 2026 15:45:20 +0000</pubDate>
</item>
<item>
<title>Fixing Threads API Authorization Code Failures</title>
<link>https://coderlegion.com/29709/fixing-threads-api-authorization-code-failures</link>
<description>&lt;p&gt;Integrating the Threads API requires navigating a three-legged OAuth process that can stall at several distinct points. When an application fails to exchange an authorization code for a valid access token, debugging often becomes a frustrating exercise in trial and error. The API returns cryptic numeric codes and generic error messages that rarely point to the root cause of the failure.&lt;/p&gt;
&lt;p&gt;The core problem stems from strict validation rules across the authorization code, the application credentials, the redirect URIs, and token families. Understanding why these failures occur helps engineering teams establish a reliable diagnostic process and complete successful token exchanges without burning single-use codes.&lt;/p&gt;
&lt;h3 id=&quot;the-three-legged-token-flow&quot;&gt;The Three-Legged Token Flow&lt;/h3&gt;
&lt;p&gt;The Threads API uses a standard OAuth three-legged flow to establish user-level access. First, the user visits an authorization URL hosted by the platform, which redirects back to the application with a short-lived authorization code. Second, the backend exchanges this authorization code for a short-lived user access token via a POST request. Third, this short-lived token is exchanged for a sixty-day long-lived token.&lt;/p&gt;
&lt;p&gt;Each leg of this journey relies on precise inputs. The authorization code expires within minutes and can only be used once. If any parameter in the subsequent requests deviates from the expected format, the platform rejects the exchange.&lt;/p&gt;
&lt;h3 id=&quot;the-four-failure-modes&quot;&gt;The Four Failure Modes&lt;/h3&gt;
&lt;p&gt;When token exchanges fail, the error typically maps to one of four specific conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wrong token family (452 Session key invalid)&lt;/strong&gt;: Tokens minted in generic testing surfaces or alternative Meta dashboards belong to a different token family. The Threads endpoint rejects these tokens outright.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrong secret of two&lt;/strong&gt;: Meta frequently issues multiple app ID and secret pairs for a single project. The Threads API requires the specific Threads credential pair, and because both secrets share the same length, visual inspection is unreliable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-identical redirect URI (36008)&lt;/strong&gt;: The redirect URI supplied during the code exchange must match the initial authorization request byte-for-byte, including trailing slashes and parameter encodings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stale or consumed codes (36006)&lt;/strong&gt;: Authorization codes are strictly single-use. Retrying a failed request with an old code or keeping multiple browser tabs open will trigger this failure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-diagnostic-ladder&quot;&gt;The Diagnostic Ladder&lt;/h3&gt;
&lt;p&gt;To isolate these issues without wasting tokens, follow a strict diagnostic order. Testing items sequentially prevents compounding errors.&lt;/p&gt;
&lt;p&gt;First, verify the token family. If a 452 error occurs, check whether the credentials originated from the correct Meta app dashboard configured specifically for Threads rather than general Facebook Login products.&lt;/p&gt;
&lt;p&gt;Second, test the secret pairing programmatically. Because visual checks fail on same-length secrets, send a lightweight client credentials grant or inspection call using the suspected secret to confirm authentication success before attempting the user code exchange.&lt;/p&gt;
&lt;p&gt;Third, compare redirect URIs at the byte level. Print out the exact string sent in both the authorization redirect and the token exchange POST body. A missing trailing slash or an unencoded character will immediately cause a 36008 error.&lt;/p&gt;
&lt;p&gt;Finally, treat authorization codes as perishable singletons. Open a fresh authorization URL, capture the code immediately, and execute the exchange request within seconds.&lt;/p&gt;
&lt;h3 id=&quot;code-example-for-token-exchange&quot;&gt;Code Example for Token Exchange&lt;/h3&gt;
&lt;p&gt;Executing the token exchange correctly requires sending the proper POST parameters with the right application credentials. Here is an example request using curl:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST &quot;https://graph.threads.net/oauth/access_token&quot; \
  -F &quot;client_id=YOUR_THREADS_CLIENT_ID&quot; \
  -F &quot;client_secret=YOUR_THREADS_CLIENT_SECRET&quot; \
  -F &quot;grant_type=authorization_code&quot; \
  -F &quot;redirect_uri=https://example.com/callback&quot; \
  -F &quot;code=AQD_SAMPLE_AUTHORIZATION_CODE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If successful, the endpoint returns a JSON payload containing the short-lived access token and the user ID. Immediately pass this token to the long-lived token endpoint to secure a sixty-day session.&lt;/p&gt;
&lt;h3 id=&quot;after-the-token-lands&quot;&gt;After the Token Lands&lt;/h3&gt;
&lt;p&gt;Once the sixty-day token is successfully retrieved, verify its permissions by calling a read-scoped endpoint before storing it. Store the final token securely in platform secret storage or a key-value store with strict rotation policies. Never commit secrets to version control or leave them exposed in local configuration files. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/android-secrets-local-config-ci/&quot;&gt;Secrets Local Config Ci&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Navigating the Threads API authentication flow depends on precise adherence to credential pairings, token families, and byte-level URI matching. By following a structured diagnostic approach and treating authorization codes as perishable singletons, developers can eliminate invalid-code failures and build resilient social integrations. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/site-wide-structured-data-and-social-graph-integration/&quot;&gt;Site Wide Structured Data Social Graph&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29709/fixing-threads-api-authorization-code-failures</guid>
<pubDate>Sun, 04 Oct 2026 09:03:46 +0000</pubDate>
</item>
<item>
<title>Parsing JSON from Thinking-Model APIs</title>
<link>https://coderlegion.com/29681/parsing-json-from-thinking-model-apis</link>
<description>&lt;p&gt;When you request structured data from a reasoning model using a JSON mime type, the response often arrives split across several content parts rather than sitting neatly in a single text block. This breaks standard assumptions in ingestion pipelines. Developers usually expect the model to return a single text payload starting with an opening brace. Reasoning models, however, routinely prepend prose preambles, attach internal thought signatures, and partition the final output across distinct blocks. For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/schema-first-gates-for-ai-publishing-pipelines/&quot;&gt;Schema First Gates Ai Publishing Pipelines&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The core problem manifests in three distinct ways during API integration. First, if your code attempts to read only the first part of the response, it captures conversational filler like introductory remarks, causing JSON parsers to throw syntax errors. Second, the model's token output limit must accommodate both the internal reasoning trace and the final JSON payload. If you allocate a small budget, the model spends all its tokens on thinking, leading to abrupt truncation with a max tokens finish reason. Third, applying blind retries to these truncated payloads merely drains rate limits without resolving the underlying deterministic failure.&lt;/p&gt;
&lt;h3 id=&quot;anatomy-of-a-split-response&quot;&gt;Anatomy of a Split Response&lt;/h3&gt;
&lt;p&gt;A typical API payload from a reasoning model arrives as an array of content parts. The first part may contain a conversational preamble. Subsequent parts can include encrypted or signed reasoning traces, followed eventually by the markdown-fenced JSON structure. Treating this stream as a monolithic document guarantees parser failure. &lt;/p&gt;
&lt;p&gt;Consider a scenario where an agent requests a configuration object from a reasoning model with an output budget set to two hundred tokens. The model generates an extensive internal monologue to work through the schema requirements. It exhausts the token limit just as it begins writing the payload, returning a truncated string without a closing brace. Your application receives a finish reason indicating truncation, while the primary text parts contain only partial data and reasoning debris.&lt;/p&gt;
&lt;h3 id=&quot;the-defensive-parsing-recipe&quot;&gt;The Defensive Parsing Recipe&lt;/h3&gt;
&lt;p&gt;To handle multi-part responses reliably, your ingestion pipeline needs a defensive parsing strategy that combines concatenation, unfencing, strict parsing, and robust fallbacks.  For a related implementation, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://raylabs.app/articles/multi-agent-review-pipeline/&quot;&gt;Multi Agent Review Pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface ModelPart {
  text?: string;
}

interface ModelResponse {
  candidates?: Array&amp;lt;{
    content?: {
      parts?: ModelPart[];
    };
    finishReason?: string;
  }&amp;gt;;
}

function extractJsonPayload(response: ModelResponse): Record&amp;lt;string, unknown&amp;gt; {
  const parts = response.candidates?.[0]?.content?.parts ?? [];
  const rawText = parts.map(p =&amp;gt; p.text ?? '').join('');
  
  const unfenced = rawText
    .replace(/^```json\s*/gm, '')
    .replace(/^```\s*$/gm, '');

  try {
    return JSON.parse(unfenced);
  } catch {
    const match = unfenced.match(/\{[\s\S]*\}/);
    if (match) {
      return JSON.parse(match[0]);
    }
    throw new Error('Failed to extract valid JSON from response parts');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function joins all available text parts into a single string before attempting any operations. It then strips markdown code fences and attempts a strict parse. If strict parsing fails due to surrounding prose, a lightweight regular expression isolates the outermost curly braces. This approach avoids heavy third-party parsing dependencies while successfully navigating messy model outputs.&lt;/p&gt;
&lt;h3 id=&quot;budgeting-for-thought-and-fallbacks&quot;&gt;Budgeting for Thought and Fallbacks&lt;/h3&gt;
&lt;p&gt;Sizing your output limits correctly prevents truncation before parsing even begins. Because reasoning models bill their internal monologue against the exact same token limit as the final answer, you must scale your output budgets upward. A budget of several thousand tokens is often necessary for complex schemas, separating the cost of thought from the cost of the data structure.&lt;/p&gt;
&lt;p&gt;When a parse failure does occur despite proper budgeting, avoid infinite retry loops. Instead, implement a log-and-skip mechanism. Record the finish reason, capture a bounded preview of the uncleaned text for debugging, and advance your fallback model chain or queue. This diagnostic approach turns silent ingestion failures into actionable telemetry.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Reliable integration with reasoning models requires moving past the assumption of clean, single-blob JSON responses. By joining multi-part outputs, budgeting appropriately for internal reasoning traces, and implementing defensive string manipulation, you can stabilize your automated pipelines against model-specific quirks.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29681/parsing-json-from-thinking-model-apis</guid>
<pubDate>Sun, 04 Oct 2026 03:15:39 +0000</pubDate>
</item>
<item>
<title>Building a Resilient Job Queue in Go: Postgres, Worker Pool and Graceful Shutdown</title>
<link>https://coderlegion.com/29442/building-a-resilient-job-queue-in-go-postgres-worker-pool-and-graceful-shutdown</link>
<description>&lt;p&gt;Building a Resilient Job Queue in Go: Postgres, Worker Pool and Graceful Shutdown&lt;/p&gt;
&lt;p&gt;I am Pires Aurélio, Go Backend Engineer from Luanda, Angola. I build production systems in public.&lt;/p&gt;
&lt;p&gt;For two years I built scrapers and simple REST APIs. I got tired of tutorials that only show Hello World. No jobs, no retries, no real production problems.&lt;/p&gt;
&lt;p&gt;So I built a real one: A Production-Ready Background Job Queue in Go.&lt;/p&gt;
&lt;p&gt;This is the production deep-dive companion to my DEV article. All links are in the first comment.&lt;/p&gt;
&lt;p&gt;WHAT WE ARE BUILDING&lt;/p&gt;
&lt;p&gt;A REST API where you POST a task, it goes to Postgres, and Go workers process it in background with retries and graceful shutdown. Like Sidekiq and Celery, but in pure Go.&lt;/p&gt;
&lt;p&gt;Stack: Go + Gin + Postgres + Goroutines + Docker&lt;br&gt;
Features: Clean Architecture, Postgres as queue, Worker Pool with 5 Goroutines, Graceful Shutdown, Exponential Backoff, Supervisor pattern.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ARCHITECTURE&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Client -&amp;gt; POST /jobs -&amp;gt; API Gin -&amp;gt; Postgres jobs table -&amp;gt; Worker Pool -&amp;gt; Process -&amp;gt; Update Status&lt;/p&gt;
&lt;p&gt;Why Postgres and not Redis? One less service and ACID guarantees. This proves you understand transactions and row-level locking.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;THE PRODUCTION SCHEMA&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Table jobs has: id UUID, payload JSONB, status pending processing done failed, attempts int, max_attempts int, last_error text, created_at and next_retry_at.&lt;/p&gt;
&lt;p&gt;The most important part is the index on status and next_retry_at where status is pending. This makes the picker O log n with 1 million rows. See Image 1.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;THE SAFE PICKER&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The mistake in tutorials is SELECT then UPDATE. Two workers get the same job.&lt;/p&gt;
&lt;p&gt;The solution is an atomic UPDATE where id comes from a sub-select with row-level locking and SKIP LOCKED. Only one worker gets a job. This is what separates toy from production. See Image 1.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WORKER POOL&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Start with concurrency 5. For each worker start a goroutine. Loop select on ctx Done for shutdown, default picks a job and processes with retry.&lt;/p&gt;
&lt;p&gt;If fails, increment attempts. If attempts &amp;gt;= max, mark failed. Else backoff = 2 power attempts seconds and set next_retry_at.&lt;/p&gt;
&lt;p&gt;See Image 2.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SUPERVISOR - NEVER LOSE A JOB&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If server crashes while processing, job stays processing forever.&lt;/p&gt;
&lt;p&gt;On startup: Reset all jobs where status is processing and updated_at older than 5 minutes back to pending. This is what real companies do.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GRACEFUL SHUTDOWN - THE INTERVIEW QUESTION&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Use signal NotifyContext for Interrupt and SIGTERM. Start workers with that context. Start HTTP server in goroutine. Block on ctx Done. On signal, create timeout context 10 seconds and call server Shutdown. Workers stop via ctx Done.&lt;/p&gt;
&lt;p&gt;See Image 3. This proves you know context propagation.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BENCHMARK&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python + Celery + Redis: 120 jobs per sec, 180 MB RAM&lt;br&gt;
Go + Postgres: 1850 jobs per sec, 18 MB RAM&lt;/p&gt;
&lt;p&gt;15x faster, 10x less memory.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WHAT RECRUITERS SEE&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You understand concurrency, you understand databases beyond SELECT, you think about failure, you can build infrastructure not just CRUD.&lt;/p&gt;
&lt;p&gt;Full code with metrics is coming to my GitHub next week. Links in first comment.&lt;/p&gt;
&lt;p&gt;Built from Luanda, Angola.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29442/building-a-resilient-job-queue-in-go-postgres-worker-pool-and-graceful-shutdown</guid>
<pubDate>Thu, 01 Oct 2026 18:08:31 +0000</pubDate>
</item>
<item>
<title>Building Smart Health API: A Production-Style REST API with CNN-Based Risk Prediction</title>
<link>https://coderlegion.com/29424/building-smart-health-api-a-production-style-rest-api-with-cnn-based-risk-prediction</link>
<description>&lt;p&gt;&lt;em&gt;By Priyadharshiny J — &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/priyadharshiny13&quot;&gt;GitHub: priyadharshiny13&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;Smart Health API is a backend platform I built to explore how a real healthcare product might structure its API layer: role-based access for different user types, secure authentication, database migrations that don't break production, and a machine learning model wired into an async pipeline instead of blocking the request thread.&lt;/p&gt;
&lt;p&gt;The result is a FastAPI service that lets Patients, Doctors, and Admins interact with the same system through different permission levels, and that returns CNN-based oral cancer risk scores without making the caller wait on model inference.&lt;/p&gt;
&lt;h3 id=&quot;stack&quot;&gt;Stack&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API layer:&lt;/strong&gt; FastAPI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; MySQL, with SQLAlchemy ORM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrations:&lt;/strong&gt; Alembic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth:&lt;/strong&gt; JWT-based authentication with Role-Based Access Control (RBAC)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ML:&lt;/strong&gt; TensorFlow CNN model for oral cancer risk prediction&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async processing:&lt;/strong&gt; Celery + Redis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infra:&lt;/strong&gt; Docker, deployed toward AWS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD:&lt;/strong&gt; GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;architecture-decisions&quot;&gt;Architecture Decisions&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why RBAC with three roles instead of a single user type?&lt;/strong&gt;&lt;br&gt;
Healthcare data access isn't uniform — a Patient should only see their own records, a Doctor needs read/write access to their assigned patients, and an Admin needs oversight without clinical permissions. I modeled this directly into the JWT claims and enforced it at the route-dependency level in FastAPI, rather than checking roles inside each endpoint's business logic. This keeps permission logic in one place and out of the individual handlers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Alembic migrations instead of &lt;code&gt;create_all()&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SQLAlchemy.create_all()&lt;/code&gt; is fine for a prototype, but it can't handle schema evolution — adding a column, renaming a table, backfilling data. Alembic gave the project versioned, reversible migrations, which is the same discipline a real production service needs the moment more than one person touches the schema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Celery + Redis for the CNN inference?&lt;/strong&gt;&lt;br&gt;
Running a CNN prediction synchronously inside an HTTP request handler blocks the worker for however long inference takes, which doesn't scale under concurrent load. I moved prediction into a Celery task queue backed by Redis: the API accepts the image, enqueues the job, and returns a task ID immediately. The client polls (or would be pushed a webhook/notification in a fuller version) for the result. This separates the request/response cycle from the compute-heavy ML work.&lt;/p&gt;
&lt;h3 id=&quot;a-real-debugging-story&quot;&gt;A Real Debugging Story&lt;/h3&gt;
&lt;p&gt;Wiring up SQLAlchemy models across multiple modules (&lt;code&gt;User&lt;/code&gt;, &lt;code&gt;Patient&lt;/code&gt;, &lt;code&gt;Doctor&lt;/code&gt;, &lt;code&gt;Prediction&lt;/code&gt;) introduced circular import errors once relationships started referencing each other — &lt;code&gt;Patient&lt;/code&gt; needed &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;User&lt;/code&gt; needed &lt;code&gt;Prediction&lt;/code&gt;, and Python's import system didn't like the cycle. The fix was restructuring model definitions to use string-based relationship references (&lt;code&gt;relationship(&quot;Patient&quot;, back_populates=...)&lt;/code&gt;) instead of direct class imports, which lets SQLAlchemy resolve the relationship lazily at mapper-configuration time rather than at import time.&lt;/p&gt;
&lt;p&gt;Small detail, but it's the kind of thing that only shows up once a schema has enough interrelated tables — and it's a good reminder that ORM relationship design needs the same intentionality as the schema itself.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add automated tests for the RBAC permission boundaries (currently manually verified)&lt;/li&gt;
&lt;li&gt;Expose Celery task status through a WebSocket instead of polling&lt;/li&gt;
&lt;li&gt;Add rate limiting on the prediction endpoint given its compute cost&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;repo&quot;&gt;Repo&lt;/h3&gt;
&lt;p&gt;Code and setup instructions: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/priyadharshiny13&quot;&gt;github.com/priyadharshiny13&lt;/a&gt; — see the &lt;code&gt;smart-health-api&lt;/code&gt; repository.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29424/building-smart-health-api-a-production-style-rest-api-with-cnn-based-risk-prediction</guid>
<pubDate>Thu, 01 Oct 2026 15:25:23 +0000</pubDate>
</item>
<item>
<title>api-performance-profiler</title>
<link>https://coderlegion.com/28855/api-performance-profiler</link>
<description>&lt;p&gt;  As backend developers, we often fly blind when building APIs locally. You write a route, but you don't really know how fast it runs until you leave your editor, set up Postman,&lt;br&gt;
  or configure a heavy APM tool. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;I wanted performance testing to feel native and frictionless, so I built an open-source, zero-dependency local profiling extension for Node.js (Express &amp;amp; NestJS). 

Here is what it brings directly to your editor:

🟢 Inline Latency Metrics: See real-time execution speeds (e.g., 🟢 84ms) rendered right beside your route definitions in the code.
🚀 1-Click Load Testing: A '▶ Load test' CodeLens sits directly above your route. Click it to replay the last recorded request 1,000 times under heavy load, simulating real
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  traffic instantly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;📊 Routes Dashboard: A dedicated VS Code sidebar showing all observed routes in your app, sorted from slowest to fastest.
🔐 100% Local &amp;amp; Secure: Everything is stored in local memory. No cloud accounts, no external dashboards, and no bloated tracking dependencies. 

It takes less than a minute to set up—just install the lightweight middleware and the VS Code extension, and your editor comes alive with performance data.

I’d love for you to try it out on your local Express or NestJS projects! Let me know your thoughts, feature requests, or feedback in the comments.
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/28855/api-performance-profiler</guid>
<pubDate>Mon, 28 Sep 2026 04:48:58 +0000</pubDate>
</item>
<item>
<title>Horizontally Scaling PostgreSQL: Database Sharding with Citus</title>
<link>https://coderlegion.com/28711/horizontally-scaling-postgresql-database-sharding-with-citus</link>
<description>&lt;blockquote&gt;&lt;p&gt;A practical guide to scaling your database infrastructure horizontally. Learn how to shard your data, optimize query routing, and build a distributed multi-tenant architecture using Postgres, Citus and Docker.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;What do you need to follow along?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A working Laptop/PC?&lt;/li&gt;
&lt;li&gt;Working knowledge of SQL or PostgreSQL.&lt;/li&gt;
&lt;li&gt;To have at least eaten something today (preferably swallow related).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Database sharding is a strategy used to solve scalability issues in applications that have a massive amount of data. It involves splitting data into smaller chunks called shards. Each shard is stored on a separate database server instance, effectively spreading the data and the workload across multiple machines.&lt;/p&gt;
&lt;h3 id=&quot;why-shard-data-&quot;&gt;Why Shard Data?&lt;/h3&gt;
&lt;p&gt;As an application grows, the number of users and the amount of data it stores increase over time. At a certain limit, a single database server will become a bottleneck. The application slows down and affects the user experience.&lt;/p&gt;
&lt;p&gt;Instead of upgrading a single server with more CPU or RAM, which will eventually hit a hardware ceiling, sharding lets you add more servers to your cluster. This allows applications to handle nearly infinite growth in data volume and user traffic.&lt;/p&gt;
&lt;h3 id=&quot;understanding-shards-and-shard-keys&quot;&gt;Understanding Shards and Shard Keys&lt;/h3&gt;
&lt;p&gt;Before we start sharding data, let's first understand what shards and shard keys are.&lt;/p&gt;
&lt;h4 id=&quot;shards&quot;&gt;Shards&lt;/h4&gt;
&lt;p&gt;The partitioned data chunks are called logical shards. The machine that stores the logical shard is called a physical shard or database node. A physical shard can contain multiple logical shards.&lt;/p&gt;
&lt;h4 id=&quot;shard-key&quot;&gt;Shard Key&lt;/h4&gt;
&lt;p&gt;A shard key is used to determine how to partition the dataset. A shard key is a specific column in your dataset that determines which rows of data group together to form a shard.&lt;/p&gt;
&lt;h3 id=&quot;sharding-methods&quot;&gt;Sharding Methods&lt;/h3&gt;
&lt;p&gt;There are a few ways to shard data depending on the application requirements.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Key-based sharding (hash sharding)&lt;/strong&gt;: Hash sharding uses a mathematical formula called a hash function. The hash function takes your shard key and generates a hash value, this hash value determines the exact physical shard where the row will be stored.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Range-based sharding&lt;/strong&gt;: Range-based sharding, or dynamic sharding, splits database rows based on a range of values. Then the database designer assigns a shard key to the respective range.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directory-based sharding&lt;/strong&gt;: Directory sharding uses a lookup table to match database information to the corresponding physical shard. A lookup table is like a table on a spreadsheet that links a database column to a shard key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geo sharding&lt;/strong&gt;: Geo sharding splits and stores database information according to geographical location.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For this guide, we are focusing exclusively on &lt;strong&gt;Hash Sharding&lt;/strong&gt;. This is the industry standard for multi-tenant applications like the e-commerce platform we will be building in this tutorial.&lt;/p&gt;
&lt;h3 id=&quot;choosing-your-shard-key&quot;&gt;Choosing Your Shard Key&lt;/h3&gt;
&lt;p&gt;If you pick the wrong shard key, your database will still crash under load. When choosing a shard key, you must ensure the data is distributed evenly across your nodes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cardinality&lt;/strong&gt;: This refers to the number of unique values a key can have. You want a key with high cardinality like a User ID, not low cardinality like a Gender or State column, so the data can be split into many small, even chunks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frequency&lt;/strong&gt;: You must avoid keys where one specific value appears too often. For example, if you shard by a City column and 80% of your users live in Ikeja, the Ikeja node will be overloaded while the others sit idle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monotonic change&lt;/strong&gt;: This is the rate of change of the shard key. A monotonically increasing or decreasing shard key results in unbalanced shards. For example, consider a feedback database that is split into three different physical shards as follows:&lt;ul&gt;
&lt;li&gt;Shard A stores feedback from customers who have made 0 to 10 purchases.&lt;/li&gt;
&lt;li&gt;Shard B stores feedback from customers who have made 11 to 20 purchases.&lt;/li&gt;
&lt;li&gt;Shard C stores feedback from customers who have made 21 or more purchases.&lt;br&gt;
As the business grows, customers will make 21 or more purchases. The application stores their feedback in Shard C. This results in an unbalanced shard because Shard C contains more feedback records than the other shards.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;is-sharding-actually-for-you-&quot;&gt;Is Sharding Actually For You?&lt;/h3&gt;
&lt;p&gt;While sharding solves massive scale requirements, it introduces significant technical and operational tradeoffs. You should not shard your database unless a single, heavily provisioned Postgres instance can no longer handle your traffic.&lt;/p&gt;
&lt;p&gt;If you do decide to shard your database, here are some of the biggest challenges you will face:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data hotspots&lt;/strong&gt;: Even with a good hash function, uneven traffic can create hotspots where one server becomes overloaded by a small group of highly active users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance regression&lt;/strong&gt;: Queries that do not use the shard key require the system to scatter and gather across every server. Cross-shard joins are computationally expensive because the application must pull and merge data from multiple physical locations over the network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational complexity&lt;/strong&gt;: Managing a sharded environment increases the difficulty of routine tasks. Schema updates, backups, and point-in-time recovery must be coordinated across multiple independent instances.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;how-can-we-shard-our-postgres-database-introducing-citus&quot;&gt;How Can We Shard Our Postgres Database: Introducing Citus&lt;/h3&gt;
&lt;p&gt;Citus is an open-source extension that transforms standard PostgreSQL into a distributed database. Instead of being limited to a single server, Citus allows your database to scale horizontally across multiple machines.&lt;/p&gt;
&lt;h4 id=&quot;how-citus-works&quot;&gt;How Citus Works&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Coordinator Node&lt;/strong&gt;: When you ask your database a question, you talk to the coordinator. The coordinator directs your request to the right servers, gathers the answers, and combines them for your application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker Nodes&lt;/strong&gt;: Citus breaks down large tables into smaller, manageable chunks (shards) and spreads them across these worker servers. They do the heavy lifting of storing the data and running the queries.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;start-early-scale-later&quot;&gt;Start Early, Scale Later&lt;/h4&gt;
&lt;p&gt;Migrating a traditional, monolithic PostgreSQL database to a distributed, sharded database later in its life is incredibly difficult. You have to change your schema, rewrite complex queries, and alter how your application connects to the database.&lt;/p&gt;
&lt;p&gt;To avoid this nightmare, you can start using Citus on a single node from day one. You design your tables with distribution keys immediately, and your application code is written to query a distributed database from the start.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Transition&lt;/strong&gt;: When your data outgrows that single machine, you won't have to rewrite your application. You simply spin up new worker machines, and Citus moves the existing shards over to the new hardware in the background while the application keeps running.&lt;/p&gt;
&lt;h3 id=&quot;running-citus-locally&quot;&gt;Running Citus Locally&lt;/h3&gt;
&lt;p&gt;We will use Docker to run Citus locally. Why use Docker instead of adding the extension directly to your host machine's Postgres installation?&lt;/p&gt;
&lt;p&gt;Simulating a distributed database means running multiple Postgres instances simultaneously. Doing this natively forces you to manually manage conflicting ports and multiple data directories. Docker Compose solves this by orchestrating the entire cluster's networking with a single command, giving you a clean state that can be torn down and rebuilt in seconds.&lt;/p&gt;
&lt;h4 id=&quot;single-node-vs-multi-node-setup&quot;&gt;Single Node vs Multi-Node Setup&lt;/h4&gt;
&lt;p&gt;While it is possible to run a single node Citus instance using a simple &lt;code&gt;docker run&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d --name citus_standalone -p 5432:5432 -e POSTGRES_PASSWORD=postgres citusdata/citus:14.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This guide will focus exclusively on setting up a &lt;strong&gt;Multi-Node Cluster&lt;/strong&gt;. If you are interested in exploring the single node setup further, you can &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/postgresql/citus/single-node-docker?view=citus-14&quot;&gt;check out the official Citus documentation here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For our distributed, multi-node cluster, we will use the following Docker Compose configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  master:
    container_name: &quot;${PROJECT_NAME:-citus}_master&quot;
    image: &quot;citusdata/citus:14.1.0&quot;
    ports: [&quot;${COORDINATOR_EXTERNAL_PORT:-5433}:5432&quot;]
    labels: [&quot;com.citusdata.role=Master&quot;]
    environment: &amp;amp;AUTH
      POSTGRES_USER: &quot;${POSTGRES_USER:-postgres}&quot;
      POSTGRES_PASSWORD: &quot;${POSTGRES_PASSWORD}&quot;
      PGUSER: &quot;${POSTGRES_USER:-postgres}&quot;
      PGPASSWORD: &quot;${POSTGRES_PASSWORD}&quot;
      POSTGRES_HOST_AUTH_METHOD: &quot;${POSTGRES_HOST_AUTH_METHOD:-trust}&quot;
  worker:
    image: &quot;citusdata/citus:14.1.0&quot;
    labels: [&quot;com.citusdata.role=Worker&quot;]
    environment: *AUTH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Citus has a built-in initialization script in its official Docker image. When launched via Docker Compose with these specific labels like &lt;code&gt;com.citusdata.role&lt;/code&gt;, the coordinator automatically detects and registers the workers for you.&lt;/p&gt;
&lt;p&gt;We can build and start the cluster in the background with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can also scale up dynamically. If you want to spin up a second or third worker machine on the fly, you do not even need to edit the compose file. Just run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d --scale worker=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where 3 is the total number of workers you want to have running.&lt;/p&gt;
&lt;p&gt;Simply spinning up the worker containers is not enough. After adding new worker nodes, we must register them with the coordinator so Citus knows they are active and ready to hold data. To do this, we will first connect to the interactive Postgres terminal on the master node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose exec master psql -U postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can register our workers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT citus_add_node('db-sharding-worker-1', 5432);
SELECT citus_add_node('db-sharding-worker-2', 5432);
SELECT citus_add_node('db-sharding-worker-3', 5432);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, if you scale up again to say 5 workers, you will have to run this command 2 more times to register the 2 new workers.&lt;/p&gt;
&lt;p&gt;Notice the &lt;code&gt;db-sharding-&lt;/code&gt; prefix in the hostnames. This is necessary because Docker Compose prefixes your project folder name to the container's network alias.&lt;/p&gt;
&lt;h3 id=&quot;a-real-world-scenario&quot;&gt;A Real World Scenario&lt;/h3&gt;
&lt;p&gt;In order to showcase the functionality of this database sharding setup, let's try to work with a real world scenario.&lt;/p&gt;
&lt;p&gt;Consider an e-commerce multi-tenant application. It serves multiple independent companies that all share the same database infrastructure. To scale this effectively, we need to use a hash-based distribution with a well-chosen shard key. Let's assume by design, every tenant has a shared &lt;code&gt;company_id&lt;/code&gt;, we will use that as our shard key.&lt;/p&gt;
&lt;p&gt;By using &lt;code&gt;company_id&lt;/code&gt; as the shard key across all operational tables, Citus guarantees that all data belonging to a specific company (like its users, its products, and its orders) is physically co-located on the exact same worker node.&lt;/p&gt;
&lt;p&gt;Now that our workers are registered, we can create our core tables directly in the master node. Citus will automatically handle the underlying sharding for us.&lt;/p&gt;
&lt;p&gt;Please note that the following schema is highly simplified to focus on the core concepts of sharding.&lt;/p&gt;
&lt;p&gt;Run the following SQL commands in your coordinator's postgres interactive terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 1. Create Tenant Table
CREATE TABLE companies (
    company_id INT PRIMARY KEY,
    company_name TEXT NOT NULL
);

-- 2. Create Tenant-Scoped Orders Table
CREATE TABLE orders (
    company_id INT NOT NULL,
    order_id INT NOT NULL,
    user_id INT NOT NULL,
    status_id INT NOT NULL, -- References our global order status table
    order_amount NUMERIC(10,2) NOT NULL,
    PRIMARY KEY (company_id, order_id)
);

-- 3. Create Global Order Reference Table
CREATE TABLE order_statuses (
    status_id INT PRIMARY KEY,
    status_name TEXT NOT NULL
);

-- 4. Create Tenant-Scoped Users Table
CREATE TABLE users (
    company_id INT NOT NULL,
    user_id INT NOT NULL,
    username TEXT NOT NULL,
    PRIMARY KEY (company_id, user_id)
);

-- 5. Create Tenant-Scoped Products Table
CREATE TABLE products (
    company_id INT NOT NULL,
    product_id INT NOT NULL,
    product_name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    PRIMARY KEY (company_id, product_id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;telling-citus-how-to-shard-and-replicate&quot;&gt;Telling Citus How to Shard and Replicate&lt;/h3&gt;
&lt;p&gt;Now that we have created our core tables, we have to tell Citus how to shard and replicate them across our worker nodes.&lt;/p&gt;
&lt;h4 id=&quot;reference-tables&quot;&gt;Reference Tables&lt;/h4&gt;
&lt;p&gt;First, we will set &lt;code&gt;order_statuses&lt;/code&gt; as a reference table. We do this using &lt;code&gt;create_reference_table&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why do we create a reference table? A reference table is designed for relatively small, slowly-changing data like user roles, country codes, or order statuses.&lt;/p&gt;
&lt;p&gt;When you mark a table as a reference table, Citus physically replicates a full copy of that entire table to every single worker node. This bypasses network overhead completely during JOIN operations. Because the worker node already has the data locally, it does not need to fetch it from another node to complete a query.&lt;/p&gt;
&lt;h4 id=&quot;distributed-tables&quot;&gt;Distributed Tables&lt;/h4&gt;
&lt;p&gt;Next, we need to distribute our large tenant-data tables. We do this using &lt;code&gt;create_distributed_table&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This function is the core of Citus's sharding mechanism. It tells the coordinator to partition the table based on a specific column (our shard key, &lt;code&gt;company_id&lt;/code&gt;), and spread those partitions across the available worker nodes.&lt;/p&gt;
&lt;p&gt;Because we use &lt;code&gt;company_id&lt;/code&gt; across all these tables, any query filtering by a specific company will be routed directly to a single node, making it incredibly fast.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Replicate order_statuses to ALL nodes
SELECT create_reference_table('order_statuses');

-- Shard data based on the Hash of company_id
SELECT create_distributed_table('companies', 'company_id');
SELECT create_distributed_table('users', 'company_id');
SELECT create_distributed_table('products', 'company_id');
SELECT create_distributed_table('orders', 'company_id');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;multi-tenant-seed-data&quot;&gt;Multi-Tenant Seed Data&lt;/h3&gt;
&lt;p&gt;Now we will populate our cluster with some dummy companies along with some global order status entries, orders, and tenant users.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Global Order Lookups
INSERT INTO order_statuses VALUES (1, 'Pending'), (2, 'Shipped'), (3, 'Delivered');

-- Company Tenants
INSERT INTO companies VALUES (1, 'EbaMart'), (2, 'FufuHub'), (3, 'GymGear');

-- Multi-Tenant Users scattered across companies
INSERT INTO users (company_id, user_id, username) VALUES
(1, 101, 'Samuel'),   -- EbaMart Customer
(2, 201, 'Folashade'),-- FufuHub Customer
(1, 102, 'Umoh'),     -- EbaMart Customer
(3, 301, 'Oladejo'),  -- GymGear Customer
(2, 202, 'BigSam');   -- FufuHub Customer

-- Orders
INSERT INTO orders (company_id, order_id, user_id, status_id, order_amount) VALUES
(1, 5001, 101, 1, 150.00),  -- EbaMart order: Pending
(1, 5002, 102, 3, 45.00),   -- EbaMart order: Delivered
(2, 6001, 201, 2, 99.00),   -- FufuHub order: Shipped
(2, 6002, 202, 1, 250.00);  -- FufuHub order: Pending
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;verify-layout-and-node-placement&quot;&gt;Verify Layout and Node Placement&lt;/h3&gt;
&lt;p&gt;We can now verify the layout and node placement to see where they are added to and display it by running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
    u.company_id,
    u.user_id,
    u.username,
    s.shardid,
    s.nodename
FROM users u
JOIN citus_shards s
  ON s.shardid = get_shard_id_for_distribution_column('users', u.company_id)
ORDER BY u.company_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;company_id &lt;/th&gt;&lt;th&gt;user_id &lt;/th&gt;&lt;th&gt;username  &lt;/th&gt;&lt;th&gt;shardid &lt;/th&gt;&lt;th&gt;nodename&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;101     &lt;/td&gt;&lt;td&gt;Samuel    &lt;/td&gt;&lt;td&gt;102042  &lt;/td&gt;&lt;td&gt;db-sharding-worker-2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1          &lt;/td&gt;&lt;td&gt;102     &lt;/td&gt;&lt;td&gt;Umoh      &lt;/td&gt;&lt;td&gt;102042  &lt;/td&gt;&lt;td&gt;db-sharding-worker-2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2          &lt;/td&gt;&lt;td&gt;201     &lt;/td&gt;&lt;td&gt;Folashade &lt;/td&gt;&lt;td&gt;102065  &lt;/td&gt;&lt;td&gt;db-sharding-worker-1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2          &lt;/td&gt;&lt;td&gt;202     &lt;/td&gt;&lt;td&gt;BigSam    &lt;/td&gt;&lt;td&gt;102065  &lt;/td&gt;&lt;td&gt;db-sharding-worker-1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3          &lt;/td&gt;&lt;td&gt;301     &lt;/td&gt;&lt;td&gt;Oladejo   &lt;/td&gt;&lt;td&gt;102056  &lt;/td&gt;&lt;td&gt;db-sharding-worker-1&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As you can see, all data for &lt;code&gt;company_id = 1&lt;/code&gt; sits neatly on &lt;code&gt;db-sharding-worker-2&lt;/code&gt;, while &lt;code&gt;company_id = 2&lt;/code&gt; sits on &lt;code&gt;db-sharding-worker-1&lt;/code&gt;. This guarantees that queries for a single tenant will only hit one node.&lt;/p&gt;
&lt;p&gt;We can also inspect the query plan for joining distributed tables with reference tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN (VERBOSE, COSTS OFF)
SELECT o.order_id, o.order_amount, s.status_name
FROM orders o
JOIN order_statuses s ON o.status_id = s.status_id
WHERE o.company_id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                                      QUERY PLAN
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------
------------------------
 Custom Scan (Citus Adaptive)
   Output: remote_scan.order_id, remote_scan.order_amount, remote_scan.status_name
   Task Count: 1
   Tasks Shown: All
   -&amp;gt;  Task
         Query: SELECT o.order_id, o.order_amount, s.status_name FROM (public.orders_102106 o JOIN public.order_statuses_102008 s ON ((o.status_id OPERATOR(pg_catalog.=) s.status_id))) WHERE (o.company_id OPERATOR(pg_catalog.=) 1)
         Node: host=db-sharding-worker-2 port=5432 dbname=postgres
         -&amp;gt;  Hash Join
               Output: o.order_id, o.order_amount, s.status_name
               Hash Cond: (s.status_id = o.status_id)
               -&amp;gt;  Seq Scan on public.order_statuses_102008 s
                     Output: s.status_id, s.status_name
               -&amp;gt;  Hash
                     Output: o.order_id, o.order_amount, o.status_id
                     -&amp;gt;  Bitmap Heap Scan on public.orders_102106 o
                           Output: o.order_id, o.order_amount, o.status_id
                           Recheck Cond: (o.company_id = 1)
                           -&amp;gt;  Bitmap Index Scan on orders_pkey_102106
                                 Index Cond: (o.company_id = 1)
(19 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query plan shows a &lt;code&gt;Custom Scan (Citus Adaptive)&lt;/code&gt; delegating the query to the specific worker node holding &lt;code&gt;company_id = 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can check the active worker nodes with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM master_get_active_worker_nodes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;node_name            &lt;/th&gt;&lt;th&gt;node_port&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;db-sharding-worker-3 &lt;/td&gt;&lt;td&gt;5432&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;db-sharding-worker-1 &lt;/td&gt;&lt;td&gt;5432&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;db-sharding-worker-2 &lt;/td&gt;&lt;td&gt;5432&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;rebalancing-the-cluster&quot;&gt;Rebalancing the Cluster&lt;/h3&gt;
&lt;p&gt;Anytime you dynamically scale the cluster and register a new worker node, you must run the Citus Shard Rebalancer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT citus_rebalance_start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When new nodes are added, the coordinator does not automatically move existing data onto them. We have to explicitly balance the load. The rebalancer handles this by dynamically moving a subset of your tenants' data over to the new worker servers in the background, without taking the database offline.&lt;/p&gt;
&lt;h3 id=&quot;production-setup&quot;&gt;Production Setup&lt;/h3&gt;
&lt;p&gt;The Docker Compose setup is perfect for local testing and development. However, running a distributed database in production across multiple physical servers or virtual machines requires a slightly different approach.&lt;/p&gt;
&lt;p&gt;We must first change how network addressing and orchestration work. In a local setup, Docker Compose handles networking automatically because everything shares a single machine loopback. On production VMs, each container must be bound to the public or private IP address of its physical host machine, and you must enforce strict firewall security.&lt;/p&gt;
&lt;h4 id=&quot;1-spin-up-your-vms&quot;&gt;1. Spin up your VMs&lt;/h4&gt;
&lt;p&gt;Spin up your virtual machines on your preferred cloud provider. Assuming we provision 4 VMs, our network layout will look something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VM 1 (Coordinator Node)&lt;/strong&gt;: Private IP 10.0.0.10&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VM 2 (Worker Node 1)&lt;/strong&gt;: Private IP 10.0.0.11&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VM 3 (Worker Node 2)&lt;/strong&gt;: Private IP 10.0.0.12&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VM 4 (Worker Node 3)&lt;/strong&gt;: Private IP 10.0.0.13&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I used random private IPs, but you should substitute them with your real VMs' private IPs.&lt;/p&gt;
&lt;h4 id=&quot;2-install-docker&quot;&gt;2. Install Docker&lt;/h4&gt;
&lt;p&gt;Next, you will need to install Docker and Docker Compose on every VM. The installation commands will depend on your specific Linux distribution, so refer to the official Docker documentation for your OS.&lt;/p&gt;
&lt;p&gt;You will no longer use a single master compose file for the cluster. Instead, you deploy an isolated, optimized compose layout directly on each corresponding VM instance.&lt;/p&gt;
&lt;h4 id=&quot;3-the-coordinator-instance-vm-1-&quot;&gt;3. The Coordinator Instance (VM 1)&lt;/h4&gt;
&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file specifically tailored to map the Postgres engine port directly out to the host VM network interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  master:
    container_name: &quot;citus_coordinator&quot;
    image: &quot;citusdata/citus:14.1.0&quot;
    ports:
      - &quot;5432:5432&quot;
    restart: always
    environment:
      POSTGRES_USER: &quot;citus_admin&quot;
      POSTGRES_PASSWORD: &quot;YOUR_SUPER_SECURE_PRODUCTION_PASSWORD&quot;
      PGUSER: &quot;citus_admin&quot;
      PGPASSWORD: &quot;YOUR_SUPER_SECURE_PRODUCTION_PASSWORD&quot;
      POSTGRES_HOST_AUTH_METHOD: &quot;scram-sha-256&quot;
    volumes:
      - coordinator_data:/var/lib/postgresql/data

volumes:
  coordinator_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-the-worker-instances-vms-2-3-and-4-&quot;&gt;4. The Worker Instances (VMs 2, 3, and 4)&lt;/h4&gt;
&lt;p&gt;Every worker VM gets its own standalone compose file. Notice we expose port 5432 on the workers too, because the coordinator node needs a direct network pipe to talk to them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  worker:
    container_name: &quot;citus_worker&quot;
    image: &quot;citusdata/citus:14.1.0&quot;
    ports:
      - &quot;5432:5432&quot;
    restart: always
    environment:
      POSTGRES_USER: &quot;citus_admin&quot;
      POSTGRES_PASSWORD: &quot;YOUR_SUPER_SECURE_PRODUCTION_PASSWORD&quot;
      PGUSER: &quot;citus_admin&quot;
      PGPASSWORD: &quot;YOUR_SUPER_SECURE_PRODUCTION_PASSWORD&quot;
      POSTGRES_HOST_AUTH_METHOD: &quot;scram-sha-256&quot;
    volumes:
      - worker_data:/var/lib/postgresql/data

volumes:
  worker_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-configure-network-firewall-security&quot;&gt;5. Configure Network Firewall Security&lt;/h4&gt;
&lt;p&gt;Since you are running across independent cloud instances, your cloud platform's Security Groups or Firewalls (like AWS Security Groups or ufw on Ubuntu) must be strictly configured to prevent public database scanning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On the Coordinator VM (VM 1)&lt;/strong&gt;: Allow inbound TCP traffic on port 5432 from Anywhere (0.0.0.0/0) only if your external application servers need public access. If your application servers are in the same VPC, restrict inbound port 5432 access exclusively to your app servers' IP subnet range.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On the Worker VMs (VMs 2, 3, 4)&lt;/strong&gt;: Allow inbound TCP traffic on port 5432 EXCLUSIVELY from the Coordinator's Private IP (10.0.0.10). Block all other inbound network requests to the workers. They should never be exposed to the internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, log in to each VM and run the containers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once all containers are healthy, access the interactive postgres interface on VM 1 (the coordinator node):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose exec master psql -U citus_admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register your external worker instances using their physical Private VPC IP addresses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT citus_add_node('10.0.0.11', 5432); -- Register Worker VM 2
SELECT citus_add_node('10.0.0.12', 5432); -- Register Worker VM 3
SELECT citus_add_node('10.0.0.13', 5432); -- Register Worker VM 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify everything works:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM master_get_active_worker_nodes();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;6-configure-production-password-authentication&quot;&gt;6. Configure Production Password Authentication&lt;/h4&gt;
&lt;p&gt;Because we updated &lt;code&gt;POSTGRES_HOST_AUTH_METHOD&lt;/code&gt; to &lt;code&gt;scram-sha-256&lt;/code&gt; for security, your instances will require passwords to authenticate cross-node.&lt;/p&gt;
&lt;p&gt;We must add a Postgres password passfile on the coordinator container:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose exec master bash -c &quot;echo '*:*:*:citus_admin:YOUR_SUPER_SECURE_PRODUCTION_PASSWORD' &amp;gt;&amp;gt; /var/lib/postgresql/.pgpass &amp;amp;&amp;amp; chmod 600 /var/lib/postgresql/.pgpass&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Surely you will know to replace &lt;code&gt;YOUR_SUPER_SECURE_PRODUCTION_PASSWORD&lt;/code&gt; with your actual password.&lt;/p&gt;
&lt;p&gt;Here is a detailed documentation of Citus multi-node installation and setup for reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/postgresql/citus/multi-node-ubuntu-debian?view=citus-14&quot;&gt;Citus Multi-Node Setup&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;adding-citus-to-an-existing-project&quot;&gt;Adding Citus to an Existing Project&lt;/h3&gt;
&lt;p&gt;To add Citus to an already existing Postgres database project, the process follows these general steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Install the Extension&lt;/strong&gt;: Install the Citus extension on your Postgres server. You will also need to add &lt;code&gt;citus&lt;/code&gt; to the &lt;code&gt;shared_preload_libraries&lt;/code&gt; in your &lt;code&gt;postgresql.conf&lt;/code&gt; file and restart the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create the Extension&lt;/strong&gt;: Run &lt;code&gt;CREATE EXTENSION citus;&lt;/code&gt; in your database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select a Shard Key&lt;/strong&gt;: Identify the column that will serve as the shard key for your distributed tables, such as a company or tenant ID.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convert Tables&lt;/strong&gt;: Use &lt;code&gt;create_distributed_table()&lt;/code&gt; to convert your existing tables into distributed tables. Use &lt;code&gt;create_reference_table()&lt;/code&gt; for lookup tables that do not need to be sharded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate Data&lt;/strong&gt;: If you already have data in the tables, it might need to be exported and loaded back in, depending on the volume of data and the specific requirements of the migration.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;conclusion-6&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Database sharding is an incredibly powerful architectural pattern, but it comes with immense operational tradeoffs. By using tools like Citus, we can bridge the gap between standard relational SQL databases and massively distributed systems.&lt;/p&gt;
&lt;p&gt;Whether you are starting a new multi-tenant application or scaling out an existing infrastructure, adopting a distributed mindset and orchestrating your local environments with tools like Citus and Docker Compose will save you countless hours of architectural nightmares down the road.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can find the full Docker Compose source code for this tutorial on GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/umohsamuel/database-sharding&quot;&gt;umohsamuel/database-sharding&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;references-1&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cloud.google.com/discover/what-is-database-sharding&quot;&gt;What is Database Sharding? (Google Cloud)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/what-is/database-sharding/&quot;&gt;What is Database Sharding? (AWS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/citusdata/docker/blob/master/docker-compose.yml&quot;&gt;Citus Docker Compose Setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28711/horizontally-scaling-postgresql-database-sharding-with-citus</guid>
<pubDate>Sat, 26 Sep 2026 10:35:35 +0000</pubDate>
</item>
<item>
<title>Stop Killing Your Database: The Power of Connection Pooling</title>
<link>https://coderlegion.com/28133/stop-killing-your-database-the-power-of-connection-pooling</link>
<description>&lt;p&gt;Connection pooling is a software development technique used to maintain a cache of database connections that can be reused for future requests. Instead of opening and closing a brand-new connection to the database every time a user wants to read or write data, your application keeps a &quot;pool&quot; of active connections ready to go. This drastically reduces the overhead of establishing new database handshakes over and over again.&lt;/p&gt;
&lt;h4 id=&quot;the-pizza-delivery-analogy&quot;&gt;The Pizza Delivery Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you run a busy pizza shop. Every time a delivery order comes in, instead of sending a driver who is already waiting at the shop, you post a job listing online, interview a candidate, hire them, buy them a car, have them deliver the pizza, and then fire them and sell the car immediately after. &lt;/p&gt;
&lt;p&gt;It sounds completely insane, right? Yet, that is exactly what your server does when it creates a brand-new database connection for every single HTTP request, only to destroy it a millisecond later. &lt;/p&gt;
&lt;p&gt;Connection pooling is like hiring a dedicated team of five delivery drivers who hang out at the shop. When an order is ready, one driver grabs it, delivers it, and immediately returns to the shop to wait for the next order.&lt;/p&gt;
&lt;h4 id=&quot;why-connection-pooling-matters-daily&quot;&gt;Why Connection Pooling Matters Daily&lt;/h4&gt;
&lt;p&gt;In production environments, creating a database connection is an expensive operation that involves cryptographic handshakes, network latency, and authentication checks. If your Node.js application receives 100 concurrent API requests and tries to spin up 100 separate database connections simultaneously, your MySQL database will quickly run out of memory or reject connections altogether, throwing the dreaded &quot;Too many connections&quot; error.&lt;/p&gt;
&lt;p&gt;By implementing a connection pool, you cap the maximum number of active database connections, protect your database from crashing under sudden traffic spikes, and shave off up to 90% of your API response latency because the connection is already warmed up and ready to run queries.&lt;/p&gt;
&lt;h4 id=&quot;implementation-in-node-js-amp-mysql&quot;&gt;Implementation in Node.js &amp;amp; MySQL&lt;/h4&gt;
&lt;p&gt;Here is how simple it is to set up a connection pool in an Express.js backend using the popular &lt;code&gt;mysql2&lt;/code&gt; driver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');

const app = express();

// Create a connection pool instead of a single connection
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'store_db',
  waitForConnections: true,
  connectionLimit: 10, // Max 10 active connections kept in the pool
  queueLimit: 0
});

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // The pool automatically rents out a connection, runs the query,
    // and returns the connection back to the pool when finished.
    const [rows] = await pool.query('SELECT * FROM products LIMIT 10');
    res.json(rows);
  } catch (error) {
    console.error('Database query error:', error);
    res.status(500).send('Database error');
  }
});

app.listen(3000, () =&amp;gt; {
  console.log('Server running on port 3000');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Optimizing your backend performance isn't always about writing faster algorithms; often, it's about managing your external resources wisely. By treating database connections as a reusable, limited public utility rather than disposable single-use items, you build highly resilient APIs capable of weathering massive traffic spikes without sweating.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-connection-pooling-can-prevent-your.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28133/stop-killing-your-database-the-power-of-connection-pooling</guid>
<pubDate>Tue, 22 Sep 2026 08:51:06 +0000</pubDate>
</item>
<item>
<title>Why I Built EasyDown: Normalizing Public Media Across Social Platforms</title>
<link>https://coderlegion.com/27934/why-i-built-easydown-normalizing-public-media-across-social-platforms</link>
<description>&lt;p&gt;I started building EasyDown after running into the same integration problem repeatedly: a user would paste a perfectly valid public social media URL, but every platform represented the media differently. A Reel was not shaped like a TikTok post, an Instagram carousel was not shaped like a Bilibili video, and even two URLs from the same platform could require different handling.&lt;/p&gt;
&lt;p&gt;The first version looked simple. Detect the host, call a platform-specific parser, and return a video URL. That design stopped working as soon as I tested real posts. Some posts contained images rather than video. Some were mixed carousels. Some video formats separated audio and video. CDN links expired. Browser requests failed because of CORS or missing headers. A profile URL looked similar to a post URL but represented a completely different resource.&lt;/p&gt;
&lt;p&gt;I decided that the useful product was not another collection of one-off download scripts. I needed a stable backend contract that could represent media from several public platforms without forcing every client to learn each upstream response format.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-i-wanted-the-api-to-solve&quot;&gt;The problem I wanted the API to solve&lt;/h3&gt;
&lt;p&gt;From an application developer's perspective, the input should be one supported public post URL. The result should be a predictable object containing the platform, title, thumbnail, duration when available, and arrays for images, videos, and audio.&lt;/p&gt;
&lt;p&gt;The arrays matter. Assuming that one post equals one MP4 is an easy mistake. An Instagram carousel may contain several photos and videos in a meaningful order. A TikTok photo post may have no video at all. A Bilibili response may expose multiple stream options. Returning a collection lets the product decide whether to show a picker, enqueue every item, or select a preferred rendition.&lt;/p&gt;
&lt;p&gt;I also wanted failures to be explicit. Private posts, deleted media, expired Stories, login-only pages, profiles, live content, and region-restricted items should not be presented as successful parses. A client needs to distinguish an invalid input from a temporary upstream failure and from a valid post that has no downloadable media.&lt;/p&gt;
&lt;h3 id=&quot;one-normalized-endpoint-and-platform-specific-endpoints&quot;&gt;One normalized endpoint and platform-specific endpoints&lt;/h3&gt;
&lt;p&gt;EasyDown now has a common endpoint for applications that want one stable media model across supported networks. It also has platform-specific endpoints for integrations that need additional public fields from a particular service.&lt;/p&gt;
&lt;p&gt;Instagram is a good example. The normalized media object is enough for a download workflow, but some applications also need the public shortcode, caption, author fields, image candidates, video renditions, or carousel items. I documented the current request and response contract in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://easydown.org/docs/en/api/instagram&quot;&gt;Instagram API documentation I maintain&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The platform request is a normal authenticated POST from a backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import requests

response = requests.post(
    &quot;https://api.easydown.org/api/v1/platforms/instagram/parse&quot;,
    headers={
        &quot;Authorization&quot;: &quot;Bearer ed_live_your_token&quot;,
        &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    json={
        &quot;url&quot;: &quot;https://www.instagram.com/reel/DY2PcmxiBE2/&quot;
    },
    timeout=30,
)

response.raise_for_status()
payload = response.json()
media = payload[&quot;data&quot;][&quot;media&quot;]

for item in media.get(&quot;videos&quot;, []):
    print(item[&quot;url&quot;], item.get(&quot;quality&quot;), item.get(&quot;mimeType&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The token stays on the server. I do not recommend placing a production token in browser JavaScript, a URL, analytics, or a public repository.&lt;/p&gt;
&lt;h3 id=&quot;platforms-i-support&quot;&gt;Platforms I support&lt;/h3&gt;
&lt;p&gt;The current API covers public post URLs from TikTok, Douyin, Toutiao, Instagram, Threads, Xiaohongshu or RedNote, Bilibili, Kuaishou or Kwai, Weibo, X or Twitter, and YouTube.&lt;/p&gt;
&lt;p&gt;Support does not mean scraping every page on those sites. The contract is intentionally scoped to documented single-post URL formats and compatible share links. Profiles, search pages, private messages, private media, arbitrary bulk collection, and unavailable content are outside that scope.&lt;/p&gt;
&lt;p&gt;Keeping the boundary narrow has made the API easier to reason about. Every new URL form has to pass through validation, platform detection, parsing, normalization, sensitive-field filtering, and media checks before the response is considered successful.&lt;/p&gt;
&lt;h3 id=&quot;lessons-from-building-the-media-layer&quot;&gt;Lessons from building the media layer&lt;/h3&gt;
&lt;p&gt;The hardest production issue has been treating returned media URLs as temporary resources. Many social media CDNs use signatures or expiration parameters. Storing the URL as though it were a permanent asset creates jobs that fail hours or days later.&lt;/p&gt;
&lt;p&gt;My current rule is to keep the original public post URL as the durable reference. A backend parses it when the application needs fresh media candidates, then downloads or proxies the selected file while it is valid. When a CDN URL expires, the application re-parses the post instead of retrying the expired URL indefinitely.&lt;/p&gt;
&lt;p&gt;The second lesson is that browser playback and backend downloading are different operations. A URL that opens in one browser session can still fail in application code because of CORS, CORP, cookies, Referer requirements, or a short-lived signature. Media fetching belongs in the backend when the product needs predictable behavior.&lt;/p&gt;
&lt;p&gt;The third lesson is to normalize conservatively. I keep stable cross-platform fields small and predictable, while versioning the extra platform data separately. That approach gives client applications a dependable base model without discarding useful public fields that only exist on one platform.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-test-before-shipping&quot;&gt;What I would test before shipping&lt;/h3&gt;
&lt;p&gt;For an Instagram integration, I would test a public Reel, a video post, a single-image post, a mixed carousel, a compatible share URL, and an unavailable post. I would also test an expired CDN URL and confirm that the application re-parses the original post.&lt;/p&gt;
&lt;p&gt;Across every platform, I check that credentials remain server-side, errors do not leak tokens, media arrays can be empty, and the UI does not promise access to private or protected content. Those cases are less exciting than the first successful request, but they decide whether an integration survives real users.&lt;/p&gt;
&lt;p&gt;Building EasyDown has mostly been an exercise in turning many unstable upstream shapes into one smaller contract that an application can trust. The parsing code still changes as platforms change. The goal of the API is to keep those changes behind a stable boundary so client code does not have to change with them.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27934/why-i-built-easydown-normalizing-public-media-across-social-platforms</guid>
<pubDate>Mon, 21 Sep 2026 11:12:52 +0000</pubDate>
</item>
<item>
<title>Jev by TypeSafe AI: Hype, Tool or Threat?</title>
<link>https://coderlegion.com/27909/jev-by-typesafe-ai-hype-tool-or-threat</link>
<description>&lt;p&gt;A few days ago, TypeSafe AI introduced &lt;a rel=&quot;nofollow&quot; href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;Jev&lt;/a&gt;, a new model designed to make fast, structured decisions that software can use directly.&lt;/p&gt;
&lt;p&gt;I read the announcement and had two questions: Is Jev more of a hype? Is it really useful for me as a backend engineer? And does it present a new threat of replacing me as a software engineer? Jev also reminded me of the OpenAI REST API &quot;structured outputs&quot; endpoints. I wondered whether Jev would offer any advantages over OpenAI.&lt;/p&gt;
&lt;p&gt;Here is what I found.&lt;/p&gt;
&lt;h3 id=&quot;what-does-jev-actually-do&quot;&gt;What Does Jev Actually Do&lt;/h3&gt;
&lt;p&gt;Jev (by TypeSafe AI) belongs to a class of narrowly specialized models that provide structured outputs rather than just prose. It accepts application state as input and returns structured decisions. The possible outputs and their types are defined in advance.&lt;/p&gt;
&lt;p&gt;For example, an application could provide information about a customer and ask Jev to classify the customer's churn risk, determine which support department should handle a request, or score a potential business opportunity. The application receives predefined values that it can use in its business logic.&lt;/p&gt;
&lt;p&gt;Very similar to OpenAI &quot;structured outputs&quot;, I think.&lt;/p&gt;
&lt;p&gt;TypeSafe AI claims that Jev provides several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Response times of 70–500 ms.&lt;/li&gt;
&lt;li&gt;Input costs of $0.042 per million tokens.&lt;/li&gt;
&lt;li&gt;Guaranteed adherence to predefined output types.&lt;/li&gt;
&lt;li&gt;Calibrated probabilities and confidence scores accompanying its decisions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The company also claims substantial improvements in speed and cost compared with existing LLMs performing similar tasks.&lt;/p&gt;
&lt;p&gt;These are the company's published results. Jev is currently in early access, and I haven't tested it myself.&lt;/p&gt;
&lt;h3 id=&quot;jev-vs-openai-structured-outputs&quot;&gt;Jev vs OpenAI Structured Outputs&lt;/h3&gt;
&lt;p&gt;I have already worked with OpenAI's API to implement structured classification and decision-making workflows. OpenAI introduced Structured Outputs in August 2024. With &lt;code&gt;strict: true&lt;/code&gt;, developers can define a JSON Schema and receive responses conforming to that schema.&lt;/p&gt;
&lt;p&gt;We can already classify incoming data, select values from predefined enums, extract structured information, and use the results in application code. ([OpenAI][2]). Jev provides essentially the same capabilities for these use cases.&lt;/p&gt;
&lt;p&gt;Jev's underlying generation mechanism (it generates its decision outputs in parallel and provides associated probabilities - confidence scores) is interesting, but there is little reason to judge Jev's advantage based on its implementation details.&lt;/p&gt;
&lt;p&gt;The practical question for a backend engineer is whether Jev produces decisions of comparable accuracy, faster and at a lower cost, for the same workload. They claim it has an edge in this. But with OpenAI Structured Outputs we can select the seriously cheaper model still very capable of producing structured outputs with a large focused (cached) context.&lt;/p&gt;
&lt;h3 id=&quot;jev-useful-in-real-backend-systems-&quot;&gt;Jev: Useful in Real Backend Systems?&lt;/h3&gt;
&lt;p&gt;At one innovation analytics startup, I worked on backend architecture and LLM-powered ingestion and scoring pipelines. The system needed to process incoming information, classify it using controlled vocabularies, and produce structured results that could be used by subsequent application components.&lt;/p&gt;
&lt;p&gt;I implemented deterministic validation gates to check model outputs before allowing the code to proceed through the workflow. Looking at Jev's capabilities, I can see where it could fit into this architecture. For example, I could use it to classify incoming records or calculate particular scores.&lt;/p&gt;
&lt;p&gt;I had great results with &lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.openai.com/api/docs/models/gpt-4o-mini&quot;&gt;GPT-4o Mini&lt;/a&gt; (quite cheaper than GPT Luna that TypeSafe AI compares Jev with) with latencies well within 100ms and costs like $0.15 per 1,000 requests of ~2K prompt and ~700 bytes JSON response.&lt;/p&gt;
&lt;p&gt;Separately, Jev's confidence scores may seem to be useful for workflows where an application needs to decide whether to accept a model's decision or send the case for further processing. But these scores can be equally misleading requiring additional deterministic validation gates, thus adding the complexity to a workflow.&lt;/p&gt;
&lt;p&gt;My conclusion is Jev deserves some A/B testing under realistic production workloads vs OpenAI structured outputs before any decision. With millions of requests it may offer an advantage. As well its confidence scores may be used in some cases.&lt;/p&gt;
&lt;h3 id=&quot;a-tool-or-an-engineer-s-replacement-&quot;&gt;A Tool Or an Engineer's Replacement.&lt;/h3&gt;
&lt;p&gt;This was my original concern. A usual senior backend engineer's work involves designing application architecture, domain models and service boundaries, implementing APIs, working with databases, and building distributed systems, and often, implementing business workflows that involve classification, scoring, and other decisions based on incoming data.&lt;/p&gt;
&lt;p&gt;Jev could automate some decision-making operations. As well as OpenAI structured outputs does. But hardly presents any threat to the entire engineering process. So the answer is &quot;a tool&quot;, that has had alternatives for already ~2 years.&lt;/p&gt;
&lt;h3 id=&quot;my-conclusion&quot;&gt;My Conclusion&lt;/h3&gt;
&lt;p&gt;After examining Jev and comparing it with OpenAI's existing Structured Outputs, I see it as a potentially useful addition to my backend engineering toolkit with advantages that have yet to be proven for production systems already using OpenAI structured outputs.&lt;/p&gt;
&lt;p&gt;For now, I am interested in seeing how Jev performs on real business workloads and whether its claimed performance improvements justify replacing existing LLM calls in production systems.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27909/jev-by-typesafe-ai-hype-tool-or-threat</guid>
<pubDate>Mon, 21 Sep 2026 09:42:28 +0000</pubDate>
</item>
<item>
<title>HTTP Just Got a New Method After 16 Years—Meet QUERY. </title>
<link>https://coderlegion.com/27817/http-just-got-a-new-method-after-16-years-meet-query</link>
<description>&lt;p&gt;&lt;strong&gt; The New HTTP QUERY Method: The First New HTTP Method in 16 Years&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What if you could have the simplicity of GET with the flexibility of POST? That's exactly what the new HTTP QUERY method aims to deliver.&lt;/p&gt;
&lt;p&gt;If you've built REST APIs, you've probably faced this situation before.&lt;/p&gt;
&lt;p&gt;You need to fetch data from your server, so naturally, GET seems like the right choice. But then your filters become more complex. You need nested objects, multiple conditions, sorting, pagination, maybe even advanced search criteria. Suddenly, your URL starts looking like a puzzle.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /orders?status=paid&amp;amp;customer=Ravi&amp;amp;from=2026-01-01&amp;amp;to=2026-06-30&amp;amp;sort=-amount&amp;amp;page=2&amp;amp;include=products,shipping,payments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As the application grows, that URL only gets longer and harder to manage.&lt;/p&gt;
&lt;p&gt;Most developers eventually switch to POST for search endpoints, even though they're only retrieving data.&lt;/p&gt;
&lt;p&gt;For years, this has been one of the awkward compromises in REST API design.&lt;/p&gt;
&lt;p&gt;That changes with the new &lt;strong&gt;QUERY&lt;/strong&gt; HTTP method, standardized in &lt;strong&gt;RFC 10008&lt;/strong&gt; (June 2026).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3fymeosu9lavyo325l4r.png&quot; alt=&quot;HTTP year wise Discovered&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-was-query-needed-&quot;&gt;Why Was QUERY Needed?&lt;/h3&gt;
&lt;p&gt;Let's understand the problem first.&lt;/p&gt;
&lt;p&gt;Imagine you're building an e-commerce platform.&lt;/p&gt;
&lt;p&gt;A user wants to search orders based on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customer name&lt;/li&gt;
&lt;li&gt;Order status&lt;/li&gt;
&lt;li&gt;Date range&lt;/li&gt;
&lt;li&gt;Price range&lt;/li&gt;
&lt;li&gt;Multiple product categories&lt;/li&gt;
&lt;li&gt;Sort order&lt;/li&gt;
&lt;li&gt;Pagination&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;strong&gt;GET&lt;/strong&gt;, everything has to fit inside the URL.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /orders?customer=Ravi&amp;amp;status=paid&amp;amp;category=laptop&amp;amp;category=gaming&amp;amp;page=3&amp;amp;sort=-price
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works...&lt;/p&gt;
&lt;p&gt;...until it doesn't.&lt;/p&gt;
&lt;p&gt;As the filters become more advanced, URLs become difficult to read, difficult to encode correctly, and can even exceed practical length limits imposed by browsers, servers, or proxies.&lt;/p&gt;
&lt;p&gt;Another downside is that query parameters often appear in logs, browser history, and analytics systems, making them less suitable for sensitive search criteria.&lt;/p&gt;
&lt;p&gt;Clearly, developers needed a better solution.&lt;/p&gt;
&lt;h3 id=&quot;why-not-just-use-post-&quot;&gt;Why Not Just Use POST?&lt;/h3&gt;
&lt;p&gt;This is exactly what many APIs do today.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /orders/search
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;`{&lt;br&gt;
  &quot;customer&quot;: &quot;Ravi&quot;,&lt;br&gt;
  &quot;status&quot;: &quot;paid&quot;,&lt;br&gt;
  &quot;categories&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Laptop&quot;,
&quot;Gaming&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;price&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;min&quot;: 500,
&quot;max&quot;: 2000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  },&lt;br&gt;
  &quot;page&quot;: 3&lt;br&gt;
}`&lt;/p&gt;
&lt;p&gt;The request body solves many problems.&lt;/p&gt;
&lt;p&gt;It's clean.&lt;/p&gt;
&lt;p&gt;It's readable.&lt;/p&gt;
&lt;p&gt;It supports complex JSON.&lt;/p&gt;
&lt;p&gt;So &lt;strong&gt;what's wrong?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The issue isn't technical.&lt;/p&gt;
&lt;p&gt;It's semantic.&lt;/p&gt;
&lt;p&gt;POST traditionally represents an operation that may change server state.&lt;/p&gt;
&lt;p&gt;Searching for data doesn't.&lt;/p&gt;
&lt;p&gt;Using POST for read-only operations has always been a compromise because HTTP infrastructure can't safely assume the request is cacheable or automatically retryable.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/sg56ca07gii5kfjbyyhx.png&quot; alt=&quot;Comparison&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;meet-query&quot;&gt;Meet QUERY&lt;/h3&gt;
&lt;p&gt;QUERY combines the strengths of both GET and POST.&lt;/p&gt;
&lt;p&gt;Instead of placing filters in the URL, you send them inside the request body.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;QUERY /orders
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;`{&lt;br&gt;
  &quot;customer&quot;: &quot;Ravi&quot;,&lt;br&gt;
  &quot;status&quot;: &quot;paid&quot;,&lt;br&gt;
  &quot;categories&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Laptop&quot;,
&quot;Gaming&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ],&lt;br&gt;
  &quot;price&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;min&quot;: 500,
&quot;max&quot;: 2000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}`&lt;/p&gt;
&lt;p&gt;Unlike POST, &lt;strong&gt;QUERY&lt;/strong&gt; is explicitly defined as a safe and idempotent method.&lt;/p&gt;
&lt;p&gt;That means everyone involved—the client, server, proxy, and cache—knows that executing the request will &lt;strong&gt;not modify server data&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This makes &lt;strong&gt;QUERY&lt;/strong&gt; a much better fit for complex searches.&lt;/p&gt;
&lt;h3 id=&quot;what-does-idempotent-actually-mean-&quot;&gt;What Does &quot;Idempotent&quot; Actually Mean?&lt;/h3&gt;
&lt;p&gt;You'll often hear this word when talking about HTTP methods.&lt;/p&gt;
&lt;p&gt;An operation is idempotent if performing it multiple times has the same effect as performing it once.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;GET /users/42&lt;/p&gt;
&lt;p&gt;Calling this request one time or ten times returns the same user.&lt;/p&gt;
&lt;p&gt;Now compare that to:&lt;/p&gt;
&lt;p&gt;POST /users&lt;/p&gt;
&lt;p&gt;Calling it twice could create two different users.&lt;/p&gt;
&lt;p&gt;That's why POST isn't considered idempotent.&lt;/p&gt;
&lt;p&gt;QUERY behaves like GET in this regard.&lt;/p&gt;
&lt;p&gt;If the network connection drops halfway through the response, a client can safely retry the QUERY request without worrying about accidentally changing server data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/x6kctbe3xzhr1xzp1ecc.png&quot; alt=&quot;Query Flow&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;The QUERY method brings several advantages:&lt;/p&gt;
&lt;p&gt;✅ Supports complex request bodies&lt;br&gt;
✅ Safe for read-only operations&lt;br&gt;
✅ Idempotent&lt;br&gt;
✅ Easier for caching systems to understand&lt;br&gt;
✅ Better HTTP semantics&lt;/p&gt;
&lt;p&gt;Instead of inventing custom search endpoints like:&lt;/p&gt;
&lt;p&gt;POST /users/search&lt;br&gt;
POST /orders/filter&lt;br&gt;
POST /products/query&lt;/p&gt;
&lt;p&gt;Applications can eventually expose cleaner APIs using QUERY.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-example&quot;&gt;A Practical Example&lt;/h3&gt;
&lt;p&gt;Imagine you're building an analytics dashboard.&lt;/p&gt;
&lt;p&gt;Today's API might look like this:&lt;/p&gt;
&lt;p&gt;POST /analytics/report&lt;/p&gt;
&lt;p&gt;Request body:&lt;/p&gt;
&lt;p&gt;`{&lt;br&gt;
  &quot;dateRange&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;from&quot;: &quot;2026-01-01&quot;,
&quot;to&quot;: &quot;2026-06-30&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  },&lt;br&gt;
  &quot;groupBy&quot;: &quot;country&quot;,&lt;br&gt;
  &quot;metrics&quot;: [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;sales&quot;,
&quot;profit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  ]&lt;br&gt;
}`&lt;/p&gt;
&lt;p&gt;The request doesn't create anything.&lt;/p&gt;
&lt;p&gt;It simply retrieves data.&lt;/p&gt;
&lt;p&gt;QUERY expresses that intent much more clearly.&lt;/p&gt;
&lt;p&gt;QUERY /analytics/report&lt;/p&gt;
&lt;p&gt;Same body.&lt;/p&gt;
&lt;p&gt;Better semantics.&lt;/p&gt;
&lt;h3 id=&quot;what-about-spring-boot-and-other-frameworks-&quot;&gt;What About Spring Boot and Other Frameworks?&lt;/h3&gt;
&lt;p&gt;If you're a Java developer like me, you've probably written endpoints such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@PostMapping(&quot;/search&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;even though they're only returning data.&lt;/p&gt;
&lt;p&gt;At the time of writing, most frameworks—including Spring Boot, Express, FastAPI, ASP.NET Core, and many API gateways—don't yet provide first-class support for QUERY because the standard is still very new.&lt;/p&gt;
&lt;p&gt;As ecosystem support grows over the next few years, it will be interesting to see how frameworks adopt it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/2cbcahba06pi2e3903av.png&quot; alt=&quot;Adoptation&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;should-you-use-query-today-&quot;&gt;Should You Use QUERY Today?&lt;/h3&gt;
&lt;p&gt;Probably not in production.&lt;/p&gt;
&lt;p&gt;Although the method has now been standardized, widespread support is still limited.&lt;/p&gt;
&lt;p&gt;Many HTTP clients, frameworks, reverse proxies, API gateways, and developer tools have yet to implement it.&lt;/p&gt;
&lt;p&gt;For now, the common approach remains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;GET&lt;/strong&gt; for simple resource retrieval.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;POST&lt;/strong&gt; when you need complex search bodies and QUERY isn't available.&lt;/li&gt;
&lt;li&gt;Keep an eye on framework support as the ecosystem evolves.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;HTTP&lt;/strong&gt; has remained remarkably stable over the years, and that's a good thing.&lt;/p&gt;
&lt;p&gt;But there has always been a missing piece between GET and POST.&lt;/p&gt;
&lt;p&gt;QUERY fills that gap by providing a standard way to perform &lt;strong&gt;complex, read-only&lt;/strong&gt; requests without sacrificing HTTP semantics.&lt;/p&gt;
&lt;p&gt;Will it replace &lt;strong&gt;GET&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Will it replace &lt;strong&gt;POST&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;Not entirely.&lt;/p&gt;
&lt;p&gt;Instead, it gives developers a better tool for scenarios where neither GET nor POST feels quite right.&lt;/p&gt;
&lt;p&gt;It may take a few years before QUERY becomes common in production APIs, but understanding it today will help you design better APIs tomorrow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What do you think about the new QUERY method?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Would you use it once your favorite framework supports it? Let me know in the comments!&lt;/p&gt;
&lt;p&gt;Thanks for reading! If you enjoyed this article, consider leaving a ❤️ and following me here on DEV Community for more content on Java, Spring Boot, backend engineering, and modern web technologies.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27817/http-just-got-a-new-method-after-16-years-meet-query</guid>
<pubDate>Sun, 20 Sep 2026 09:21:23 +0000</pubDate>
</item>
<item>
<title>Why Your Node.js App Crashes Under Traffic: The Magic of Connection Pooling</title>
<link>https://coderlegion.com/27815/why-your-node-js-app-crashes-under-traffic-the-magic-of-connection-pooling</link>
<description>&lt;p&gt;Imagine launching your shiny new web application. During development, everything runs smoothly. But the moment you launch on social media and a few hundred users hit your API at the exact same time, your server grinds to a halt and starts throwing nasty database connection timeout errors. This common disaster is almost always caused by a lack of database connection pooling.&lt;/p&gt;
&lt;h4 id=&quot;what-is-connection-pooling-&quot;&gt;What is Connection Pooling?&lt;/h4&gt;
&lt;p&gt;Database connection pooling is a performance optimization technique where a set of active database connections is kept open and shared among multiple requests. Instead of creating and destroying a new database connection every single time an application needs to run a query, the application borrows an existing connection from a pre-allocated pool, executes the query, and immediately returns the connection back to the pool. This prevents the costly overhead of starting new communication channels from scratch.&lt;/p&gt;
&lt;h4 id=&quot;the-rental-car-analogy&quot;&gt;The Rental Car Analogy&lt;/h4&gt;
&lt;p&gt;To understand this concept, think of a rental car agency at an airport. Imagine if every time a traveler landed at the airport, the rental agency had to buy a brand-new car from the factory, register it with the DMV, customize it for the driver, and then completely crush and destroy the car at the end of the traveler's weekend trip. That would be an absurdly slow and expensive process.&lt;/p&gt;
&lt;p&gt;Instead, the agency maintains a &quot;pool&quot; of 50 pre-registered, fully fueled, ready-to-drive cars parked in a lot. When a traveler arrives, they borrow a car from the lot, use it for their trip, and then return it to the lot. The car is washed and immediately made available for the next traveler. This is connection pooling: instead of building and destroying connections on the fly, you manage a recycled fleet of them.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;Opening a database connection is an incredibly resource-intensive operation. To establish a new connection, your backend application and database server must complete a network handshake, negotiate security protocols (like TLS/SSL), authenticate user credentials, and allocate dedicated RAM and CPU threads to handle that specific session.&lt;/p&gt;
&lt;p&gt;If you build an application where every user request opens a brand-new connection, a sudden spike in web traffic will force your Node.js server to attempt to open hundreds of database connections simultaneously. Your database server (like MySQL) has a built-in safety limit of maximum allowable concurrent connections. Once that limit is hit, the database will aggressively reject all incoming connection requests, throwing &quot;Too many connections&quot; errors, causing API endpoints to return 500 errors, and crashing your platform. Connection pooling solves this by capping the total number of open database connections and keeping extra queries safely queued up in memory until a connection becomes available.&lt;/p&gt;
&lt;h4 id=&quot;connection-pooling-in-action-node-js-amp-mysql-&quot;&gt;Connection Pooling in Action (Node.js &amp;amp; MySQL)&lt;/h4&gt;
&lt;p&gt;Here is how you implement connection pooling using Express and the standard &lt;code&gt;mysql2/promise&lt;/code&gt; library in a Node.js environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

// 1. Create a global connection pool with configuration limits
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'secure_password',
  database: 'ecommerce_db',
  waitForConnections: true,    // Queue queries if no connections are free
  connectionLimit: 10,         // Maximum number of open connections
  queueLimit: 0                // No limit on the number of queued queries
});

app.get('/api/products', async (req, res) =&amp;gt; {
  try {
    // 2. pool.query automatically borrows an idle connection, 
    // executes the query, and returns it to the pool.
    const [products] = await pool.query('SELECT id, name, price FROM products LIMIT 20');
    res.json(products);
  } catch (error) {
    console.error('Database failure:', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(3000, () =&amp;gt; {
  console.log('Server is running on port 3000');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-1&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Software scalability is not just about writing clean logic or optimizing loops; it is about protecting and sharing finite hardware resources under heavy load. By implementing connection pooling, you protect your database from crashing, reduce API response latency, and build backend systems capable of handling traffic spikes gracefully without costing a fortune in extra server infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/stop-rebuilding-bridge-how-connection.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27815/why-your-node-js-app-crashes-under-traffic-the-magic-of-connection-pooling</guid>
<pubDate>Sun, 20 Sep 2026 08:56:50 +0000</pubDate>
</item>
<item>
<title>Rate Limiting Isn't One Layer: What LogicVisor and Titan Actually Do Differently</title>
<link>https://coderlegion.com/27708/rate-limiting-isnt-one-layer-what-logicvisor-and-titan-actually-do-differently</link>
<description>&lt;p&gt;&quot;Add rate limiting&quot; sounds like one task. It's actually three separate layers, each defending against a different failure mode, with different trade-offs and different amounts of trust you can place in them. I only understood the distinction properly once I had to pick a strategy for two systems solving different problems: LogicVisor (a public AI tool anyone can hit anonymously) and Titan (a payments platform where the cost of a bad actor is different from the cost of someone burning through free AI credits).&lt;/p&gt;
&lt;h3 id=&quot;1-client-side-ux-not-security&quot;&gt;1. Client-side: UX, not security&lt;/h3&gt;
&lt;p&gt;Debouncing a search input, greying out a submit button after the first click, backing off exponentially after a 429. All of this makes an app feel considerate. None of it stops anyone. A malicious actor skips your JavaScript entirely and hits the endpoint directly with curl. Client-side limiting is worth doing (it saves you real traffic and prevents accidental double-submits), but it is not a security control. If it's the only thing standing between your API and abuse, you don't have rate limiting, you have a polite suggestion.&lt;/p&gt;
&lt;h3 id=&quot;2-server-side-the-layer-that-actually-protects-you&quot;&gt;2. Server-side: the layer that actually protects you&lt;/h3&gt;
&lt;p&gt;This is where LogicVisor and Titan diverge, because they're not defending against the same thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LogicVisor: several checks before a single AI token gets spent&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LogicVisor is public. Anyone gets 3 free code reviews with no signup, which means the abuse surface is wide open by design. The submission route runs a stack of checks before it ever calls Gemini or Groq, because every AI call costs real money:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 1. Check if this exact code has already been reviewed by this model
const cachedReview = await getCachedAIReview(preferred_model.id + &quot;-&quot; + canonicalHash);
if (cachedReview) {
  return NextResponse.json({ success: true, data: cachedReview }, { status: 201 });
}

// 2. Enforce the actual rate limit (IP + session based for anon users)
const rateLimitResult = await enforceAIRateLimit(user.id, request);

// 3. Slow down premium users intelligently instead of hard-blocking them
const throttleDelay = await getThrottleDelay(user.id);
if (throttleDelay &amp;gt; 0) {
  await new Promise((resolve) =&amp;gt; setTimeout(resolve, throttleDelay));
}

// 4. Under heavy load, degrade gracefully instead of rejecting outright
const shouldDegrade = await shouldGracefullyDegrade(user.id, &quot;ai_request&quot;);
if (shouldDegrade) {
  // return a basic, non-AI response instead of a 429
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few things worth naming separately, because they get lumped together under &quot;rate limiting&quot; but aren't the same mechanism:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Abuse prevention&lt;/strong&gt;: anonymous sessions are capped at 3 reviews, tracked by a UUID session (7-day expiry) plus a browser fingerprint (User-Agent + Accept-Language + Accept-Encoding). This is the classic server-side layer, IP and fingerprint based, closest to the &quot;token bucket per identity&quot; pattern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost control via deduplication&lt;/strong&gt;: before rate limiting even runs, the code is canonicalized (AST-based) and hashed. If someone (or the same person twice) submits identical logic, they get the cached review instead of triggering a new paid AI call. This isn't rate limiting in the strict sense, it's closer to database-level duplicate detection, but it does the same job of protecting a scarce resource.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graceful degradation over hard rejection&lt;/strong&gt;: instead of a flat 429 when usage spikes, authenticated users can get a stripped-down, non-AI response. It costs nothing and still gives the user something, rather than a wall.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is a single algorithm out of a textbook. It's several cheap, layered checks, ordered so the most expensive resource (the AI call) is the last thing hit, not the first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Titan: NestJS Throttler backed by Redis&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Titan's rate limiting is intentionally boring by comparison, and that's the correct choice for what it is. It's &lt;code&gt;@nestjs/throttler&lt;/code&gt; wired up as a global guard, with Redis swapped in as the storage backend instead of the default in-memory store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;ThrottlerModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) =&amp;gt; ({
    throttlers: [
      {
        ttl: 60000,
        limit: 10,
      },
    ],
    storage: new ThrottlerStorageRedisService(
      configService.get&amp;lt;string&amp;gt;('REDIS_URL'),
    ),
  }),
}),

// registered alongside the JWT guard as a global APP_GUARD
{
  provide: APP_GUARD,
  useClass: ThrottlerGuard,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;10 requests per 60 seconds, globally, enforced before a request even reaches a controller. No per-route tuning yet, no per-user tiers, just a flat ceiling applied everywhere via &lt;code&gt;APP_GUARD&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The reason Redis matters here isn't exotic: an in-memory counter only knows about requests hitting that one process. The moment you run more than one instance behind a load balancer, in-memory rate limiting stops being true rate limiting, each instance is independently under-counting. Swapping the storage to &lt;code&gt;ThrottlerStorageRedisService&lt;/code&gt; gives every instance a shared, consistent view of who's made how many requests, which is the actual requirement once you're not running a single box.&lt;/p&gt;
&lt;p&gt;LogicVisor's layered, identity-aware approach makes sense for a system where the main threat is &quot;someone is farming free AI reviews.&quot; Titan's flat, Redis-backed throttle makes sense for a system where the main threat is &quot;someone is hammering an endpoint,&quot; and the priority is consistency across instances over nuance per user.&lt;/p&gt;
&lt;h3 id=&quot;3-database-level-the-layer-i-m-deliberately-not-using-yet&quot;&gt;3. Database-level: the layer I'm deliberately not using yet&lt;/h3&gt;
&lt;p&gt;Postgres and Supabase both support rate limiting closer to the data layer (unlogged tables with triggers, PL/pgSQL token bucket functions, TTL-based counters in Mongo). I don't use this in either project, and I don't think I should yet. It's the right call for protecting specific high-value operations (financial writes, heavy analytical queries) where you need the database itself to be the source of truth and can't tolerate a race between the app layer and the DB. LogicVisor's canonical-hash cache lookup is adjacent to this idea (checking the data layer before doing expensive work), but it's a caching pattern, not a rate limiter, and I'd rather keep that distinction honest than dress it up as something it isn't.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-mattered-in-order&quot;&gt;What actually mattered, in order&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Client-side limiting is UX. Treat it as UX.&lt;/strong&gt; It's not a line item in your security review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-side is where the real decision lives&lt;/strong&gt;, and the right shape of it depends on what you're protecting: identity-based abuse (LogicVisor) versus flat request volume across a distributed system (Titan). They are not the same problem and don't need the same solution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The most expensive resource should be the last thing your code touches.&lt;/strong&gt; LogicVisor checks a cache, then a rate limit, then a throttle delay, before it ever calls an LLM. Ordering is a rate limiting decision too, even when it's not framed as one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database-level limiting is a tool for a specific job&lt;/strong&gt;, not a default. I'm not reaching for it until there's an operation that actually needs that guarantee.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;small&gt;Cover photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/@able527?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;HsinKai Tai&lt;/a&gt; on &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/photos/traffic-jam-at-a-toll-booth-on-a-highway-5rwx1kzWpFk?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27708/rate-limiting-isnt-one-layer-what-logicvisor-and-titan-actually-do-differently</guid>
<pubDate>Sat, 19 Sep 2026 09:19:55 +0000</pubDate>
</item>
<item>
<title>Streaming vs JSON: Trade-offs in AI-Powered Apps</title>
<link>https://coderlegion.com/27704/streaming-vs-json-trade-offs-in-ai-powered-apps</link>
<description>&lt;p&gt;This is a breakdown of a decision I made building LogicVisor, not a general &quot;streaming vs JSON&quot; guide. AI API responses aren't limited to text: some return images, audio, structured data. I only needed text, so streaming vs JSON became a real fork in the road. If I'd needed image responses too, this calculus changes entirely.&lt;/p&gt;
&lt;p&gt;Two constraints were in tension from the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The AI evaluation needed to be stored in the database.&lt;/li&gt;
&lt;li&gt;The same information needed to be available to the user immediately, at submission time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-you-can-t-reliably-stream-json&quot;&gt;Why you can't reliably stream JSON&lt;/h3&gt;
&lt;p&gt;JSON has to be parsed before it's usable, and you can't parse it in pieces as it arrives. It needs to be complete, start to finish, before &lt;code&gt;JSON.parse()&lt;/code&gt; does anything with it. That forces a binary choice: stream, or don't. Streaming, in my case, meant raw text, because that's all the underlying APIs stream.&lt;/p&gt;
&lt;p&gt;I started with streaming. Feedback was near-instant on the user end, and it felt great. Then I needed a more structured response, for both the user view and the database, so I switched the request over to ask for JSON instead of free text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const groqParams: ChatCompletionCreateParamsNonStreaming = {
  messages: [{ role: &quot;user&quot;, content: prompt.content }],
  model: groqModel,
  stream: false,
  response_format: {
    type: &quot;json_object&quot;,
  },
};

const groqResponse = await groq.chat.completions.create(groqParams);
aiReviewText = groqResponse.choices[0].message.content ?? &quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stream: false&lt;/code&gt; plus &lt;code&gt;response_format: json_object&lt;/code&gt; is the whole trick. The model still wraps its answer in a markdown code fence half the time regardless of what you ask for, so I still need to strip that off before parsing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export function extractJSONFromMarkdown(markdownString: string) {
  let jsonString = markdownString.trim();

  if (jsonString.startsWith(&quot;```json&quot;)) {
    jsonString = jsonString.substring(7);
  }
  if (jsonString.endsWith(&quot;```&quot;)) {
    jsonString = jsonString.substring(0, jsonString.length - 3);
  }

  const parsedData: PromptResult = JSON.parse(jsonString.trim());
  return parsedData;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got the data shape I wanted. What I lost was the user experience. Now the user was staring at a loading state for the full response time instead of watching text stream in.&lt;/p&gt;
&lt;h3 id=&quot;what-i-actually-learned&quot;&gt;What I actually learned&lt;/h3&gt;
&lt;p&gt;The streaming version was never faster in absolute terms. It just &lt;em&gt;felt&lt;/em&gt; faster, because you're more patient when you can see progress happening. Streaming exploited that. JSON removed it.&lt;/p&gt;
&lt;p&gt;Since I couldn't stream JSON (parsing requires a complete payload) and I couldn't get structured data out of raw streamed text without hacking something together after the fact, I stopped trying to force one approach to do both jobs. I fixed the UX problem directly instead: loading spinners, pulsers, rotating status text. Not real progress, but it reads as progress, and that's what the user actually responds to.&lt;/p&gt;
&lt;h3 id=&quot;two-paths-two-jobs&quot;&gt;Two paths, two jobs&lt;/h3&gt;
&lt;p&gt;When I added anonymous, free-trial reviews, the storage requirements dropped. Anonymous users didn't need the same depth of data persisted as authenticated ones. So that path defaults to streaming, for the fastest perceived response, which is exactly what you want when you're trying to hook a new user during a free trial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const groqStream = await groq.chat.completions.create({
  messages: [{ role: &quot;user&quot;, content: prompt.content }],
  model: groqModel,
  stream: true,
});

for await (const chunk of groqStream) {
  const text = chunk.choices[0]?.delta?.content ?? &quot;&quot;;
  fullText += text;

  controller.enqueue(
    encoder.encode(`data: ${JSON.stringify({ type: &quot;content&quot;, content: text })}\n\n`),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same &lt;code&gt;groq.chat.completions.create&lt;/code&gt; call as before, just &lt;code&gt;stream: true&lt;/code&gt; instead of &lt;code&gt;false&lt;/code&gt;, and no &lt;code&gt;response_format&lt;/code&gt;. Each chunk gets pushed to the client over server-sent events as it arrives, and I accumulate &lt;code&gt;fullText&lt;/code&gt; on the server side so I've still got the complete response to hand off for logging once the stream closes.&lt;/p&gt;
&lt;p&gt;(The real implementation also retries on 503s with backoff and sends a &quot;waking up&quot; notice on cold starts. Left out here since it's not specific to the streaming vs JSON decision, just general resilience around a serverless AI provider.)&lt;/p&gt;
&lt;p&gt;Authenticated users, who need full structured data persisted, get the non-streaming JSON path from the first section, feeding straight into a database write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const result = extractJSONFromMarkdown(aiReviewText ?? &quot;&quot;);

const review = await createAIReview({
  user_id: user.id,
  time_complexity: result?.time_complexity ?? &quot;&quot;,
  overall_grade: result?.overall_grade ?? &quot;&quot;,
  markdown_review: result?.markdown_review ?? &quot;&quot;,
  submission_id: submission.id,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two code paths, one architecture, each one earning its keep for a different constraint: speed for anonymous trial users, structure for authenticated ones.&lt;/p&gt;
&lt;h3 id=&quot;the-takeaway-2&quot;&gt;The takeaway&lt;/h3&gt;
&lt;p&gt;Building AI-powered products (or honestly, any product) comes down to picking the trade-off that serves your actual goal, not the one that looks cleanest on paper. Streaming and JSON aren't competing solutions here, they're two tools solving two different problems in the same app.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;small&gt;Cover photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/@loomydoons?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Myles Bloomfield&lt;/a&gt; on &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/photos/a-sculpture-with-balls-on-top-of-it-p8JbzOUwdjg?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27704/streaming-vs-json-trade-offs-in-ai-powered-apps</guid>
<pubDate>Sat, 19 Sep 2026 08:33:43 +0000</pubDate>
</item>
<item>
<title>How I Eliminated 19,494 Duplicate MongoDB Records (And What I'd Do Differently Today)</title>
<link>https://coderlegion.com/27703/how-i-eliminated-19-494-duplicate-mongodb-records-and-what-id-do-differently-today</link>
<description>&lt;p&gt;At a previous organization, I was handed ownership of a job platform serving 1,000+ users worldwide. Jobs on the platform could be scoped to a single city, a state, a whole country, or &quot;available worldwide.&quot; Simple enough. At least, you'd think so.&lt;/p&gt;
&lt;p&gt;When I joined, this was early in my career and I was a frontend dev. I was interested in backend work but hadn't done any real backend engineering yet — I wasn't battle-tested. Somewhere along the line, I got handed the whole platform. Frontend, backend, all of it. Bracing moment. I forced myself to slow down, actually understand the system end to end, and build a real plan before touching anything.&lt;/p&gt;
&lt;p&gt;After talking with product management, I pulled up the backend task backlog. I learn best by doing, so I started at the bottom — smallest bugs first, working my way up. It went well for a while. Then I hit a ticket tied to core business logic.&lt;/p&gt;
&lt;h3 id=&quot;the-wake-up-call&quot;&gt;The Wake-Up Call&lt;/h3&gt;
&lt;p&gt;Some endpoints were clocking API response times north of &lt;strong&gt;7,000ms&lt;/strong&gt;. I genuinely thought it was a typo. Then I remembered running into something similar on the frontend side before — so I patched the UX so users never felt the full weight of the delay. But the actual problem was still sitting there, untouched.&lt;/p&gt;
&lt;p&gt;Before touching any code, I made a checklist. We were running MongoDB with NestJS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What schemas was the endpoint touching?&lt;/li&gt;
&lt;li&gt;What optimization had already been applied to those schemas?&lt;/li&gt;
&lt;li&gt;How was the endpoint fetching data?&lt;/li&gt;
&lt;li&gt;How many records did each related schema currently hold?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Checklist in hand, I went into the codebase.&lt;/p&gt;
&lt;p&gt;My innocent dev brain was not prepared for the chaos I found.&lt;/p&gt;
&lt;h3 id=&quot;what-was-actually-happening&quot;&gt;What Was Actually Happening&lt;/h3&gt;
&lt;p&gt;Here's the setup: assume an empty &lt;code&gt;jobs&lt;/code&gt; collection. A recruiter creates a job listing scoped to &lt;strong&gt;New York City&lt;/strong&gt;. The backend takes the payload and creates one record with location set to New York City. Straightforward.&lt;/p&gt;
&lt;p&gt;Now assume another empty collection, and this time the job is scoped to the &lt;strong&gt;United States&lt;/strong&gt; instead of a single city. What do you think happens?&lt;/p&gt;
&lt;p&gt;If you guessed &quot;the same thing — one record, location set to United States&quot; — I have bad news for you.&lt;/p&gt;
&lt;p&gt;What the backend &lt;em&gt;actually&lt;/em&gt; did: it looked at the payload, recognized &quot;United States&quot; as a country rather than a city, looked up every known city in the U.S. — and created a separate job record for &lt;strong&gt;each one&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Read that again.&lt;/p&gt;
&lt;p&gt;Instead of one record, the platform generated one identical record per city, differing only in &lt;code&gt;_id&lt;/code&gt; and location. And it got worse: a job marked &quot;available worldwide&quot; would spawn a record for every city on the planet.&lt;/p&gt;
&lt;p&gt;This was a live product with real traffic. Querying that collection — even with indexes in place — was a nightmare.&lt;/p&gt;
&lt;p&gt;For context on scale: a single job scoped to just the USA generated &lt;strong&gt;19,495 records&lt;/strong&gt; — one canonical listing plus 19,494 exact duplicates.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Database records (1 USA-wide job) &lt;/td&gt;&lt;td&gt;19,495&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Exact duplicates among them &lt;/td&gt;&lt;td&gt;19,494&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Query time on affected endpoints &lt;/td&gt;&lt;td&gt;~2.3s (worst cases exceeded 7s for worldwide-scoped jobs)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;My first reaction was horrified confusion. After talking it over with management, I learned there &lt;em&gt;was&lt;/em&gt; a reason for the original design — I won't get into it here, it's not the point of this piece. What mattered was that I had free rein to fix it if I could pull it off. Hold my Pokémon card.&lt;/p&gt;
&lt;h3 id=&quot;the-constraints-1&quot;&gt;The Constraints&lt;/h3&gt;
&lt;p&gt;Fixing the architecture wasn't the hard part conceptually — the hard part was fixing it under real constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We still needed to resolve &lt;em&gt;which&lt;/em&gt; cities a job was available in, for every existing feature that depended on that.&lt;/li&gt;
&lt;li&gt;Whatever I built had to slot into the existing infrastructure without a rewrite of everything downstream.&lt;/li&gt;
&lt;li&gt;I needed to show measurable, provable performance gains — not just &quot;trust me, it's better.&quot;&lt;/li&gt;
&lt;li&gt;None of this could disrupt daily active users. No maintenance windows, no downtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;designing-the-fix&quot;&gt;Designing the Fix&lt;/h3&gt;
&lt;p&gt;I started with the schema. The core question: how do I preserve full location fidelity while collapsing tens of thousands of records into one?&lt;/p&gt;
&lt;p&gt;I thought back to basic compression concepts — take data, strip it to a minimal representation, and pair it with the instructions needed to reconstruct the original. That's essentially what I needed here: a compact way to represent &quot;this job is available in X&quot; without one document per X.&lt;/p&gt;
&lt;p&gt;Two deliverables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A new, efficient schema.&lt;/li&gt;
&lt;li&gt;A migration path that collapsed the old duplicated records into the new schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the new schema, I kept every existing field and replaced the flat &lt;code&gt;location&lt;/code&gt; string with a structured &lt;code&gt;locationObj&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface LocationObj {
  country: string | string[];
  state: string | string[];
  city: string | string[];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single city&lt;/strong&gt; → &lt;code&gt;city&lt;/code&gt; is a string, &lt;code&gt;state&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt; resolve to that city's parent state/country.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A specific group of cities&lt;/strong&gt; → &lt;code&gt;city&lt;/code&gt; becomes a string array.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An entire state&lt;/strong&gt; → &lt;code&gt;city&lt;/code&gt; becomes the sentinel value &lt;code&gt;&quot;all&quot;&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt; holds the actual state name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An entire country&lt;/strong&gt; → &lt;code&gt;state&lt;/code&gt; and &lt;code&gt;city&lt;/code&gt; both become &lt;code&gt;&quot;all&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worldwide&lt;/strong&gt; → &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, and &lt;code&gt;city&lt;/code&gt; all become &lt;code&gt;&quot;all&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a job available across Berlin, Munich, and Hamburg looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  title: &quot;Marketing Manager&quot;,
  company: &quot;TechCorp&quot;,
  locationObj: {
    country: &quot;Germany&quot;,
    state: &quot;all&quot;,
    city: [&quot;Berlin&quot;, &quot;Munich&quot;, &quot;Hamburg&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a job available anywhere in the world:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  title: &quot;Software Developer&quot;,
  company: &quot;TechCorp&quot;,
  locationObj: {
    country: &quot;all&quot;,
    state: &quot;all&quot;,
    city: &quot;all&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One document. Full location semantics preserved. No fan-out.&lt;/p&gt;
&lt;h3 id=&quot;migrating-without-downtime&quot;&gt;Migrating Without Downtime&lt;/h3&gt;
&lt;p&gt;The deduplication piece ran as a batch script against the old collection: group records that shared everything &lt;em&gt;except&lt;/em&gt; location (same title, company, description, salary — the actual &quot;identity&quot; of a job), reconstruct the correct &lt;code&gt;locationObj&lt;/code&gt; from the set of location values in that group, and write a single new document into the new collection.&lt;/p&gt;
&lt;p&gt;Once the new collection was validated against the old one, I built new endpoints on top of the new schema, mirroring the contracts of the old ones, and prepared the frontend to point at them.&lt;/p&gt;
&lt;p&gt;The cutover itself was a backfill with two catch-up passes, not a one-shot migration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run the migration script once against the live old collection.&lt;/li&gt;
&lt;li&gt;Right before deploying the new endpoints, re-run it — this catches any jobs created &lt;em&gt;while&lt;/em&gt; the first pass was running, since the old collection kept accepting writes the entire time.&lt;/li&gt;
&lt;li&gt;Deploy the new endpoints and switch the frontend over.&lt;/li&gt;
&lt;li&gt;Re-run the script once more right after — this catches anything created under the old schema during the deploy window itself, in case the rollout wasn't instantaneous.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I tested the full path — frontend, backend, and DB — on local and staging before any of this touched production, so by the time it shipped, the swap itself was just a normal deploy. From a user's perspective, they kept seeing exactly the jobs they were supposed to see, the whole way through.&lt;/p&gt;
&lt;h3 id=&quot;querying-the-new-schema&quot;&gt;Querying the New Schema&lt;/h3&gt;
&lt;p&gt;The query logic has to walk the hierarchy — country, then state, then city — because &quot;all&quot; at any level short-circuits the rest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const query = {
  $or: [
    { &quot;locationObj.country&quot;: &quot;all&quot; },
    { &quot;locationObj.country&quot;: userCountry, &quot;locationObj.state&quot;: &quot;all&quot; },
    { &quot;locationObj.country&quot;: userCountry, &quot;locationObj.state&quot;: userState, &quot;locationObj.city&quot;: &quot;all&quot; },
    { &quot;locationObj.country&quot;: userCountry, &quot;locationObj.city&quot;: userCity },
    { &quot;locationObj.country&quot;: userCountry, &quot;locationObj.city&quot;: { $in: [userCity] } }
  ]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combined with a compound index on &lt;code&gt;locationObj.country&lt;/code&gt;, &lt;code&gt;locationObj.state&lt;/code&gt;, and &lt;code&gt;locationObj.city&lt;/code&gt;, this scales linearly with the number of &lt;em&gt;actual&lt;/em&gt; jobs — not the number of cities they happen to be available in.&lt;/p&gt;
&lt;h3 id=&quot;the-results&quot;&gt;The Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Database records (1 USA-wide job) &lt;/td&gt;&lt;td&gt;19,495 &lt;/td&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;99.99% fewer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Query time &lt;/td&gt;&lt;td&gt;~2.3s &lt;/td&gt;&lt;td&gt;~0.12s &lt;/td&gt;&lt;td&gt;~95% faster&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Storage footprint &lt;/td&gt;&lt;td&gt;847 MB &lt;/td&gt;&lt;td&gt;0.043 MB &lt;/td&gt;&lt;td&gt;99.99% smaller&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Index size &lt;/td&gt;&lt;td&gt;156 MB &lt;/td&gt;&lt;td&gt;0.008 MB &lt;/td&gt;&lt;td&gt;99.99% smaller&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memory usage &lt;/td&gt;&lt;td&gt;~4.2 GB &lt;/td&gt;&lt;td&gt;~0.21 MB &lt;/td&gt;&lt;td&gt;99.99% less RAM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Exact duplicate documents &lt;/td&gt;&lt;td&gt;19,494 &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;Eliminated&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Management was, unsurprisingly, impressed.&lt;/p&gt;
&lt;h3 id=&quot;how-i-d-do-it-now&quot;&gt;How I'd Do It Now&lt;/h3&gt;
&lt;p&gt;Looking back at this with a few more years under my belt, the schema still holds up. What I'd change is the migration and verification process — at the time, &quot;backfill, then re-run twice around the cutover&quot; felt rigorous. It worked. But it also relied on timing more than I'd want to admit if I were reviewing this today.&lt;/p&gt;
&lt;p&gt;The more rigid version of this — the one I'd actually reach for now — is the &lt;strong&gt;expand-contract pattern&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Expand&lt;/strong&gt;: Deploy dual-writes behind a feature flag — every job creation or update writes to both the old and new schema at the same time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync&lt;/strong&gt;: Use MongoDB Change Streams to mirror any old-schema writes into the new schema in real time during the transition, instead of relying on timed reruns of a migration script. This closes the race-condition window entirely — there's no gap to miss, because nothing is waiting for a scheduled pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contract&lt;/strong&gt;: Once reads are fully on the new schema and stable, kill the old write path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I'd also make the migration script itself &lt;strong&gt;idempotent and checkpointed&lt;/strong&gt; — track the last processed &lt;code&gt;_id&lt;/code&gt; so a crash mid-run means it resumes instead of reprocessing or silently skipping documents. Mine was a script I babysat through completion; a checkpointed version is one I could walk away from.&lt;/p&gt;
&lt;p&gt;On verification — &quot;compare the counts&quot; was good enough to convince management, but it wasn't rigorous. I'd now do a real reconciliation pass: diff the reconstructed &lt;code&gt;locationObj&lt;/code&gt; against the actual set of cities in the old schema, per job, and flag any mismatch automatically instead of trusting a spot check. And before flipping reads over, I'd run a dark-launch period — querying both schemas in production and logging discrepancies without serving the shadow result to users — so any gap shows up before it's user-facing, not after.&lt;/p&gt;
&lt;p&gt;One more thing I'd change that isn't about the migration at all: I'd want this caught before it ever shipped. The root failure was that nothing stopped a single job-creation event from silently producing thousands of documents. A write-layer guardrail — &quot;creating one job should never result in more than one document, alert if it does&quot; — plus a five-line regression test asserting exactly that, would have caught this in review instead of in production months later.&lt;/p&gt;
&lt;h3 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Embedding vs. referencing isn't the real question — normalization is.&lt;/strong&gt; The original design wasn't wrong because it embedded or referenced incorrectly; it was wrong because it materialized every possible resolution of a hierarchical value instead of storing the hierarchy itself. The general MongoDB advice of &quot;embed for read-heavy, reference for write-heavy&quot; didn't apply here — the fix was closer to a compression problem than a modeling problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sentinel values are underrated.&lt;/strong&gt; Using &lt;code&gt;&quot;all&quot;&lt;/code&gt; as a stand-in for &quot;every value at this level&quot; let one field type (&lt;code&gt;string | string[]&lt;/code&gt;) represent five distinct scopes without a single boolean flag or separate &lt;code&gt;scope&lt;/code&gt; enum.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the actual scale of the problem before designing the fix.&lt;/strong&gt; I didn't just need &quot;make it faster&quot; — I needed a number to prove it. Knowing it was 19,495 records for &lt;em&gt;one job&lt;/em&gt; is what made the case to management, and it's what makes this story worth telling.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Fields, numbers, and code above are reconstructed from a project where I owned the full backend under an NDA — details are generalized where necessary, but the architecture, the failure mode, and the metrics are real.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;small&gt;Cover photo by &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/@kommumikation?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Mika Baumeister&lt;/a&gt; on &lt;a rel=&quot;nofollow&quot; href=&quot;https://unsplash.com/photos/white-printing-paper-with-numbers-Wpnoqo2plFA?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/27703/how-i-eliminated-19-494-duplicate-mongodb-records-and-what-id-do-differently-today</guid>
<pubDate>Sat, 19 Sep 2026 08:33:15 +0000</pubDate>
</item>
<item>
<title>Supercharge Your Node.js App: Why You Need Database Connection Pooling</title>
<link>https://coderlegion.com/27716/supercharge-your-node-js-app-why-you-need-database-connection-pooling</link>
<description>&lt;h2 id=&quot;what-is-connection-pooling--1&quot;&gt;What is Connection Pooling?&lt;/h2&gt;
&lt;p&gt;Database connection pooling is a technique where a cache of database connections is kept open and shared among multiple queries, rather than opening and closing a brand-new connection for every single query. When your application needs to talk to the database, it borrows an existing connection from the pool, runs the query, and immediately returns it. This avoids the heavy performance penalty of creating and destroying connections constantly.&lt;/p&gt;
&lt;h3 id=&quot;the-pizza-scooter-analogy&quot;&gt;The Pizza Scooter Analogy&lt;/h3&gt;
&lt;p&gt;Imagine a busy local pizza shop. Every time a customer orders a pizza, if the shop had to buy a brand-new delivery scooter from the dealership, register it, insure it, deliver the pizza, and then scrap or sell the scooter immediately afterward, the business would go bankrupt in an hour.&lt;/p&gt;
&lt;p&gt;Instead, the shop maintains a stable fleet of five delivery scooters parked in the back. When a delivery is ready, a driver grabs an available scooter from the fleet, completes the delivery, and parks it back in the lot for the next driver to use. Connection pooling is that stable fleet of scooters.&lt;/p&gt;
&lt;h3 id=&quot;why-connection-pooling-matters-daily-1&quot;&gt;Why Connection Pooling Matters Daily&lt;/h3&gt;
&lt;p&gt;In backend development, establishing a connection to a database like MySQL is an incredibly heavy operation. It requires several steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A TCP network handshake.&lt;/li&gt;
&lt;li&gt;SSL/TLS security negotiation.&lt;/li&gt;
&lt;li&gt;User authentication and privilege verification.&lt;/li&gt;
&lt;li&gt;Memory allocation on the database server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your Express.js server handles 1,000 requests per second and you open a new connection for each one, your database server's CPU will spike to 100% just managing handshakes. Eventually, the database will run out of resources and throw a dreaded &quot;Too many connections&quot; error, crashing your app. By using a connection pool, you bypass this startup overhead. Your queries run almost instantly because the connection is already active and authenticated.&lt;/p&gt;
&lt;h3 id=&quot;simple-code-example&quot;&gt;Simple Code Example&lt;/h3&gt;
&lt;p&gt;Let's see the difference in a MERN stack application using Node.js, Express, and the &lt;code&gt;mysql2&lt;/code&gt; package.&lt;/p&gt;
&lt;h4 id=&quot;the-bad-way-new-connection-per-request-&quot;&gt;The Bad Way (New Connection per Request)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

const dbConfig = { host: 'localhost', user: 'root', database: 'store' };

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // Slow and resource-intensive: creating a connection on every request
    const connection = await mysql.createConnection(dbConfig);
    const [rows] = await connection.execute('SELECT * FROM products LIMIT 10');

    // Closing it manually
    await connection.end();
    res.json(rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-good-way-using-a-connection-pool-&quot;&gt;The Good Way (Using a Connection Pool)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

const dbConfig = { host: 'localhost', user: 'root', database: 'store' };

// Create a pool of up to 10 reusable connections
const pool = mysql.createPool({
  ...dbConfig,
  connectionLimit: 10,
  waitForConnections: true,
  queueLimit: 0
});

app.get('/products', async (req, res) =&amp;gt; {
  try {
    // Automatically borrows an active connection and returns it when done
    const [rows] = await pool.execute('SELECT * FROM products LIMIT 10');
    res.json(rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway-1&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Database connection pooling is not just a performance optimization; it is a fundamental survival mechanism for production databases. It turns chaotic database traffic into a predictable, queued stream, ensuring your server remains responsive and your database stays healthy under heavy load.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-to-prevent-database-crashes-under.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27716/supercharge-your-node-js-app-why-you-need-database-connection-pooling</guid>
<pubDate>Sat, 19 Sep 2026 08:17:43 +0000</pubDate>
</item>
<item>
<title>Code Fingerprinting: Detecting Duplicate Submissions Without Losing Your Mind (or Your API Budget)</title>
<link>https://coderlegion.com/27702/code-fingerprinting-detecting-duplicate-submissions-without-losing-your-mind-your-budget</link>
<description>&lt;p&gt;While building LogicVisor's review system, I was faced with a question I hadn't thought about before: what would happen if users submitted duplicate algorithm solutions?&lt;/p&gt;
&lt;p&gt;I decided to test it myself. I submitted a solution to a simple palindrome challenge. The request went through, my solution was reviewed correctly. Normal happy stuff. Then I submitted the exact same solution a second time. And the normal happy stuff happened again. But I was far from happy.&lt;/p&gt;
&lt;p&gt;That duplicate submission meant more tokens spent, more database memory consumed, more remote procedure calls, more AI calls, and more waiting time. For a problem that had already been solved.&lt;/p&gt;
&lt;p&gt;I shuddered at the thought of a bad actor effectively DOS-ing my site or running up my AI bill just by hammering the same solution repeatedly. Rate limiting controls the number of requests per client — but it has nothing to do with the &lt;em&gt;content&lt;/em&gt; of those requests. A user could stay well within their rate limit and still abuse the system by submitting the same code over and over.&lt;/p&gt;
&lt;p&gt;This wasn't going to work. I needed a way to identify duplicate submissions and return a cached response instead of processing everything from scratch. Which led me to a more interesting question: how do you actually detect identical code?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-identical-&quot;&gt;The Problem With &quot;Identical&quot;&lt;/h3&gt;
&lt;p&gt;Programming by its very nature allows us to solve the same problem in multiple ways. But even a single solution can be written in an infinite number of ways.&lt;/p&gt;
&lt;p&gt;User A could create a function that takes and returns a string, and name it &lt;code&gt;strawberries&lt;/code&gt;. User B could write the exact same function with the exact same logic and name it &lt;code&gt;bananas&lt;/code&gt;. Different names, same implementation, same result.&lt;/p&gt;
&lt;p&gt;For LogicVisor's purposes, these should be counted as duplicates. But how do you get a computer to see past the surface differences and recognize that two pieces of code are structurally the same?&lt;/p&gt;
&lt;p&gt;I started thinking about it from first principles. At the end of the day, programming languages get compiled down to binary and then to electrical pulses. If two solutions solve the same problem the same way, they're ultimately firing the same set of pulses at the machine level. Variable names be damned.&lt;/p&gt;
&lt;p&gt;So the question became: can I strip a solution down to its most fundamental representation and compare &lt;em&gt;that&lt;/em&gt;?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;enter-the-abstract-syntax-tree&quot;&gt;Enter the Abstract Syntax Tree&lt;/h3&gt;
&lt;p&gt;More research led me to the Abstract Syntax Tree — something my OS professor had mentioned in school that I'd filed away as &quot;probably never useful in web dev.&quot; No knowledge is wasted.&lt;/p&gt;
&lt;p&gt;An Abstract Syntax Tree (AST) is a hierarchical, tree-shaped data structure that represents the syntactic structure of source code. It strips away superficial syntax — semicolons, curly braces, whitespace, comments — and keeps only the structural and semantic skeleton of the code. The &lt;em&gt;shape&lt;/em&gt; of what you wrote, not the clothing it's wearing.&lt;/p&gt;
&lt;p&gt;This was it. But I hit a wall almost immediately: I can't store and index an entire AST reliably in PostgreSQL. It's a complex nested structure, not a scalar value you can slap an index on.&lt;/p&gt;
&lt;p&gt;Then I remembered an article about handling duplicate file uploads in Express with Multer. The trick was generating a SHA-256 hash of the file's content. A hash is a fixed-length fingerprint — the same input always produces the same output, and different inputs produce different outputs. Perfect.&lt;/p&gt;
&lt;p&gt;The plan crystallized:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the submitted code into an AST&lt;/li&gt;
&lt;li&gt;Normalize the AST (rename variables, strip comments, etc.)&lt;/li&gt;
&lt;li&gt;Hash the normalized output with SHA-256&lt;/li&gt;
&lt;li&gt;Store and index that hash&lt;/li&gt;
&lt;li&gt;On every new submission, compute the hash and check if it already exists&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the hash is in the database, return the cached review. No AI call. No wasted tokens. No waiting.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;two-parsers-for-the-price-of-one&quot;&gt;Two Parsers for the Price of One&lt;/h3&gt;
&lt;p&gt;LogicVisor supports eight languages: JavaScript, TypeScript, Python, Java, C, C++, Go, and Rust. Getting an AST for all of them required two different tools, each with their own approach to the world.&lt;/p&gt;
&lt;h4 id=&quot;babel-parser-for-javascript-and-typescript-&quot;&gt;Babel Parser (for JavaScript and TypeScript)&lt;/h4&gt;
&lt;p&gt;Babel is something most JavaScript developers know as a transpiler — the thing that converts modern JS to something older browsers can run. But under the hood, Babel has a full-featured parser that produces a detailed AST of your JavaScript or TypeScript code. The &lt;code&gt;@babel/parser&lt;/code&gt; package exposes this directly.&lt;/p&gt;
&lt;p&gt;I'd used Babel's &lt;em&gt;output&lt;/em&gt; for years but never touched the parser API directly. It's surprisingly clean. You hand it source code and options, and it gives you back a structured AST you can traverse and manipulate.&lt;/p&gt;
&lt;p&gt;What made Babel the right choice for JS/TS specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native TypeScript support via a plugin — no separate toolchain&lt;/li&gt;
&lt;li&gt;JSX support out of the box&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@babel/traverse&lt;/code&gt; package lets you walk the AST and modify nodes in place, which is exactly how I rename identifiers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@babel/generator&lt;/code&gt; converts the modified AST back to compact source code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The JS/TS canonicalization path: parse with Babel → traverse the AST and rename all non-builtin identifiers to generic names (&lt;code&gt;v0&lt;/code&gt;, &lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;...) → optionally sort top-level declarations → regenerate compact source → hash it.&lt;/p&gt;
&lt;h4 id=&quot;tree-sitter-for-everything-else-&quot;&gt;Tree-sitter (for everything else)&lt;/h4&gt;
&lt;p&gt;Tree-sitter is a different beast. It's a parser &lt;em&gt;generator&lt;/em&gt; — a tool for building fast, incremental, error-tolerant parsers for programming languages. You might have encountered it without knowing it: if you've used Neovim with syntax highlighting that actually understands your code structure (my first encounter with the tool), that's Tree-sitter. Same if you've used editors where renaming a variable highlights every reference correctly. It's doing real parsing, not just regex matching.&lt;/p&gt;
&lt;p&gt;What makes Tree-sitter interesting for this use case is that it has community-maintained grammars for a huge range of languages — C, C++, Go, Rust, Java, Python, and many more. Each grammar is a separate package that teaches Tree-sitter how to parse that language. You install the grammar, set it on a parser instance, and Tree-sitter gives you a concrete syntax tree you can walk.&lt;/p&gt;
&lt;p&gt;I hadn't really understood what Tree-sitter &lt;em&gt;was&lt;/em&gt; before this. I'd seen it referenced in Neovim config files and assumed it was some Vim-specific thing. It's not — it's a general purpose parsing library with bindings for multiple languages including Node.js via the &lt;code&gt;node-tree-sitter&lt;/code&gt; package.&lt;/p&gt;
&lt;p&gt;The Tree-sitter path: parse the source → walk the resulting syntax tree with a DFS traversal → collect all identifier nodes by their byte ranges → build a name mapping (first-occurrence order) → apply replacements from the end of the file backwards (so byte indices don't shift) → normalize whitespace → hash it.&lt;/p&gt;
&lt;p&gt;The backwards-replacement trick is worth calling out. When you're doing string replacements by index, if you replace something early in the file, every subsequent index is now wrong. By sorting replacements by start position descending and working from the end, you avoid that entire class of bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-implementation&quot;&gt;The Implementation&lt;/h3&gt;
&lt;p&gt;Here's how it all fits together.&lt;/p&gt;
&lt;h4 id=&quot;the-simple-canonicalization-pre-ast-&quot;&gt;The Simple Canonicalization (Pre-AST)&lt;/h4&gt;
&lt;p&gt;Before I landed on the AST approach, I had a simpler version that just handled whitespace and comments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export function canonicalizeCode(code: string): string {
  // Remove multi-line comments
  let cleanedCode = code.replace(/\/\*[\s\S]*?\*\//g, &quot;&quot;);

  // Remove single-line comments
  cleanedCode = cleanedCode.replace(/\/\/.*$/gm, &quot;&quot;);

  // Normalize all whitespace to a single space
  cleanedCode = cleanedCode.replace(/\s+/g, &quot; &quot;).trim();

  return cleanedCode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This catches the obvious cases — same code with different formatting or comments. But it falls apart the moment someone renames a variable. The AST approach handles that.&lt;/p&gt;
&lt;h4 id=&quot;the-router&quot;&gt;The Router&lt;/h4&gt;
&lt;p&gt;The main entry point checks the language and dispatches to the right parser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function canonicalizeCodeAST(
  code: string,
  language: LanguageKey,
  options?: CanonicalizeOptions
): Promise&amp;lt;string&amp;gt; {
  const opts = { ...defaultOptions, ...(options || {}) };

  if (language === &quot;javascript&quot; || language === &quot;typescript&quot;) {
    return canonicalizeJS(code, opts);
  }

  // C/C++/Go/Rust/Java/Python go through Tree-sitter
  return canonicalizeWithTreeSitter(code, language, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-babel-path-js-ts-&quot;&gt;The Babel Path (JS/TS)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function canonicalizeJS(
  code: string,
  opts: Required&amp;lt;CanonicalizeOptions&amp;gt;
): string {
  const plugins: any[] = [&quot;jsx&quot;];
  if (opts) plugins.push(&quot;typescript&quot;);

  const ast = babelParser.parse(code, {
    sourceType: &quot;module&quot;,
    plugins,
    allowReturnOutsideFunction: true,
  });

  // Rename all non-builtin identifiers to generic names
  const nameMap = new Map&amp;lt;string, string&amp;gt;();
  let counter = 0;

  traverse(ast as any, {
    Identifier(path) {
      if (!opts.normalizeVariableNames) return;

      const name = path.node.name;
      if (isBuiltinJsName(name)) return;

      if (!nameMap.has(name)) {
        nameMap.set(name, `v${counter++}`);
      }

      path.node.name = nameMap.get(name)!;
    },
  });

  // Optionally sort top-level declarations for order-independent comparison
  if (
    opts.sortTopLevel &amp;amp;&amp;amp;
    (ast as any).program &amp;amp;&amp;amp;
    Array.isArray((ast as any).program.body)
  ) {
    (ast as any).program.body.sort((a: any, b: any) =&amp;gt; {
      const aStr = generate(a).code;
      const bStr = generate(b).code;
      return aStr.localeCompare(bStr);
    });
  }

  // Regenerate compact source from the modified AST
  return generate(ast as any, {
    comments: !opts.removeComments,
    compact: true,
  }).code;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-tree-sitter-path-everything-else-&quot;&gt;The Tree-sitter Path (Everything Else)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function canonicalizeWithTreeSitter(
  code: string,
  language: LanguageKey,
  opts: Required&amp;lt;CanonicalizeOptions&amp;gt; | null
): string {
  const LangCtor = (treeSitterMap as any)[language];

  if (!LangCtor) {
    throw new Error(`Unsupported language for tree-sitter: ${language}`);
  }

  const parser = new Parser();
  parser.setLanguage(LangCtor);

  const tree = parser.parse(code);
  const root = tree.rootNode;

  const identifierNodeTypes = identifierTypeMap[language] || [&quot;identifier&quot;];
  const idNodes: { start: number; end: number; text: string }[] = [];
  const commentNodes: { start: number; end: number }[] = [];

  // DFS traversal to collect identifier and comment nodes
  const stack = [root];
  while (stack.length) {
    const node = stack.pop()!;
    const type = node.type;

    if (type.includes(&quot;comment&quot;) &amp;amp;&amp;amp; opts?.removeComments) {
      commentNodes.push({ start: node.startIndex, end: node.endIndex });
    }

    if (identifierNodeTypes.includes(type) || /identifier|name/i.test(type)) {
      const text = code.slice(node.startIndex, node.endIndex);
      idNodes.push({ start: node.startIndex, end: node.endIndex, text });
    }

    for (let i = 0; i &amp;lt; node.childCount; i++) {
      stack.push(node.child(i) as SyntaxNode);
    }
  }

  // Build name mapping in first-occurrence order
  const nameMap = new Map&amp;lt;string, string&amp;gt;();
  let counter = 0;

  for (const n of idNodes) {
    const orig = n.text;
    if (isLanguageBuiltinName(orig, language)) continue;
    if (!nameMap.has(orig)) {
      nameMap.set(orig, `v${counter++}`);
    }
  }

  // Build replacement list
  const replacements: { start: number; end: number; replacement: string }[] = [];

  if (opts?.removeComments) {
    for (const c of commentNodes) {
      replacements.push({ start: c.start, end: c.end, replacement: &quot;&quot; });
    }
  }

  if (opts?.normalizeVariableNames) {
    for (const id of idNodes) {
      const mapped = nameMap.get(id.text);
      if (mapped) {
        replacements.push({ start: id.start, end: id.end, replacement: mapped });
      }
    }
  }

  // Apply replacements from end to start so byte indices stay valid
  replacements.sort((a, b) =&amp;gt; b.start - a.start);

  let out = code;
  for (const r of replacements) {
    out = out.slice(0, r.start) + r.replacement + out.slice(r.end);
  }

  return out.trim();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;hashing-the-canonical-form&quot;&gt;Hashing the Canonical Form&lt;/h4&gt;
&lt;p&gt;Once we have the normalized source, hashing is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function createCanonicalHash(canonicalCode: string): Promise&amp;lt;string&amp;gt; {
  const encoder = new TextEncoder();
  const data = encoder.encode(canonicalCode);
  const hashBuffer = await crypto.subtle.digest(&quot;SHA-256&quot;, data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) =&amp;gt; b.toString(16).padStart(2, &quot;0&quot;)).join(&quot;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SHA-256 gives us a fixed 64-character hex string regardless of how long the original code was. That's what gets stored and indexed in PostgreSQL.&lt;/p&gt;
&lt;h4 id=&quot;putting-it-all-together-in-the-request-handler&quot;&gt;Putting It All Together in the Request Handler&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;let canonicalCode;
try {
  canonicalCode = await canonicalizeCodeAST(solution, preferred_language);
} catch (error) {
  console.error(&quot;Canonicalization error:&quot;, error);
  return NextResponse.json(
    {
      success: false,
      error: {
        code: &quot;BAD_USER_INPUT&quot;,
        message: &quot;Unable to parse your code. Please check for syntax errors and try again.&quot;,
      },
    },
    { status: 400 }
  );
}

const canonicalHash = await createCanonicalHash(
  typeof canonicalCode === &quot;string&quot; ? canonicalCode : &quot;&quot;
);

// Check cache before touching the AI provider
const cachedReview = await getCachedAIReview(
  preferred_model.id + &quot;-&quot; + canonicalHash
);

if (cachedReview) {
  return NextResponse.json(
    { success: true, data: cachedReview },
    { status: 201 }
  );
}

// No cache hit — proceed with AI review and store the result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model ID is included in the cache key because the same code reviewed by Gemini and Groq will produce different outputs. You don't want to serve a Gemini review to someone who selected Groq.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;does-it-actually-work-&quot;&gt;Does It Actually Work?&lt;/h3&gt;
&lt;p&gt;Here's the test that proved it out. Five TypeScript implementations of the same palindrome algorithm — different variable names, different formatting, different comments, and one with operand order swapped:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Solution 1 — clean, descriptive names
function isPalindrome(x: number): boolean {
  let originalNum = x;
  let reversedNum = 0;
  while (originalNum &amp;gt; 0) {
    let lastDigit = originalNum % 10;
    reversedNum = (reversedNum * 10) + lastDigit;
    originalNum = Math.floor(originalNum / 10);
  }
  return x === reversedNum;
}

// Solution 2 — different variable names
function isPalindrome(n: number): boolean {
  let temp = n;
  let result = 0;
  while (temp &amp;gt; 0) {
    let digit = temp % 10;
    result = (result * 10) + digit;
    temp = Math.floor(temp / 10);
  }
  return n === result;
}

// Solution 3 — compressed formatting + comments
function isPalindrome(x: number): boolean{
  // Store original
  let originalNum=x;let reversedNum=0;
  while(originalNum&amp;gt;0){
    let lastDigit=originalNum%10; /* get digit */
    reversedNum=(reversedNum*10)+lastDigit;
    originalNum=Math.floor(originalNum/10);
  }
  return x===reversedNum;
}

// Solution 4 — addition operands swapped
function isPalindrome(x: number): boolean {
  let originalNum = x;
  let reversedNum = 0;
  while (originalNum &amp;gt; 0) {
    let lastDigit = originalNum % 10;
    reversedNum = lastDigit + (reversedNum * 10); // addition order changed
    originalNum = Math.floor(originalNum / 10);
  }
  return reversedNum === x; // comparison order changed
}

// Solution 5 — completely different algorithm
function isPalindrome(x: number): boolean {
  const str = x.toString();
  return str === str.split('').reverse().join('');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the test harness:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function testDuplicateDetection() {
  const solutions = [ /* ...the five solutions above... */ ];

  console.log(&quot;Code hash comparison:&quot;);

  solutions.forEach(async (code, index) =&amp;gt; {
    const canonicalized = await canonicalizeCodeAST(code, &quot;typescript&quot;);
    const hash = await createCanonicalHash(canonicalized);
    console.log(`\nSolution ${index + 1}:`);
    console.log(`Hash: ${hash.substring(0, 12)}...`);
    console.log(
      `Canonical: ${
        typeof canonicalized === &quot;string&quot; ? canonicalized.substring(0, 100) : &quot;&quot;
      }...`,
    );
  });

  // Canonicalize before hashing — not after
  const hashes = await Promise.all(
    solutions.map(async (code) =&amp;gt; {
      const canonicalized = await canonicalizeCodeAST(code, &quot;typescript&quot;);
      return createCanonicalHash(canonicalized);
    }),
  );
  const uniqueHashes = new Set(hashes);

  console.log(`\nOriginal solutions: ${solutions.length}`);
  console.log(`Unique hashes: ${uniqueHashes.size}`);
  console.log(`Duplicates detected: ${solutions.length - uniqueHashes.size}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;Code hash comparison:

Solution 1:
Hash: f7142f71e35a...
Canonical: function v0(v1:number):boolean{let v2=v1;let v3=0;while(v2&amp;gt;0){let v4=v2%10;v3=v3*10+v4;v2=Math.v5(v2...

Solution 2:
Hash: f7142f71e35a...
Canonical: function v0(v1:number):boolean{let v2=v1;let v3=0;while(v2&amp;gt;0){let v4=v2%10;v3=v3*10+v4;v2=Math.v5(v2...

Solution 3:
Hash: f7142f71e35a...
Canonical: function v0(v1:number):boolean{let v2=v1;let v3=0;while(v2&amp;gt;0){let v4=v2%10;v3=v3*10+v4;v2=Math.v5(v2...

Solution 4:
Hash: da8268fe24dd...
Canonical: function v0(v1:number):boolean{let v2=v1;let v3=0;while(v2&amp;gt;0){let v4=v2%10;v3=v4+v3*10;v2=Math.v5(v2...

Solution 5:
Hash: e3d2696010ad...
Canonical: function v0(v1:number):boolean{const v2=v1.v3();return v2===v2.v4('').v5().v6('');}...

Original solutions: 5
Unique hashes: 3
Duplicates detected: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dhyxcd5wx7i8up034dbi.png&quot; alt=&quot;Console screenshot of duplicate detection test results&quot;&gt;&lt;/p&gt;
&lt;p&gt;Solutions 1, 2, and 3 collapse to the same hash — variable names, whitespace, and comments are all stripped away during canonicalization, leaving identical ASTs. Solution 4 gets a distinct hash because operand order (lastDigit + reversedNum &lt;em&gt; 10 vs reversedNum &lt;/em&gt; 10 + lastDigit) is a syntactic difference the AST preserves — commutativity is math, not structure, and the canonicalizer doesn't evaluate expressions. Solution 5 is correctly identified as a genuinely different algorithm.&lt;br&gt;
Two duplicates detected. Working as intended.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-tradeoffs-worth-knowing&quot;&gt;The Tradeoffs Worth Knowing&lt;/h3&gt;
&lt;p&gt;This approach isn't magic. A few things it doesn't catch:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semantic equivalence across algorithms.&lt;/strong&gt; Two completely different implementations that produce the same result — bubble sort vs. merge sort, for example — will have different hashes. This system detects structural duplicates, not algorithmic equivalence. That's actually the right behavior for a code review platform.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Heuristic identifier detection in Tree-sitter.&lt;/strong&gt; The identifier node types vary between language grammars, and I'm using pattern matching (&lt;code&gt;/identifier|name/i&lt;/code&gt;) as a fallback. It works well in practice but it's not exhaustive. If a grammar names its identifier nodes something unusual, you might miss some.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Builtin lists are incomplete.&lt;/strong&gt; The &lt;code&gt;isBuiltinJsName&lt;/code&gt; and &lt;code&gt;isLanguageBuiltinName&lt;/code&gt; functions cover the most common builtins but aren't exhaustive. If a user names their variable &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;setTimeout&lt;/code&gt;, those won't be normalized. Not a critical flaw, but worth knowing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-took-away-from-this&quot;&gt;What I Took Away From This&lt;/h3&gt;
&lt;p&gt;The part I didn't expect: how much I'd enjoy it. This was one of those problems that pulled me away from the normal web dev loop of &quot;call an API, render some data&quot; and into something that felt closer to computer science. Parsers, ASTs, tree traversal — things I'd learned in school that felt abstract at the time suddenly had a real home in a real production system.&lt;/p&gt;
&lt;p&gt;The other thing I took away: sometimes the right caching strategy isn't about time-to-live or cache invalidation. Sometimes it's about recognizing that two requests are asking for the same thing, even when they look different on the surface. That realization changed how I thought about a whole class of optimization problems.&lt;/p&gt;
&lt;p&gt;If you're building anything that takes code as input — a linter, a reviewer, a plagiarism detector, an interview platform — the AST + hash pattern is worth having in your toolkit.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;LogicVisor is an AI-powered tool that reviews your algorithmic code, breaks down time and space complexity, and gives you the kind of feedback you'd want before a technical interview. You can try it at &lt;a rel=&quot;nofollow&quot; href=&quot;https://logicvisor.vercel.app&quot;&gt;logicvisor.vercel.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/27702/code-fingerprinting-detecting-duplicate-submissions-without-losing-your-mind-your-budget</guid>
<pubDate>Sat, 19 Sep 2026 07:49:18 +0000</pubDate>
</item>
<item>
<title>Why I Move AI Model Calls to the Server — Security, Performance, and Everything In Between</title>
<link>https://coderlegion.com/27700/why-i-move-ai-model-calls-to-the-server-security-performance-and-everything-in-between</link>
<description>&lt;p&gt;When I was building &lt;a rel=&quot;nofollow&quot; href=&quot;https://logicvisor.vercel.app/&quot;&gt;Logicvisor&lt;/a&gt; — an AI-powered tool that reviews your algorithmic code, breaks down time and space complexity, and gives you the kind of feedback you'd want before a technical interview — I had to make a foundational architectural decision early on.&lt;/p&gt;
&lt;p&gt;Where do the AI calls actually live?&lt;/p&gt;
&lt;p&gt;It sounds simple. It really isn't. And I think it's a decision a lot of developers make too quickly, usually defaulting to whatever gets something running fastest. So I want to walk through how I thought about it, what the tradeoffs actually look like in practice, and why for Logicvisor — and honestly most production projects I work on — the answer was never really up for debate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-let-s-establish-what-we-re-even-choosing-between&quot;&gt;First, Let's Establish What We're Even Choosing Between&lt;/h3&gt;
&lt;p&gt;When your app needs to talk to an AI model — Gemini, Claude, GPT, whatever — that HTTP request to the model provider has to originate &lt;em&gt;somewhere&lt;/em&gt;. You have two options:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client-side:&lt;/strong&gt; The browser makes the call directly to the AI provider's API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server-side:&lt;/strong&gt; The browser calls your server, your server calls the AI provider, and the response comes back through your infrastructure.&lt;/p&gt;
&lt;p&gt;That's the whole decision. But the consequences of each branch run deep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-case-for-client-side-ai-calls&quot;&gt;The Case for Client-Side AI Calls&lt;/h3&gt;
&lt;p&gt;Let's be fair to the other side first, because client-side AI calls aren't just laziness — there are legitimate reasons to reach for them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero backend overhead.&lt;/strong&gt; If you're prototyping, building an MVP, or hacking something together for a weekend project, standing up a server just to proxy AI calls adds friction you might not need yet. The client calls the API, gets a response, done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One less network hop.&lt;/strong&gt; Client → AI provider is a straight line. Client → your server → AI provider is also a straight line, but a longer one. Every additional hop is a potential source of latency, and if your server is not geographically close to the AI provider, that gap compounds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fast iteration during development.&lt;/strong&gt; Tweak a prompt, refresh the page, see the result. No redeployment cycle, no server restart. For the early exploratory phase of building with AI, this feedback loop is genuinely valuable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fine for purely client-facing tools.&lt;/strong&gt; If you're building something that doesn't touch your own database, doesn't need user sessions, and doesn't have sensitive business logic — a personal productivity tool, a browser extension, an internal utility — client-side calls can be perfectly appropriate.&lt;/p&gt;
&lt;p&gt;So that's the honest upside. Now here's where it falls apart.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-client-side-ai-calls-break-down-in-production&quot;&gt;Why Client-Side AI Calls Break Down in Production&lt;/h3&gt;
&lt;h4 id=&quot;the-api-key-problem&quot;&gt;The API Key Problem&lt;/h4&gt;
&lt;p&gt;This is the most obvious one, but it's worth being precise about &lt;em&gt;why&lt;/em&gt; it's as bad as it is.&lt;/p&gt;
&lt;p&gt;When you make an API call from the browser, your API key has to be in that request. There's no way around this — the provider needs to authenticate you. And since that request is made from the browser, the key is accessible to anyone who opens DevTools, intercepts traffic, or extracts it from your bundled JavaScript.&lt;/p&gt;
&lt;p&gt;The consequence isn't just that someone can &lt;em&gt;see&lt;/em&gt; your key. It's that they can &lt;em&gt;use&lt;/em&gt; it. At your expense. Without your knowledge. AI API billing is usage-based, which means a single bad actor with your key can run up a bill that drains your account before your monitoring even fires an alert — if you have monitoring at all.&lt;/p&gt;
&lt;p&gt;Key rotation helps, but it's reactive. The damage is usually already done.&lt;/p&gt;
&lt;h4 id=&quot;your-prompt-engineering-is-public&quot;&gt;Your Prompt Engineering Is Public&lt;/h4&gt;
&lt;p&gt;This one gets less attention but matters more than people realize.&lt;/p&gt;
&lt;p&gt;The prompts you write are often where your actual product value lives. If you've spent time crafting a system prompt that makes your AI reviewer give structured, consistent, high-quality feedback on algorithmic code — that prompt &lt;em&gt;is&lt;/em&gt; the product. Client-side calls expose it completely. A competitor can open DevTools, read your system prompt, and replicate your core feature in an afternoon.&lt;/p&gt;
&lt;p&gt;On the server, your prompts never leave your infrastructure. The client sends input; the server decides what to do with it.&lt;/p&gt;
&lt;h4 id=&quot;you-have-no-control-over-abuse&quot;&gt;You Have No Control Over Abuse&lt;/h4&gt;
&lt;p&gt;On the client side, there's nothing stopping a user from writing a script that hammers your AI endpoint in a loop. Every one of those requests hits the AI provider and costs you tokens. You have no rate limiting, no request validation, no way to enforce quotas per user.&lt;/p&gt;
&lt;p&gt;You're not just vulnerable to malicious actors either — a bug in your own frontend code that causes unintended re-fetching can silently burn through your API budget.&lt;/p&gt;
&lt;h4 id=&quot;no-caching&quot;&gt;No Caching&lt;/h4&gt;
&lt;p&gt;AI API calls cost money per token. If multiple users ask your tool to review functionally identical code, why would you want to pay for that same inference three hundred times?&lt;/p&gt;
&lt;p&gt;On the client side, you can't cache at the API level. Every identical request goes to the provider, incurs latency, and costs tokens. On the server, you can cache responses intelligently — hash the input, check your cache layer, return the cached result. You pay once.&lt;/p&gt;
&lt;h4 id=&quot;you-re-flying-blind-in-production&quot;&gt;You're Flying Blind in Production&lt;/h4&gt;
&lt;p&gt;Without server-side infrastructure, you have no centralized view of how your AI layer is actually being used. Which prompts are performing well? Which inputs are producing garbage responses? Which users are hitting rate limits? Where is your token spend going?&lt;/p&gt;
&lt;p&gt;Client-side AI calls mean you're guessing at all of this. Logs, monitoring, and observability — the basic instrumentation of a production system — require a server in the loop.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-case-for-server-side-ai-calls&quot;&gt;The Case for Server-Side AI Calls&lt;/h3&gt;
&lt;p&gt;With that context established, here's what you actually get when the AI calls live on the server.&lt;/p&gt;
&lt;h4 id=&quot;the-api-key-never-touches-the-client&quot;&gt;The API Key Never Touches the Client&lt;/h4&gt;
&lt;p&gt;Your key lives in an environment variable on the server. The client has zero knowledge of it, zero access to it, and zero ability to extract it. This is the minimum acceptable security posture for any application that will see real users.&lt;/p&gt;
&lt;h4 id=&quot;you-control-rate-limiting&quot;&gt;You Control Rate Limiting&lt;/h4&gt;
&lt;p&gt;You decide how many requests a given user can make in a given window. You can enforce this per account, per IP, per session — whatever your threat model calls for. Abuse becomes something you manage rather than something that happens to you.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export async function enforceAIRateLimit(
userId: string,
request?: NextRequest
): Promise&amp;lt;RateLimitResult&amp;gt; {
const tierLimits = await getUserTierLimits(userId);

if (!tierLimits) {
throw new RateLimitError(&quot;Unable to determine user tier limits&quot;);
}

// Free/Pro users: per-minute limits
if (tierLimits.ai_requests_per_minute !== null) {
const minuteLimit = await checkRateLimit(userId, &quot;ai_request&quot;, &quot;minute&quot;);

if (!minuteLimit.isAllowed) {
  await logRateLimitViolation(userId, &quot;ai_request&quot;, &quot;minute&quot;, ...);
  throw new RateLimitError(``Rate limit exceeded. Resets at ${minuteLimit.resetTime.toISOString()}``);
}

return minuteLimit;
}

// Admin users: daily + monthly
const dailyLimit = await checkRateLimit(userId, &quot;ai_request&quot;, &quot;daily&quot;);
// ...monthly check follows same pattern
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;response-caching-becomes-possible&quot;&gt;Response Caching Becomes Possible&lt;/h4&gt;
&lt;p&gt;Identical or near-identical inputs can return cached results, cutting both latency and cost. For a tool like Logicvisor where multiple users might submit similar sorting algorithm implementations, the savings on repeated inferences compound quickly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Normalize the code to a canonical form before hashing
// so that formatting differences don't result in cache misses
const canonicalCode = await canonicalizeCodeAST(solution, preferred_language);
const canonicalHash = await createCanonicalHash(
  typeof canonicalCode === &quot;string&quot; ? canonicalCode : &quot;&quot;
);

// Check cache before hitting the AI provider
const cachedReview = await getCachedAIReview(
  preferred_model.id + &quot;-&quot; + canonicalHash
);
if (cachedReview) {
  return NextResponse.json(
    { success: true, data: cachedReview },
    { status: 201 }
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;you-can-inject-server-side-context-into-prompts&quot;&gt;You Can Inject Server-Side Context Into Prompts&lt;/h4&gt;
&lt;p&gt;This is where things get architecturally interesting. The client sends raw input — code, a question, a request. But your server knows things the client doesn't: who the user is, what their history looks like, what tier they're on, what language they've selected, what results they've already received. All of that context can be injected into the prompt &lt;em&gt;before&lt;/em&gt; it ever leaves your infrastructure.&lt;/p&gt;
&lt;p&gt;The client can't fake or manipulate that context because it never touches it.&lt;/p&gt;
&lt;h4 id=&quot;full-observability&quot;&gt;Full Observability&lt;/h4&gt;
&lt;p&gt;Every request is logged. Every response is traceable. You can monitor token usage, flag anomalous behavior, track which prompts produce the best results, and debug production issues with actual data. This is what running software in production looks like.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await updateAPIUsageAnalytics(&quot;/api/internal&quot;, true, responseTime, aiTokensUsed, estimatedCost, user.id, {
  modelName: modelUsed,
  modelProvider,
  inputTokens,
  outputTokens,
  totalTokens: aiTokensUsed,
  actualCostUsd,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-bff-argument-one-round-trip-instead-of-many&quot;&gt;The BFF Argument — One Round Trip Instead of Many&lt;/h4&gt;
&lt;p&gt;There's a broader architectural win here that goes beyond just AI calls. Without a server in the middle, the client has to orchestrate everything itself: call the AI, wait for the response, then maybe hit your database, wait again, then update the UI. Each of those is a visible pause for the user.&lt;/p&gt;
&lt;p&gt;With a Backend for Frontend (BFF) pattern, the client makes one request. The server handles the AI call, processes the response, queries the database if needed, applies any business logic, and returns a single resolved payload. The user feels one network round trip instead of a cascading waterfall of them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-diagrams&quot;&gt;Architecture Diagrams&lt;/h3&gt;
&lt;p&gt;Before getting into the implementation specifics, here's the architectural difference visualised.&lt;/p&gt;
&lt;h4 id=&quot;client-side-ai-calls-the-problem&quot;&gt;Client-Side AI Calls — The Problem&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uhpgoz7l818tpstti3xt.png&quot; alt=&quot;Client-side architecture diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Browser → AI Provider directly · API key exposed in transit&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;server-side-ai-calls-the-solution&quot;&gt;Server-Side AI Calls — The Solution&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7rdgtpkuhj8z5rxex2r.png&quot; alt=&quot;Server-side architecture diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Browser → API Route → [Cache · Rate Limiter · AI Provider · DB] → Browser&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-this-played-out-in-logicvisor&quot;&gt;How This Played Out in Logicvisor&lt;/h3&gt;
&lt;p&gt;Let me get concrete. Here's what moving the AI layer to the server actually looked like in practice.&lt;/p&gt;
&lt;h4 id=&quot;performance-collapsing-the-waterfall&quot;&gt;Performance — Collapsing the Waterfall&lt;/h4&gt;
&lt;p&gt;Logicvisor uses Supabase on the backend. At various points in the app, I need to pull data from multiple tables, run the AI review, and return everything the page needs in one shot.&lt;/p&gt;
&lt;p&gt;If this were all happening on the client, you'd be looking at: call Supabase for user context → wait → call the AI provider → wait → call Supabase again for historical reviews → wait → render. Each of those waits is visible to the user, and each one is an opportunity for something to fail mid-chain.&lt;/p&gt;
&lt;p&gt;On the server, those calls happen in close proximity to each other and to the data. The AI call, the database queries, and any necessary transformations all resolve server-side, and the client gets one clean response. The user experiences a single loading state, not a series of UI flickers.&lt;/p&gt;
&lt;p&gt;There's also the matter of compute. Parsing and processing a large AI response — stripping JSON fences, validating structure, transforming the output into the format the UI expects — is work that browsers are not well-suited for. Browsers are memory and CPU constrained by design, and they're competing with the DOM, with other tabs, with everything the user has open. A server doesn't have those constraints.&lt;/p&gt;
&lt;h4 id=&quot;security-api-obfuscation-as-a-feature&quot;&gt;Security — API Obfuscation as a Feature&lt;/h4&gt;
&lt;p&gt;Moving calls to the server means the client's entire interaction is scoped to your own API. It calls your endpoint, gets a response, done. It has no visibility into what your server does with that request internally — which external APIs it calls, what keys those calls carry, or how the response was constructed.&lt;/p&gt;
&lt;p&gt;This isn't security through obscurity; the obfuscation is a structural property of the architecture. OS-level network tools could theoretically expose some of this, but you've dramatically raised the bar for what an attacker needs to do to compromise your stack.&lt;/p&gt;
&lt;p&gt;Your system prompts — the part of the product that actually encodes your domain knowledge and review methodology — never leave the server. That's not a small thing.&lt;/p&gt;
&lt;h4 id=&quot;authentication-stateless-and-seamless&quot;&gt;Authentication — Stateless and Seamless&lt;/h4&gt;
&lt;p&gt;A server in the loop makes proper auth architecture dramatically cleaner. I was able to set HttpOnly cookies, attach signed JWT tokens, and build a stateless authentication and authorization system that the client participates in without controlling.&lt;/p&gt;
&lt;p&gt;Without a server, you end up storing tokens in localStorage or client-side state, which is a well-documented attack surface. The session becomes something the client manages, which means it's something an attacker can manipulate.&lt;/p&gt;
&lt;h4 id=&quot;caching-paying-for-inference-once&quot;&gt;Caching — Paying for Inference Once&lt;/h4&gt;
&lt;p&gt;Token costs are real. For Logicvisor, where users might submit variations of common algorithm patterns — bubble sort, binary search, dynamic programming problems — I can cache AI responses keyed on a normalized hash of the input. A user submitting a well-known algorithm implementation gets a fast, cached response. The AI provider gets called once.&lt;/p&gt;
&lt;p&gt;This also improves response times for cached queries significantly. The round trip to an AI provider is the most expensive part of the request by a wide margin. Eliminating it for repeat queries is the single highest-leverage performance optimization available to you.&lt;/p&gt;
&lt;h4 id=&quot;provider-abstraction-switching-models-without-touching-the-client&quot;&gt;Provider Abstraction — Switching Models Without Touching the Client&lt;/h4&gt;
&lt;p&gt;Here's something the client-side approach makes nearly impossible: swapping AI providers without your frontend caring at all.&lt;/p&gt;
&lt;p&gt;Logicvisor supports both Gemini and Groq depending on the user's selected model. Gemini for deeper analysis with its thinking budget and Google Search grounding, Groq for speed. Two different SDKs, two different response shapes, two different token counting strategies, two different pricing models. The client knows none of this. It sends a request, it gets a review back.&lt;/p&gt;
&lt;p&gt;That abstraction only works because the AI calls live on the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;switch (modelProvider.toLowerCase()) {
  case &quot;google&quot;:
    // Gemini — thinking budget + Google Search grounding
    const response = await ai.models.generateContent({
      model: preferred_model.id,
      config: {
        thinkingConfig: { thinkingBudget: prompt.estimatedTokens },
        tools: [{ googleSearch: {} }],
        seed: SEED,
        temperature: TEMPERATURE,
      },
      contents,
    });
    aiReviewText = response.text ?? &quot;&quot;;
    // Extract actual token counts from usageMetadata
    inputTokens = response.usageMetadata?.promptTokenCount || 0;
    outputTokens = response.usageMetadata?.candidatesTokenCount || 0;
    break;

  case &quot;groq&quot;:
    // Llama 3.3 70B — forces structured JSON response
    const groqResponse = await groq.chat.completions.create({
      messages: [{ role: &quot;user&quot;, content: prompt.content }],
      model: groqModel,
      temperature: TEMPERATURE,
      seed: SEED,
      response_format: { type: &quot;json_object&quot; },
      stream: false,
    });
    aiReviewText = groqResponse.choices[0].message.content ?? &quot;&quot;;
    inputTokens = groqResponse.usage?.prompt_tokens || 0;
    outputTokens = groqResponse.usage?.completion_tokens || 0;
    break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I wanted to add a third provider tomorrow — say, Claude for a specific model tier — that's a new &lt;code&gt;case&lt;/code&gt; block on the server. The client contract doesn't change. No frontend deployment, no API key exposure, no breaking changes for users mid-session.&lt;/p&gt;
&lt;p&gt;Try doing this cleanly when the calls are in the browser. You'd be shipping provider-specific SDK logic, API keys, and token counting math directly to the client — and every provider swap would mean a frontend change. The server is the only place this kind of abstraction is clean.&lt;/p&gt;
&lt;p&gt;This also matters for cost tracking. Notice that each branch extracts token counts differently — Gemini from &lt;code&gt;usageMetadata&lt;/code&gt;, Groq from &lt;code&gt;usage&lt;/code&gt;. That per-provider normalization feeds into the analytics pipeline downstream, giving you a consistent view of cost across providers regardless of how each SDK reports it. That's only possible because all the provider-specific handling is in one place.&lt;/p&gt;
&lt;h4 id=&quot;ai-response-processing-cleaning-up-the-model-s-output&quot;&gt;AI Response Processing — Cleaning Up the Model's Output&lt;/h4&gt;
&lt;p&gt;Here's the unglamorous part that doesn't get written about enough: AI models don't always return clean output.&lt;/p&gt;
&lt;p&gt;Gemini, for example, sometimes wraps JSON responses in markdown fences even when you've explicitly told it not to. If your UI is trying to parse that response and render structured data, you need to clean it before it gets anywhere near the client.&lt;/p&gt;
&lt;p&gt;On the server, I handle all of that: strip the fences, validate the JSON structure, handle the cases where the model returned something unexpected, and only send a clean, predictable payload to the client. If something goes wrong at this layer, I can log it, inspect it, and fix the prompt. The client just sees a well-structured response or a proper error.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export function extractJSONFromMarkdown(markdownString: string) {
  try {
    // Remove the ```json wrapper
    let jsonString = markdownString.trim();
    
    // Remove ```json from start
    if (jsonString.startsWith(&quot;```json&quot;)) {
      jsonString = jsonString.substring(7);
    }
    
    // Remove ``` from end
    if (jsonString.endsWith(&quot;```&quot;)) {
      jsonString = jsonString.substring(0, jsonString.length - 3);
    }
    
    // Parse the JSON
    const parsedData: PromptResult = JSON.parse(jsonString.trim());
    
    // Extract and decode the markdown_review field
    let markdownReview = parsedData.markdown_review;
    if (markdownReview) {
      // Decode JSON escape sequences
      markdownReview = markdownReview
        .replace(/\\n/g, &quot;\n&quot;)   // Convert \n to actual newlines
        .replace(/\\t/g, &quot;\t&quot;)   // Convert \t to actual tabs
        .replace(/\\r/g, &quot;\r&quot;)   // Convert \r to carriage returns
        .replace(/\\&quot;/g, '&quot;')    // Convert \&quot; to actual quotes
        .replace(/\\\\/g, &quot;\\&quot;); // Convert \\ to actual backslashes
        
      return { parsedData, markdownReview };
    } else {
      console.log(&quot;No markdown_review field found&quot;);
      return null;
    }
  } catch (error) {
    console.error(&quot;Error parsing JSON:&quot;, error);
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this processing happened on the client, every user's browser would be doing it — inconsistently, with no visibility, and with no way to fix edge cases without a frontend deployment.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;so-was-it-ever-really-a-debate-&quot;&gt;So Was It Ever Really a Debate?&lt;/h3&gt;
&lt;p&gt;Honestly, not for Logicvisor.&lt;/p&gt;
&lt;p&gt;Client-side AI calls are a valid tool in specific, narrow contexts — personal tools, internal utilities, quick prototypes where security is not a concern and scale is not a goal. The moment you have real users, real API costs, and real data flowing through your system, the calculus changes completely.&lt;/p&gt;
&lt;p&gt;Security, performance, observability, and maintainability all point in the same direction. The extra infrastructure is real overhead. But it's the kind of overhead that pays for itself the first time an abuse attempt hits your rate limiter instead of your API bill.&lt;/p&gt;
&lt;p&gt;For any production system where AI inference is part of the core product — keep it on the server. Build the client thin. Let the backend do the heavy lifting.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27700/why-i-move-ai-model-calls-to-the-server-security-performance-and-everything-in-between</guid>
<pubDate>Sat, 19 Sep 2026 07:45:04 +0000</pubDate>
</item>
<item>
<title>We Rebuilt MYZ Payments Around a Canonical Ledger — Here’s What We Learned</title>
<link>https://coderlegion.com/27661/we-rebuilt-myz-payments-around-a-canonical-ledger-heres-what-we-learned</link>
<description>&lt;p&gt;We Rebuilt MYZ Payments Around a Canonical Ledger — Here’s What We Learned&lt;/p&gt;
&lt;p&gt;When we started adding internal MYZ payments to MyZubster Marketplace, the obvious task looked simple:&lt;/p&gt;
&lt;p&gt;buyer balance -= amount&lt;br&gt;
seller balance += amount&lt;/p&gt;
&lt;p&gt;That turned out to be the wrong level of abstraction.&lt;/p&gt;
&lt;p&gt;The real challenge was not adding a payment button. It was making sure that every part of the application agreed on what a MYZ balance actually is.&lt;/p&gt;
&lt;p&gt;During the implementation, we found multiple historical paths capable of representing or modifying MYZ independently.&lt;/p&gt;
&lt;p&gt;That is exactly the kind of situation that eventually produces accounting bugs.&lt;/p&gt;
&lt;p&gt;So instead of adding another payment path, we refactored the system around one rule:&lt;/p&gt;
&lt;p&gt;There must be one canonical source of truth for spendable MYZ.&lt;/p&gt;
&lt;p&gt;This post covers the architecture we implemented, the problems we found, and the trade-offs that are still open.&lt;/p&gt;
&lt;p&gt;The original problem: balances were too easy to create&lt;/p&gt;
&lt;p&gt;A field like this looks harmless:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  userId,&lt;br&gt;
  balanceMYZ: 500&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Until another subsystem has:&lt;/p&gt;
&lt;p&gt;{&lt;br&gt;
  accountId,&lt;br&gt;
  myzCredit: 200&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;And another computes:&lt;/p&gt;
&lt;p&gt;displayedBalance =&lt;br&gt;
  ledgerBalance +&lt;br&gt;
  mongoCredits -&lt;br&gt;
  utilityRedemptions;&lt;/p&gt;
&lt;p&gt;Now the question becomes:&lt;/p&gt;
&lt;p&gt;What is the user's real balance?&lt;/p&gt;
&lt;p&gt;If multiple components can answer that question differently, the system no longer has a reliable accounting model.&lt;/p&gt;
&lt;p&gt;That was the architectural problem we wanted to eliminate.&lt;/p&gt;
&lt;p&gt;First principle: MYZ is an internal accounting unit&lt;/p&gt;
&lt;p&gt;Before touching the code, we established the semantics.&lt;/p&gt;
&lt;p&gt;MYZ is an internal MyZubster utility/accounting credit.&lt;/p&gt;
&lt;p&gt;It is not automatically convertible into:&lt;/p&gt;
&lt;p&gt;EUR&lt;br&gt;
BTC&lt;br&gt;
XMR&lt;br&gt;
cash&lt;/p&gt;
&lt;p&gt;and it is not represented by this implementation as an on-chain token.&lt;/p&gt;
&lt;p&gt;This matters technically because it means we do not need an exchange-rate engine in order to spend MYZ.&lt;/p&gt;
&lt;p&gt;If a Marketplace listing costs:&lt;/p&gt;
&lt;p&gt;250 MYZ&lt;/p&gt;
&lt;p&gt;the payment is simply:&lt;/p&gt;
&lt;p&gt;250 MYZ&lt;/p&gt;
&lt;p&gt;There is no intermediate:&lt;/p&gt;
&lt;p&gt;250 MYZ → €X → seller&lt;/p&gt;
&lt;p&gt;Likewise, a Zorgax plan can have an explicit MYZ price without deriving it from its EUR price.&lt;/p&gt;
&lt;p&gt;The canonical ledger&lt;/p&gt;
&lt;p&gt;The core service is:&lt;/p&gt;
&lt;p&gt;src/services/myzLedgerApiService.js&lt;/p&gt;
&lt;p&gt;Instead of mutating a user balance directly, transfers are recorded as ledger entries.&lt;/p&gt;
&lt;p&gt;A payment creates two legs:&lt;/p&gt;
&lt;p&gt;buyer    -250 MYZ&lt;br&gt;
seller   +250 MYZ&lt;/p&gt;
&lt;p&gt;Both are linked by the same:&lt;/p&gt;
&lt;p&gt;transfer_id&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;              transfer_id: T-123
             ┌─────────────────┐
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;buyer account    │ -250 MYZ        │&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             │                 │
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;seller account   │ +250 MYZ        │&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us something a plain balance field does not provide naturally:&lt;/p&gt;
&lt;p&gt;auditability.&lt;/p&gt;
&lt;p&gt;We can answer:&lt;/p&gt;
&lt;p&gt;Why is this balance 750 MYZ?&lt;/p&gt;
&lt;p&gt;by looking at the entries that produced it.&lt;/p&gt;
&lt;p&gt;Transfers are atomic inside the ledger&lt;/p&gt;
&lt;p&gt;The transfer operation performs both accounting legs under the same write operation.&lt;/p&gt;
&lt;p&gt;The important invariant is:&lt;/p&gt;
&lt;p&gt;debit exists  ⇔  credit exists&lt;/p&gt;
&lt;p&gt;We do not want:&lt;/p&gt;
&lt;p&gt;buyer -250&lt;/p&gt;
&lt;p&gt;without:&lt;/p&gt;
&lt;p&gt;seller +250&lt;/p&gt;
&lt;p&gt;because the process crashed halfway through.&lt;/p&gt;
&lt;p&gt;The ledger therefore validates the transfer first, then persists both entries together.&lt;/p&gt;
&lt;p&gt;The relevant checks include:&lt;/p&gt;
&lt;p&gt;valid payer&lt;br&gt;
valid payee&lt;br&gt;
payer != payee&lt;br&gt;
valid amount&lt;br&gt;
sufficient balance&lt;br&gt;
valid idempotency key&lt;br&gt;
no transfer ID collision&lt;/p&gt;
&lt;p&gt;Only after those checks do the entries become recorded.&lt;/p&gt;
&lt;p&gt;Never trust the UI to prevent overspending&lt;/p&gt;
&lt;p&gt;One of the tests we cared about most was not:&lt;/p&gt;
&lt;p&gt;Can Alice buy an item?&lt;/p&gt;
&lt;p&gt;It was:&lt;/p&gt;
&lt;p&gt;Can Alice buy two items at the same time&lt;br&gt;
when she can only afford one?&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;Alice balance: 300 MYZ&lt;/p&gt;
&lt;p&gt;Purchase A: 250 MYZ&lt;br&gt;
Purchase B: 250 MYZ&lt;/p&gt;
&lt;p&gt;Both requests may arrive concurrently.&lt;/p&gt;
&lt;p&gt;A naïve implementation can do this:&lt;/p&gt;
&lt;p&gt;if (balance &amp;gt;= 250) {&lt;br&gt;
  // request A sees 300&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;if (balance &amp;gt;= 250) {&lt;br&gt;
  // request B also sees 300&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;Then both succeed.&lt;/p&gt;
&lt;p&gt;Final logical balance:&lt;/p&gt;
&lt;p&gt;-200 MYZ&lt;/p&gt;
&lt;p&gt;That is unacceptable for an accounting system.&lt;/p&gt;
&lt;p&gt;Our expected result is:&lt;/p&gt;
&lt;p&gt;one purchase succeeds&lt;br&gt;
one purchase fails&lt;br&gt;
final balance = 50 MYZ&lt;/p&gt;
&lt;p&gt;The protection belongs in the ledger write path, not in React.&lt;/p&gt;
&lt;p&gt;Idempotency became a first-class concept&lt;/p&gt;
&lt;p&gt;Payment APIs are retried.&lt;/p&gt;
&lt;p&gt;Browsers retry.&lt;/p&gt;
&lt;p&gt;Networks fail.&lt;/p&gt;
&lt;p&gt;Users double-click.&lt;/p&gt;
&lt;p&gt;Servers can complete the financial operation and crash before returning 200 OK.&lt;/p&gt;
&lt;p&gt;So this API:&lt;/p&gt;
&lt;p&gt;POST /api/marketplace/orders/:id/payment/myz&lt;/p&gt;
&lt;p&gt;requires an idempotency key.&lt;/p&gt;
&lt;p&gt;A simplified request looks like:&lt;/p&gt;
&lt;p&gt;Idempotency-Key: marketplace-order-123&lt;/p&gt;
&lt;p&gt;If the exact operation is retried, we return the original transfer instead of charging again.&lt;/p&gt;
&lt;p&gt;But there is another subtle problem.&lt;/p&gt;
&lt;p&gt;What happens if somebody reuses the same idempotency key with a different payload?&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;key = abc&lt;br&gt;
plan = Zorgax Pro&lt;br&gt;
amount = 40 MYZ&lt;/p&gt;
&lt;p&gt;followed by:&lt;/p&gt;
&lt;p&gt;key = abc&lt;br&gt;
plan = Zorgax Developer&lt;br&gt;
amount = 40 MYZ&lt;/p&gt;
&lt;p&gt;The amount is identical, but the operation is not.&lt;/p&gt;
&lt;p&gt;So idempotency is bound not only to the numeric amount, but also to the semantic reference of the operation.&lt;/p&gt;
&lt;p&gt;Same key + different meaning = conflict.&lt;/p&gt;
&lt;p&gt;That protects against a class of bugs that simple &quot;key already exists&quot; implementations often miss.&lt;/p&gt;
&lt;p&gt;Marketplace became a true buyer → seller transfer&lt;/p&gt;
&lt;p&gt;For MYZ-priced Marketplace orders, the flow is now:&lt;/p&gt;
&lt;p&gt;Buyer&lt;br&gt;
  ↓&lt;br&gt;
canonical MYZ transfer&lt;br&gt;
  ↓&lt;br&gt;
Seller&lt;br&gt;
  ↓&lt;br&gt;
order marked PAID&lt;/p&gt;
&lt;p&gt;The order stores evidence of the accounting operation:&lt;/p&gt;
&lt;p&gt;payment: {&lt;br&gt;
  asset: 'MYZ',&lt;br&gt;
  network: 'internal-ledger',&lt;br&gt;
  status: 'PAID',&lt;br&gt;
  transferId,&lt;br&gt;
  debitEntryId,&lt;br&gt;
  creditEntryId&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;We also enforce several business invariants.&lt;/p&gt;
&lt;p&gt;The buyer must be authenticated.&lt;/p&gt;
&lt;p&gt;The buyer must actually own the order.&lt;/p&gt;
&lt;p&gt;Buyer and seller cannot be the same account.&lt;/p&gt;
&lt;p&gt;The order must be accepted before payment.&lt;/p&gt;
&lt;p&gt;And an unpaid MYZ order cannot transition to completed.&lt;/p&gt;
&lt;p&gt;The important point is that these rules are checked server-side.&lt;/p&gt;
&lt;p&gt;The React button is just UX.&lt;/p&gt;
&lt;p&gt;Application state and ledger state are not the same transaction&lt;/p&gt;
&lt;p&gt;There is an interesting systems problem here.&lt;/p&gt;
&lt;p&gt;The MYZ ledger and the Marketplace order are not stored in the same transactional database.&lt;/p&gt;
&lt;p&gt;So this can happen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MYZ transfer succeeds&lt;/li&gt;
&lt;li&gt;application crashes&lt;/li&gt;
&lt;li&gt;order has not yet been marked PAID&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You cannot solve that with a normal database transaction across unrelated stores.&lt;/p&gt;
&lt;p&gt;Instead, we use an idempotent recovery model.&lt;/p&gt;
&lt;p&gt;On retry:&lt;/p&gt;
&lt;p&gt;ledger sees existing transfer&lt;br&gt;
→ does not charge again&lt;/p&gt;
&lt;p&gt;application receives original receipt&lt;br&gt;
→ retries order persistence&lt;/p&gt;
&lt;p&gt;This is closer to a small saga than a traditional ACID transaction.&lt;/p&gt;
&lt;p&gt;The important requirement is recoverability.&lt;/p&gt;
&lt;p&gt;Zorgax uses the same accounting primitive&lt;/p&gt;
&lt;p&gt;We reused the same ledger for Zorgax plans.&lt;/p&gt;
&lt;p&gt;The flow is:&lt;/p&gt;
&lt;p&gt;user MYZ account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;canonical transfer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;zorgax:system:treasury&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;grant entitlement&lt;/p&gt;
&lt;p&gt;Prices are configuration values:&lt;/p&gt;
&lt;p&gt;ZORGAX_PRO_PRICE_MYZ&lt;br&gt;
ZORGAX_DEVELOPER_PRICE_MYZ&lt;/p&gt;
&lt;p&gt;They are deliberately not calculated from:&lt;/p&gt;
&lt;p&gt;priceEur&lt;/p&gt;
&lt;p&gt;If the MYZ price is missing, the checkout fails closed.&lt;/p&gt;
&lt;p&gt;Production currently reports the MYZ plan as unavailable instead of guessing a price.&lt;/p&gt;
&lt;p&gt;That is a feature, not a failure.&lt;/p&gt;
&lt;p&gt;The most important refactor was deleting shadow accounting&lt;/p&gt;
&lt;p&gt;While reviewing the system, we found an older Payment Dashboard flow that effectively calculated MYZ like this:&lt;/p&gt;
&lt;p&gt;canonical ledger&lt;br&gt;
+&lt;/p&gt;
&lt;h3 id=&quot;mongo-credits&quot;&gt;Mongo credits&lt;/h3&gt;
&lt;p&gt;Mongo redemptions&lt;/p&gt;
&lt;p&gt;It also contained an automatic EUR → MYZ rule.&lt;/p&gt;
&lt;p&gt;That meant the canonical ledger was not actually canonical.&lt;/p&gt;
&lt;p&gt;So we changed the model.&lt;/p&gt;
&lt;p&gt;New Stripe payments do not automatically generate MYZ.&lt;/p&gt;
&lt;p&gt;Historical Mongo conversion records remain available as legacy audit data, but they no longer contribute to spendable MYZ.&lt;/p&gt;
&lt;p&gt;Utility spending now hits the canonical ledger.&lt;/p&gt;
&lt;p&gt;MongoDB redemption records are retained for things like:&lt;/p&gt;
&lt;p&gt;fulfillment status&lt;br&gt;
receipt metadata&lt;br&gt;
application workflow&lt;/p&gt;
&lt;p&gt;but they are no longer a second balance system.&lt;/p&gt;
&lt;p&gt;That distinction is extremely useful:&lt;/p&gt;
&lt;p&gt;ledger = accounting truth&lt;/p&gt;
&lt;p&gt;application database = business state&lt;br&gt;
Then we found another hard-coded system: Marketplace categories&lt;/p&gt;
&lt;p&gt;The same development cycle uncovered a less dangerous but very visible problem.&lt;/p&gt;
&lt;p&gt;The React Marketplace category list was mostly hard-coded.&lt;/p&gt;
&lt;p&gt;That means adding a category required changing application code.&lt;/p&gt;
&lt;p&gt;We replaced that with a dynamic catalog:&lt;/p&gt;
&lt;p&gt;GET /api/listings/categories&lt;/p&gt;
&lt;p&gt;Users can now propose new categories from the Marketplace itself.&lt;/p&gt;
&lt;p&gt;The interesting UX decision is that the creator can use their pending category immediately for their own listing.&lt;/p&gt;
&lt;p&gt;Global visibility still requires moderation.&lt;/p&gt;
&lt;p&gt;So the flow becomes:&lt;/p&gt;
&lt;p&gt;create category&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pending&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;creator may use it&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;admin review&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;global category&lt;/p&gt;
&lt;p&gt;This is much more flexible than making category taxonomy a deploy-time decision.&lt;/p&gt;
&lt;p&gt;Knowledge became a Marketplace object too&lt;/p&gt;
&lt;p&gt;We also introduced a first-class:&lt;/p&gt;
&lt;p&gt;knowledge&lt;/p&gt;
&lt;p&gt;category.&lt;/p&gt;
&lt;p&gt;Users can submit things like:&lt;/p&gt;
&lt;p&gt;guides&lt;br&gt;
methods&lt;br&gt;
know-how&lt;br&gt;
tutorials&lt;br&gt;
manuals&lt;br&gt;
practical skills&lt;/p&gt;
&lt;p&gt;through a separate knowledge submission flow.&lt;/p&gt;
&lt;p&gt;But we explicitly did not connect submission directly to rewards.&lt;/p&gt;
&lt;p&gt;A new knowledge item starts as:&lt;/p&gt;
&lt;p&gt;PENDING_REVIEW&lt;/p&gt;
&lt;p&gt;The API response explicitly indicates that creation does not produce:&lt;/p&gt;
&lt;p&gt;reward&lt;br&gt;
ledger write&lt;br&gt;
MYZ transfer&lt;/p&gt;
&lt;p&gt;This separation matters.&lt;/p&gt;
&lt;p&gt;A useful architecture is:&lt;/p&gt;
&lt;p&gt;submission&lt;br&gt;
   ↓&lt;br&gt;
review&lt;br&gt;
   ↓&lt;br&gt;
verification&lt;br&gt;
   ↓&lt;br&gt;
optional reward&lt;/p&gt;
&lt;p&gt;not:&lt;/p&gt;
&lt;p&gt;user submits text&lt;br&gt;
   ↓&lt;br&gt;
money appears&lt;br&gt;
Deployment uncovered another important distinction&lt;/p&gt;
&lt;p&gt;The code was merged through PR #1286.&lt;/p&gt;
&lt;p&gt;Production deployment succeeded on Vercel.&lt;/p&gt;
&lt;p&gt;Marketplace category endpoints respond correctly.&lt;/p&gt;
&lt;p&gt;knowledge is available in production.&lt;/p&gt;
&lt;p&gt;No fatal/runtime errors appeared during our verification window.&lt;/p&gt;
&lt;p&gt;But we are intentionally not saying the MYZ accounting system is fully production-ready yet.&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because the current ledger implementation is file-based.&lt;/p&gt;
&lt;p&gt;And there is a big difference between:&lt;/p&gt;
&lt;p&gt;the application can write a file&lt;/p&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;p&gt;this storage is durable, shared, atomic,&lt;br&gt;
and appropriate for production accounting&lt;/p&gt;
&lt;p&gt;In a serverless environment, local filesystem assumptions deserve special attention.&lt;/p&gt;
&lt;p&gt;A file lock only protects concurrent writers if those writers actually share the same durable filesystem.&lt;/p&gt;
&lt;p&gt;Multiple isolated serverless instances may not.&lt;/p&gt;
&lt;p&gt;So this implementation establishes the accounting contract and business logic, but production MYZ writes still need a properly durable storage architecture before we treat the system as authoritative under distributed load.&lt;/p&gt;
&lt;p&gt;That may mean moving the ledger to a transactional database or placing it behind a dedicated single-writer service.&lt;/p&gt;
&lt;p&gt;We also refused to fake a green CI status&lt;/p&gt;
&lt;p&gt;One final engineering lesson came from CI.&lt;/p&gt;
&lt;p&gt;We created a dedicated GitHub Actions workflow for the MYZ ledger contract.&lt;/p&gt;
&lt;p&gt;It includes service tests, route tests, internal-payment tests, and ledger verification.&lt;/p&gt;
&lt;p&gt;But GitHub did not generate a workflow run.&lt;/p&gt;
&lt;p&gt;We even created a dedicated commit touching the workflow itself:&lt;/p&gt;
&lt;p&gt;33180d233dd6abbb558ed58b56371d77eb11faa2&lt;br&gt;
ci(myz): trigger canonical ledger contract&lt;/p&gt;
&lt;p&gt;Still:&lt;/p&gt;
&lt;p&gt;workflow runs = 0&lt;/p&gt;
&lt;p&gt;Other workflows in the repository have execution history.&lt;/p&gt;
&lt;p&gt;So instead of writing:&lt;/p&gt;
&lt;p&gt;CI ✅&lt;/p&gt;
&lt;p&gt;we documented:&lt;/p&gt;
&lt;p&gt;CI execution issue still open&lt;/p&gt;
&lt;p&gt;It sounds minor, but this is one of the habits that matters most in engineering:&lt;/p&gt;
&lt;p&gt;do not convert “we expected this to run” into “it passed.”&lt;/p&gt;
&lt;p&gt;The architecture now looks much simpler&lt;/p&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;p&gt;Dashboard MYZ&lt;br&gt;
Payment Dashboard MYZ&lt;br&gt;
Marketplace payment logic&lt;br&gt;
Zorgax payment logic&lt;br&gt;
conversion logic&lt;br&gt;
redemption records&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            CANONICAL MYZ LEDGER
                     │
    ┌────────────────┼────────────────┐
    │                │                │
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  Marketplace          Zorgax            P2P&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    │                │
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; buyer → seller     user → system&lt;/p&gt;
&lt;p&gt;Business databases still exist.&lt;/p&gt;
&lt;p&gt;They should.&lt;/p&gt;
&lt;p&gt;But they track business state.&lt;/p&gt;
&lt;p&gt;They no longer get to invent their own spendable MYZ balance.&lt;/p&gt;
&lt;p&gt;What I would improve next&lt;/p&gt;
&lt;p&gt;The implementation is merged and deployed, but the architecture still has clear next steps:&lt;/p&gt;
&lt;p&gt;move the ledger to genuinely durable shared storage;&lt;br&gt;
validate double-spend protection across multiple application instances, not only within one process;&lt;br&gt;
fix the GitHub Actions workflow execution;&lt;br&gt;
add persistent purchase intents for even stronger recovery across client idempotency-key loss;&lt;br&gt;
define explicit migration rules for historical MYZ records;&lt;br&gt;
define verified identity mapping between older contributor:&lt;em&gt; accounts and newer marketplace:user:&lt;/em&gt; accounts.&lt;/p&gt;
&lt;p&gt;None of those invalidate the current work.&lt;/p&gt;
&lt;p&gt;They define the boundary between correct application logic and production-grade distributed accounting infrastructure.&lt;/p&gt;
&lt;p&gt;And knowing that boundary is probably the most useful lesson from this refactor.&lt;/p&gt;
&lt;p&gt;The biggest takeaway for me was simple:&lt;/p&gt;
&lt;p&gt;A balance is a derived value.&lt;br&gt;
A ledger is the history that makes the balance trustworthy.&lt;/p&gt;
&lt;p&gt;Once we started treating MYZ that way, Marketplace payments, Zorgax purchases, legacy cleanup, receipts, idempotency, and concurrency all became parts of the same system instead of separate features.&lt;/p&gt;
&lt;p&gt;MyZubster is open source, and we’re building this architecture in public.&lt;/p&gt;
&lt;p&gt;Repository: MyZubster-Ecosystem/myzubster&lt;br&gt;
Main implementation: PR #1286&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27661/we-rebuilt-myz-payments-around-a-canonical-ledger-heres-what-we-learned</guid>
<pubDate>Fri, 18 Sep 2026 16:46:27 +0000</pubDate>
</item>
<item>
<title>Stop Cascading Failures: Implementing the Circuit Breaker Pattern in Node.js</title>
<link>https://coderlegion.com/27582/stop-cascading-failures-implementing-the-circuit-breaker-pattern-in-node-js</link>
<description>&lt;p&gt;In modern web development, our systems rarely run in isolation. Your Node.js server likely connects to external APIs, databases, authentication providers, and third-party payment gateways. But what happens when one of those external systems slows down or crashes entirely? If your system continues to blindly send traffic to a broken dependency, you risk crashing your own backend. This is where &lt;strong&gt;The Circuit Breaker Pattern&lt;/strong&gt; comes into play.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-circuit-breaker-pattern-&quot;&gt;What is the Circuit Breaker Pattern?&lt;/h4&gt;
&lt;p&gt;The Circuit Breaker pattern is an architectural safety mechanism that monitors outbound requests to external dependencies and halts execution when failure rates cross a certain threshold. Instead of allowing your application to repeatedly attempt an operation that is almost guaranteed to fail, the circuit breaker trips. It quickly returns an error or a fallback response, saving system resources and preventing minor, isolated downtime from turning into a massive, system-wide outage.&lt;/p&gt;
&lt;h4 id=&quot;a-relatable-real-life-analogy&quot;&gt;A Relatable Real-Life Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you are trying to call a local pizza shop to place an order, but their phone line is completely down due to a storm. If you sit there redialing their number every ten seconds, you are wasting your own time, draining your phone's battery, and growing increasingly frustrated. &lt;/p&gt;
&lt;p&gt;A circuit breaker is like having a smart digital assistant. The first few times you try to call, the assistant lets the call go through. But after three failed attempts, the assistant intercepts your future attempts. Instead of dialing, it instantly tells you, &quot;The shop is unreachable right now. I am not going to let you waste your time calling them. Let's try again in ten minutes, or order from another place instead.&quot; It protects your time and energy from being wasted on a guaranteed failure.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-daily-in-the-tech-industry-1&quot;&gt;Why It Matters Daily in the Tech Industry&lt;/h4&gt;
&lt;p&gt;In a production environment, when an external API becomes sluggish or goes offline, incoming HTTP requests to your Express server will start to pile up. Each waiting request holds onto a system thread, a database connection, and a chunk of memory. Within seconds, your server runs out of available resources and crashes.&lt;/p&gt;
&lt;p&gt;Engineers use circuit breakers to prevent these cascading failures. By failing fast, the circuit breaker immediately releases resources back to the server. Your application can then degrade gracefully—perhaps by serving cached data or displaying a polite &quot;service temporarily offline&quot; message—ensuring that the rest of your application remains fast and responsive for your users.&lt;/p&gt;
&lt;h4 id=&quot;code-example-simulating-a-circuit-breaker-in-node-js&quot;&gt;Code Example: Simulating a Circuit Breaker in Node.js&lt;/h4&gt;
&lt;p&gt;Here is a simple, plain Node.js implementation of a circuit breaker class protecting a route in an Express application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const app = express();

class CircuitBreaker {
  constructor(requestFunction, failureThreshold = 3, cooldownPeriod = 10000) {
    this.requestFunction = requestFunction;
    this.failureThreshold = failureThreshold;
    this.cooldownPeriod = cooldownPeriod;
    this.state = 'CLOSED'; // States: CLOSED, OPEN, HALF-OPEN
    this.failureCount = 0;
    this.nextAttemptTime = Date.now();
  }

  async execute(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() &amp;gt; this.nextAttemptTime) {
        this.state = 'HALF-OPEN';
        console.log('[Circuit Breaker] Transitioning to HALF-OPEN. Testing service...');
      } else {
        throw new Error('Circuit is OPEN. External service temporarily offline.');
      }
    }

    try {
      const result = await this.requestFunction(...args);
      this.reset();
      return result;
    } catch (error) {
      this.handleFailure();
      throw error;
    }
  }

  reset() {
    this.state = 'CLOSED';
    this.failureCount = 0;
    console.log('[Circuit Breaker] Success! Circuit reset to CLOSED.');
  }

  handleFailure() {
    this.failureCount++;
    console.warn(`[Circuit Breaker] Failure recorded. Count: ${this.failureCount}`);
    if (this.failureCount &amp;gt;= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttemptTime = Date.now() + this.cooldownPeriod;
      console.error(`[Circuit Breaker] Threshold reached! Circuit is now OPEN for ${this.cooldownPeriod}ms.`);
    }
  }
}

// A mock external API function that always fails
const unstableExternalAPI = async () =&amp;gt; {
  throw new Error('API Timeout');
};

const breaker = new CircuitBreaker(unstableExternalAPI);

app.get('/external-data', async (req, res) =&amp;gt; {
  try {
    const data = await breaker.execute();
    res.json({ success: true, data });
  } catch (error) {
    res.status(503).json({
      success: false,
      message: error.message,
      fallbackData: { items: ['Cached Item 1', 'Cached Item 2'] } // Graceful fallback
    });
  }
});

app.listen(3000, () =&amp;gt; console.log('Server running on port 3000'));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaway-2&quot;&gt;Key Takeaway&lt;/h4&gt;
&lt;p&gt;Designing resilient distributed systems means accepting that third-party dependencies will inevitably fail. Instead of hoping for 100% uptime from external APIs, developers must use the Circuit Breaker pattern to isolate those inevitable failures, shield their own resources, and keep their core application functional.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/how-to-build-unshakable-apis-deep-dive.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27582/stop-cascading-failures-implementing-the-circuit-breaker-pattern-in-node-js</guid>
<pubDate>Fri, 18 Sep 2026 08:33:49 +0000</pubDate>
</item>
<item>
<title>Idempotency in APIs - Why Your Retry Logic Can Break Everything (And How to Fix It)</title>
<link>https://coderlegion.com/26972/idempotency-in-apis-why-your-retry-logic-can-break-everything-and-how-to-fix-it</link>
<description>&lt;p&gt;Modern applications rely heavily on APIs. But networks are unreliable - requests fail, time out, or get retried.&lt;/p&gt;
&lt;p&gt;Now imagine this scenario:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User clicks &lt;strong&gt;“Pay Now”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Request sent to server&lt;/li&gt;
&lt;li&gt;Payment processed successfully&lt;/li&gt;
&lt;li&gt;But response is lost due to network issue&lt;/li&gt;
&lt;li&gt;Client retries the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;💥 Payment gets processed again. User is charged twice.&lt;/p&gt;
&lt;p&gt;This is one of the most dangerous and common problems in distributed systems.&lt;/p&gt;
&lt;p&gt;The solution? &lt;strong&gt;Idempotency.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-is-idempotency-&quot;&gt;What is Idempotency?&lt;/h2&gt;
&lt;p&gt;An operation is &lt;strong&gt;idempotent&lt;/strong&gt; if performing it multiple times produces the same result as performing it once.&lt;/p&gt;
&lt;p&gt;In simple terms:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Same request → same effect → no duplicate side effects&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create Order (without idempotency):
Request sent twice → 2 orders created ❌

Create Order (with idempotency):
Request sent twice → only 1 order created ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Idempotency ensures system correctness even when retries happen.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-do-retries-happen-&quot;&gt;Why Do Retries Happen?&lt;/h2&gt;
&lt;p&gt;Retries are extremely common due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Network timeouts&lt;/li&gt;
&lt;li&gt;Server crashes&lt;/li&gt;
&lt;li&gt;Client retries&lt;/li&gt;
&lt;li&gt;Load balancers retrying requests&lt;/li&gt;
&lt;li&gt;Mobile networks instability&lt;/li&gt;
&lt;li&gt;API gateway retries&lt;/li&gt;
&lt;li&gt;Reverse proxy retries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retries are normal. Duplicate side effects are not.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;which-http-methods-are-idempotent-&quot;&gt;Which HTTP Methods Are Idempotent?&lt;/h2&gt;
&lt;p&gt;According to HTTP specification:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Method &lt;/th&gt;&lt;th&gt;Idempotent &lt;/th&gt;&lt;th&gt;Explanation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;GET    &lt;/td&gt;&lt;td&gt;✅ Yes      &lt;/td&gt;&lt;td&gt;Fetch data&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PUT    &lt;/td&gt;&lt;td&gt;✅ Yes      &lt;/td&gt;&lt;td&gt;Replace resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DELETE &lt;/td&gt;&lt;td&gt;✅ Yes      &lt;/td&gt;&lt;td&gt;Delete resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;POST   &lt;/td&gt;&lt;td&gt;❌ No       &lt;/td&gt;&lt;td&gt;Create resource&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PATCH  &lt;/td&gt;&lt;td&gt;❌ No       &lt;/td&gt;&lt;td&gt;Partial update&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;POST is NOT idempotent by default.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /orders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calling twice → creates 2 orders.&lt;/p&gt;
&lt;p&gt;This must be handled explicitly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-world-example-payment-api-problem&quot;&gt;Real-World Example: Payment API Problem&lt;/h2&gt;
&lt;p&gt;Client sends:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /payments
{
  &quot;user_id&quot;: &quot;123&quot;,
  &quot;amount&quot;: 100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If client retries due to timeout:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /payments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without idempotency → duplicate payment ❌&lt;/p&gt;
&lt;p&gt;With idempotency → single payment ✅&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;solution-idempotency-key&quot;&gt;Solution: Idempotency Key&lt;/h2&gt;
&lt;p&gt;Client sends a unique key with request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /payments
Idempotency-Key: abc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If key is new → process request&lt;/li&gt;
&lt;li&gt;If key already exists → return previous result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guarantees single execution.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-idempotency-works-internally&quot;&gt;How Idempotency Works Internally&lt;/h2&gt;
&lt;p&gt;Flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client generates unique key&lt;/li&gt;
&lt;li&gt;Sends request with key&lt;/li&gt;
&lt;li&gt;Server checks database/cache for key&lt;/li&gt;
&lt;li&gt;If key exists → return stored response&lt;/li&gt;
&lt;li&gt;If not exists → process request&lt;/li&gt;
&lt;li&gt;Store key and response&lt;/li&gt;
&lt;li&gt;Return response&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;go-implementation-example&quot;&gt;Go Implementation Example&lt;/h2&gt;
&lt;p&gt;Let’s implement idempotent payment API using Go.&lt;/p&gt;
&lt;h3 id=&quot;database-table&quot;&gt;Database Table&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;idempotency_keys table:

key (primary key)
response
created_at
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;go-example-basic-version-&quot;&gt;Go Example (Basic Version)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type IdempotencyRecord struct {
    Key      string
    Response string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;handler-implementation&quot;&gt;Handler Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func PaymentHandler(w http.ResponseWriter, r *http.Request) {

    key := r.Header.Get(&quot;Idempotency-Key&quot;)

    if key == &quot;&quot; {
        http.Error(w, &quot;Idempotency-Key required&quot;, http.StatusBadRequest)
        return
    }

    // Check if key exists
    record, exists := getIdempotencyRecord(key)

    if exists {
        w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
        w.Write([]byte(record.Response))
        return
    }

    // Process payment
    paymentID := processPayment()

    response := fmt.Sprintf(`{&quot;payment_id&quot;: &quot;%s&quot;}`, paymentID)

    // Store key and response
    saveIdempotencyRecord(key, response)

    w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
    w.Write([]byte(response))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;example-flow&quot;&gt;Example Flow&lt;/h2&gt;
&lt;p&gt;First request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /payments
Idempotency-Key: abc123

Response:
{
  &quot;payment_id&quot;: &quot;pay_001&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retry request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /payments
Idempotency-Key: abc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response returned from storage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;payment_id&quot;: &quot;pay_001&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No duplicate payment created.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;production-implementation-using-redis-recommended-&quot;&gt;Production Implementation Using Redis (Recommended)&lt;/h2&gt;
&lt;p&gt;Redis is ideal because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extremely fast&lt;/li&gt;
&lt;li&gt;Supports expiration (TTL)&lt;/li&gt;
&lt;li&gt;Perfect for temporary idempotency storage&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;redis-example-in-go&quot;&gt;Redis Example in Go&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func PaymentHandler(w http.ResponseWriter, r *http.Request) {

    key := r.Header.Get(&quot;Idempotency-Key&quot;)

    val, err := redisClient.Get(ctx, key).Result()

    if err == nil {
        w.Write([]byte(val))
        return
    }

    paymentID := processPayment()

    response := fmt.Sprintf(`{&quot;payment_id&quot;: &quot;%s&quot;}`, paymentID)

    redisClient.Set(ctx, key, response, time.Hour*24)

    w.Write([]byte(response))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;important-handle-race-conditions&quot;&gt;Important: Handle Race Conditions&lt;/h2&gt;
&lt;p&gt;Two requests may arrive simultaneously with same key.&lt;/p&gt;
&lt;p&gt;Solution: Use atomic operations.&lt;/p&gt;
&lt;p&gt;Redis example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET key value NX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NX = set only if not exists&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;important-best-practice-store-full-response&quot;&gt;Important Best Practice: Store Full Response&lt;/h2&gt;
&lt;p&gt;Do not just store key.&lt;/p&gt;
&lt;p&gt;Store:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;status code&lt;/li&gt;
&lt;li&gt;response body&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because retry must return exact same response.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;example-database-schema-postgresql-&quot;&gt;Example Database Schema (PostgreSQL)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE idempotency_keys (
    key TEXT PRIMARY KEY,
    response JSONB,
    status_code INT,
    created_at TIMESTAMP DEFAULT NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Primary key ensures uniqueness.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-world-systems-that-use-idempotency&quot;&gt;Real-World Systems That Use Idempotency&lt;/h2&gt;
&lt;p&gt;Critical systems use idempotency extensively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Payment APIs&lt;/li&gt;
&lt;li&gt;Order creation APIs&lt;/li&gt;
&lt;li&gt;Financial transactions&lt;/li&gt;
&lt;li&gt;Booking systems&lt;/li&gt;
&lt;li&gt;Wallet transfers&lt;/li&gt;
&lt;li&gt;Stripe payments&lt;/li&gt;
&lt;li&gt;Banking APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without idempotency, financial systems would break.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;common-mistake-developers-make&quot;&gt;Common Mistake Developers Make&lt;/h2&gt;
&lt;p&gt;❌ Using retry without idempotency&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;retry 3 times on failure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This multiplies side effects.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Retry logic: 3 times
Result: 3 payments
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dangerous bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency-vs-duplicate-prevention-using-unique-constraints&quot;&gt;Idempotency vs Duplicate Prevention Using Unique Constraints&lt;/h2&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UNIQUE(user_id, order_id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents duplicates at DB level.&lt;/p&gt;
&lt;p&gt;But idempotency is better because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Works at API level&lt;/li&gt;
&lt;li&gt;Handles retries cleanly&lt;/li&gt;
&lt;li&gt;Returns same response&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Best approach: use BOTH.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-should-you-use-idempotency-&quot;&gt;When Should You Use Idempotency?&lt;/h2&gt;
&lt;p&gt;Use idempotency for operations that create side effects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Payment creation&lt;/li&gt;
&lt;li&gt;Order creation&lt;/li&gt;
&lt;li&gt;Wallet transfer&lt;/li&gt;
&lt;li&gt;Booking&lt;/li&gt;
&lt;li&gt;Resource creation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not required for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GET requests&lt;/li&gt;
&lt;li&gt;Read-only operations&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-client-should-generate-idempotency-key&quot;&gt;How Client Should Generate Idempotency Key&lt;/h2&gt;
&lt;p&gt;Use UUID:&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;uuid.New().String()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unique per operation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency-key-expiration-strategy&quot;&gt;Idempotency Key Expiration Strategy&lt;/h2&gt;
&lt;p&gt;Keys should expire after some time.&lt;/p&gt;
&lt;p&gt;Common TTL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;24 hours&lt;/li&gt;
&lt;li&gt;48 hours&lt;/li&gt;
&lt;li&gt;7 days (depending on business need)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis makes expiration easy.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;example-architecture&quot;&gt;Example Architecture&lt;/h2&gt;
&lt;p&gt;Client → API Gateway → Service → Redis → Database&lt;/p&gt;
&lt;p&gt;Redis handles idempotency keys&lt;br&gt;
Database handles actual data&lt;/p&gt;
&lt;p&gt;Fast and reliable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-production-scenario-mobile-network-retry&quot;&gt;Real Production Scenario: Mobile Network Retry&lt;/h2&gt;
&lt;p&gt;Mobile user taps &quot;Place Order&quot;&lt;/p&gt;
&lt;p&gt;Network slow → timeout&lt;br&gt;
Client retries automatically&lt;/p&gt;
&lt;p&gt;Without idempotency → duplicate orders&lt;/p&gt;
&lt;p&gt;With idempotency → safe retry&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;important-difference-safe-retry-vs-unsafe-retry&quot;&gt;Important Difference: Safe Retry vs Unsafe Retry&lt;/h2&gt;
&lt;p&gt;Unsafe retry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /orders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Safe retry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /orders
Idempotency-Key: unique-key
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency-vs-exactly-once-execution&quot;&gt;Idempotency vs Exactly-Once Execution&lt;/h2&gt;
&lt;p&gt;Exactly-once execution is impossible in distributed systems.&lt;/p&gt;
&lt;p&gt;Idempotency provides practical solution.&lt;/p&gt;
&lt;p&gt;It ensures same final state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;best-practices-checklist&quot;&gt;Best Practices Checklist&lt;/h2&gt;
&lt;p&gt;Always:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use idempotency keys for POST APIs&lt;/li&gt;
&lt;li&gt;Store keys in Redis or database&lt;/li&gt;
&lt;li&gt;Store full response&lt;/li&gt;
&lt;li&gt;Use expiration&lt;/li&gt;
&lt;li&gt;Handle race conditions&lt;/li&gt;
&lt;li&gt;Use UUID keys&lt;/li&gt;
&lt;li&gt;Combine with database constraints&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;key-takeaway-3&quot;&gt;Key Takeaway&lt;/h2&gt;
&lt;p&gt;Retries are normal.&lt;/p&gt;
&lt;p&gt;Duplicate side effects are dangerous.&lt;/p&gt;
&lt;p&gt;Idempotency ensures your APIs remain safe, reliable, and production-ready.&lt;/p&gt;
&lt;p&gt;It is not optional for critical APIs - it is essential.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h2&gt;
&lt;p&gt;If your API handles payments, orders, bookings, or financial transactions - idempotency is mandatory.&lt;/p&gt;
&lt;p&gt;Without it, retries can silently corrupt your system.&lt;/p&gt;
&lt;p&gt;With it, your APIs become reliable, predictable, and safe.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26972/idempotency-in-apis-why-your-retry-logic-can-break-everything-and-how-to-fix-it</guid>
<pubDate>Sat, 12 Sep 2026 13:30:46 +0000</pubDate>
</item>
<item>
<title>Stop Recreating Database Connections: An Illustrated Guide to Connection Pooling</title>
<link>https://coderlegion.com/25256/stop-recreating-database-connections-an-illustrated-guide-to-connection-pooling</link>
<description>&lt;p&gt;If you have ever built an application that talks to a database, you have used a database connection. A database is simply a structured storage system for digital information, and a connection is the secure, active pipeline established between your application's code and that database. &lt;/p&gt;
&lt;p&gt;However, constantly opening and closing these connections for every single user request is incredibly slow and resource-heavy. To solve this, developers use a technique called &lt;strong&gt;connection pooling&lt;/strong&gt;. Connection pooling is a performance optimization method where a group (or &quot;pool&quot;) of database connections is kept open and idle, waiting to be reused by whoever needs them, rather than being destroyed and recreated on demand.&lt;/p&gt;
&lt;h4 id=&quot;the-analogy-the-pizza-delivery-fleet&quot;&gt;The Analogy: The Pizza Delivery Fleet&lt;/h4&gt;
&lt;p&gt;To understand why connection pooling is so vital, imagine you run a busy, high-volume pizza restaurant. Every time an order comes in, you need to deliver it to a customer. &lt;/p&gt;
&lt;p&gt;Imagine if, for every single delivery, you had to hire a brand-new driver, run a background check, sign a legal employment contract, set up their vehicle registration, and purchase a GPS. Once that single delivery was complete, you immediately fired them, canceled their insurance, and sold the car. It sounds absurd, right? The administrative overhead of hiring and firing would take three times longer than the actual delivery, and your restaurant would go bankrupt in a week.&lt;/p&gt;
&lt;p&gt;Instead, you hire a permanent, dedicated fleet of five delivery drivers. They sit in the break room, fully certified and ready. When a pizza is boxed, the first available driver grabs the box, drives to the house, delivers it, and returns to wait in the break room for the next order. &lt;/p&gt;
&lt;p&gt;In this analogy, the drivers are the &quot;connection pool,&quot; the deliveries are your database queries (requests for information), and the hiring process is the network handshake required to open a new database connection.&lt;/p&gt;
&lt;h4 id=&quot;why-it-matters-in-daily-software-engineering-1&quot;&gt;Why It Matters in Daily Software Engineering&lt;/h4&gt;
&lt;p&gt;In real-world software, opening a database connection is highly expensive in terms of computing power. The application must perform a security handshake, authenticate its username and password, allocate memory on the database server, and establish a network route. This process can easily take 50 to 100 milliseconds.&lt;/p&gt;
&lt;p&gt;If your website experiences a surge in traffic—say, 1,000 users clicking a button at the exact same moment—your server would try to open 1,000 separate connections simultaneously. The database server would quickly run out of memory, slow down to a crawl, and eventually crash, displaying a &quot;database connection error&quot; to your users.&lt;/p&gt;
&lt;p&gt;Connection pooling prevents this complete system collapse. It sets a strict limit on the maximum number of connections allowed (e.g., capping it at 20). If 1,000 users arrive, they politely share those 20 open connections. Because each connection is already established, queries execute in 1 to 2 milliseconds. Once a query is done, the connection is instantly recycled for the next user in line, protecting your database from crashing.&lt;/p&gt;
&lt;h4 id=&quot;a-simple-code-example-in-node-js&quot;&gt;A Simple Code Example in Node.js&lt;/h4&gt;
&lt;p&gt;Below is a simple JavaScript example using a popular PostgreSQL database driver. It demonstrates how we initialize a pool and query the database without manually opening and closing connections.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { Pool } = require('pg');

// 1. Create a pool with a maximum capacity of 10 connections
const pool = new Pool({
  user: 'db_user',
  host: 'database.server.com',
  database: 'mydb',
  password: 'securepassword',
  port: 5432,
  max: 10, // Maximum connections in the pool
});

async function fetchUserData(userId) {
  // 2. Grab an already-open connection from the pool
  const client = await pool.connect();
  
  try {
    // 3. Execute the database query
    const result = await client.query('SELECT * FROM users WHERE id = $1', [userId]);
    return result.rows[0];
  } catch (err) {
    console.error('Database query error:', err);
  } finally {
    // 4. Crucial: Release the connection back to the pool
    // This does NOT close the connection; it just makes it available for others!
    client.release();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-3&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;Connection pooling turns a heavy, repetitive administrative chore into a fast, shared utility. By keeping a smart, limited buffer of active connections ready to work, software systems can handle tens of thousands of requests smoothly, ensuring high-speed page loads while protecting underlying servers from sudden traffic spikes.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;resources&quot;&gt;Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;react-hook-lab:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect with me on LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;Saurav Pandey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/08/how-connection-pooling-speeds-up-your.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/saurav_tb_pandey/stop-recreating-database-connections-an-illustrated-guide-to-connection-pooling-4i64&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25256/stop-recreating-database-connections-an-illustrated-guide-to-connection-pooling</guid>
<pubDate>Fri, 11 Sep 2026 02:43:51 +0000</pubDate>
</item>
<item>
<title>Building a Real-Time Dashboard with FastAPI, WebSockets, and MySQL</title>
<link>https://coderlegion.com/26578/building-a-real-time-dashboard-with-fastapi-websockets-and-mysql</link>
<description>&lt;p&gt;I recently worked on a lightweight real-time dashboard built with &lt;strong&gt;FastAPI&lt;/strong&gt;, &lt;strong&gt;WebSockets&lt;/strong&gt;, and &lt;strong&gt;aiomysql&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The main goal was simple: retrieve live data from a MySQL database and push updates to the frontend in real time without relying on continuous polling.&lt;/p&gt;
&lt;p&gt;This project provided a practical opportunity to explore asynchronous programming, WebSocket communication, and database integration using Python.&lt;/p&gt;
&lt;h3 id=&quot;project-overview&quot;&gt;Project Overview&lt;/h3&gt;
&lt;p&gt;The application uses a &lt;strong&gt;FastAPI backend&lt;/strong&gt; to communicate with a MySQL database and a &lt;strong&gt;WebSocket endpoint&lt;/strong&gt; to deliver database updates to connected clients.&lt;/p&gt;
&lt;p&gt;The overall architecture can be summarized as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MySQL Database
      │
      ▼
   aiomysql
      │
      ▼
FastAPI Backend
      │
      ▼
  WebSocket
      │
      ▼
Frontend Dashboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The WebSocket connection allows the server to push new data directly to the client, making the dashboard capable of displaying updates in real time.&lt;/p&gt;
&lt;h3 id=&quot;what-this-project-demonstrates&quot;&gt;What This Project Demonstrates&lt;/h3&gt;
&lt;p&gt;The project covers several important backend development concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting up Python and the required dependencies&lt;/li&gt;
&lt;li&gt;Building an asynchronous FastAPI backend&lt;/li&gt;
&lt;li&gt;Connecting to MySQL using &lt;code&gt;aiomysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Implementing a WebSocket endpoint&lt;/li&gt;
&lt;li&gt;Retrieving database records asynchronously&lt;/li&gt;
&lt;li&gt;Sending live data to connected clients&lt;/li&gt;
&lt;li&gt;Running the application with Uvicorn&lt;/li&gt;
&lt;li&gt;Structuring a simple real-time backend application&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-use-websockets-&quot;&gt;Why Use WebSockets?&lt;/h3&gt;
&lt;p&gt;Traditional REST APIs generally follow a request/response model.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client → Request → Server
Client ← Response ← Server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the frontend needs updated information continuously, it may have to repeatedly send requests to the server.&lt;/p&gt;
&lt;p&gt;This is commonly known as &lt;strong&gt;polling&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client → Request
Server → Response

Client → Request
Server → Response

Client → Request
Server → Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With WebSockets, the client establishes a persistent connection with the server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client ⇄ WebSocket Connection ⇄ Server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server can then push new information to the client whenever it becomes available.&lt;/p&gt;
&lt;p&gt;This makes WebSockets particularly useful for applications where data needs to be delivered quickly and continuously.&lt;/p&gt;
&lt;h3 id=&quot;potential-use-cases&quot;&gt;Potential Use Cases&lt;/h3&gt;
&lt;p&gt;This architecture can be useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time dashboards&lt;/li&gt;
&lt;li&gt;Monitoring systems&lt;/li&gt;
&lt;li&gt;Live administration panels&lt;/li&gt;
&lt;li&gt;Student management systems&lt;/li&gt;
&lt;li&gt;Record management applications&lt;/li&gt;
&lt;li&gt;System status monitoring&lt;/li&gt;
&lt;li&gt;Live data visualization&lt;/li&gt;
&lt;li&gt;Notification systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;documentation-and-source-code&quot;&gt;Documentation and Source Code&lt;/h3&gt;
&lt;p&gt;I created a complete guide covering the setup and implementation process, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python installation&lt;/li&gt;
&lt;li&gt;Required package installation&lt;/li&gt;
&lt;li&gt;Project configuration&lt;/li&gt;
&lt;li&gt;Server startup&lt;/li&gt;
&lt;li&gt;Complete &lt;code&gt;main.py&lt;/code&gt; implementation&lt;/li&gt;
&lt;li&gt;FastAPI and WebSocket configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GitHub repository also includes the documentation and source code for reference.&lt;/p&gt;
&lt;h4 id=&quot;github-repository&quot;&gt;GitHub Repository&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/NithilaLD/fastapi-websocket-realtime-dashboard&quot;&gt;FastAPI WebSocket Real-Time Dashboard - GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The repository contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complete Guide (PDF)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source Code (&lt;code&gt;main.py&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;key-technologies&quot;&gt;Key Technologies&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Technology &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Python     &lt;/td&gt;&lt;td&gt;Backend programming&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;FastAPI    &lt;/td&gt;&lt;td&gt;Asynchronous web framework&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;WebSockets &lt;/td&gt;&lt;td&gt;Real-time client-server communication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;aiomysql   &lt;/td&gt;&lt;td&gt;Asynchronous MySQL connectivity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MySQL      &lt;/td&gt;&lt;td&gt;Database&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Uvicorn    &lt;/td&gt;&lt;td&gt;ASGI server&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;final-thoughts-3&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This project was a useful exercise in combining &lt;strong&gt;asynchronous programming, database connectivity, and real-time communication&lt;/strong&gt; into a practical backend application.&lt;/p&gt;
&lt;p&gt;FastAPI and WebSockets provide a straightforward foundation for building applications where data needs to move between the server and client with minimal delay.&lt;/p&gt;
&lt;p&gt;I look forward to extending this project with features such as authentication, multiple connected clients, automatic database change detection, improved error handling, and a more advanced frontend dashboard.&lt;/p&gt;
&lt;p&gt;If you are working with FastAPI or real-time applications, I'd be interested to hear about your projects and approaches.&lt;/p&gt;
&lt;h3 id=&quot;also-published-on&quot;&gt;Also Published On&lt;/h3&gt;
&lt;p&gt;This article is also available on the following platforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/pulse/building-real-time-fastapi-dashboard-websockets-async-liyanarachchi-nvf7c/&quot;&gt;Read on LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dulannithilaliyanarachchi.medium.com/building-a-real-time-fastapi-dashboard-with-websockets-and-async-mysql-344d86522a0e&quot;&gt;Read on Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://builder.aws.com/content/3Ij3xiZllF1UvwYpk5OCYx52UfK/building-a-real-time-fastapi-dashboard-with-websockets-and-async-mysql&quot;&gt;Read on AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DEV Community:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/dulannithilaliyanarachchi/building-a-real-time-dashboard-with-fastapi-websockets-and-mysql-1494&quot;&gt;Read on DEV Community&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26578/building-a-real-time-dashboard-with-fastapi-websockets-and-mysql</guid>
<pubDate>Wed, 09 Sep 2026 08:42:41 +0000</pubDate>
</item>
<item>
<title>Demystifying ACID Transactions: How Databases Prevent Chaos and Keep Your Data Safe</title>
<link>https://coderlegion.com/26471/demystifying-acid-transactions-how-databases-prevent-chaos-and-keep-your-data-safe</link>
<description>&lt;p&gt;If you have ever used an online app to transfer money, buy a concert ticket, or update your profile, you have relied on a database. Behind the scenes, databases must process millions of instructions without making mistakes, even if the power suddenly cuts out or the server crashes. To guarantee this level of safety, databases rely on a concept called an ACID transaction, which is a set of four rules designed to keep your data perfectly accurate, clean, and trustworthy.&lt;/p&gt;
&lt;h4 id=&quot;the-analogy-the-vending-machine&quot;&gt;The Analogy: The Vending Machine&lt;/h4&gt;
&lt;p&gt;To understand how an ACID transaction works, imagine buying a bag of chips from a vending machine. This simple action relies on several rules to make sure nobody gets cheated:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Atomicity (All-or-Nothing):&lt;/strong&gt; You enter your money and press the button. Either you get your chips and the machine keeps your money, or the machine jams, returns your cash, and keeps the chips. You will never experience a state where the machine keeps your money but does not give you the chips.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency (Rule Following):&lt;/strong&gt; The machine must always balance its books. If a bag of chips costs two dollars, the machine's internal tally must show exactly two dollars more in cash and one fewer bag of chips in inventory. It cannot end up with a random, unexplainable state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolation (Privacy):&lt;/strong&gt; If two people press buttons on the vending machine at the exact same moment, the machine processes one transaction first, finishes it completely, and then processes the second. The two orders do not get mixed up, and you do not end up getting each other's snacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durability (Permanence):&lt;/strong&gt; Once your chips drop into the retrieval slot, they are yours. If the power outlet is pulled from the wall a millisecond later, the chips do not magically fly back up into the machine. The physical transfer is permanent.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;why-it-matters-in-tech&quot;&gt;Why It Matters in Tech&lt;/h4&gt;
&lt;p&gt;Without ACID transactions, software development would be an absolute nightmare. Imagine writing code for a banking system without these guarantees. If a customer transfers fifty dollars from Account A to Account B, the system must perform two database updates: subtract fifty from Account A, and add fifty to Account B.&lt;/p&gt;
&lt;p&gt;If the server crashes exactly halfway through this process, fifty dollars could simply vanish into thin air. Engineers use ACID transactions to bundle these two updates into a single, bulletproof package. If any part of the process fails, the database automatically rolls back to the beginning, behaving as if the transfer never started, protecting the user's money and the business's reputation.&lt;/p&gt;
&lt;h4 id=&quot;seeing-it-in-action&quot;&gt;Seeing It in Action&lt;/h4&gt;
&lt;p&gt;Here is a simple example in JavaScript using a hypothetical database library. It shows how we bundle multiple database operations into a single, safe transaction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function transferMoney(senderId, receiverId, amount) {
  // 1. Start the transaction block
  await database.query(&quot;BEGIN TRANSACTION&quot;);

  try {
    // 2. Subtract the amount from the sender
    await database.query(
      &quot;UPDATE accounts SET balance = balance - $1 WHERE id = $2&quot;, 
      [amount, senderId]
    );

    // 3. Add the amount to the receiver
    await database.query(
      &quot;UPDATE accounts SET balance = balance + $1 WHERE id = $2&quot;, 
      [amount, receiverId]
    );

    // 4. If both succeeded, lock in the changes permanently
    await database.query(&quot;COMMIT&quot;);
    console.log(&quot;Transfer completed successfully!&quot;);
  } catch (error) {
    // 5. If ANY error occurred, cancel and reverse all changes
    await database.query(&quot;ROLLBACK&quot;);
    console.error(&quot;Transfer failed. System rolled back to safety:&quot;, error);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-takeaway-4&quot;&gt;The Takeaway&lt;/h4&gt;
&lt;p&gt;ACID transactions are the unsung heroes of software reliability, transforming chaotic and unpredictable environments into predictable, safe spaces. By ensuring that database operations either succeed completely or fail harmlessly without a trace, they give developers the confidence to build robust systems that users can trust with their most sensitive financial, personal, and operational data.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;resources-1&quot;&gt;Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Saurav-TB-Pandey/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;react-hook-lab:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect with me on LinkedIn:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/pandeysaurav/&quot;&gt;Saurav Pandey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/09/the-guardian-of-data-integrity-why-your.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26471/demystifying-acid-transactions-how-databases-prevent-chaos-and-keep-your-data-safe</guid>
<pubDate>Tue, 08 Sep 2026 08:20:04 +0000</pubDate>
</item>
</channel>
</rss>