<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmad Fauzan Alghifari</title>
    <description>The latest articles on DEV Community by Ahmad Fauzan Alghifari (@fazghfr).</description>
    <link>https://dev.to/fazghfr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3877662%2F0534252a-653d-417d-9e10-e1ce5a6194b2.jpeg</url>
      <title>DEV Community: Ahmad Fauzan Alghifari</title>
      <link>https://dev.to/fazghfr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fazghfr"/>
    <language>en</language>
    <item>
      <title>How I Built a Job Tracker Backend with a Concurrent Job Queue in Go</title>
      <dc:creator>Ahmad Fauzan Alghifari</dc:creator>
      <pubDate>Mon, 18 May 2026 04:25:03 +0000</pubDate>
      <link>https://dev.to/fazghfr/how-i-built-a-job-tracker-backend-with-a-concurrent-job-queue-in-go-mna</link>
      <guid>https://dev.to/fazghfr/how-i-built-a-job-tracker-backend-with-a-concurrent-job-queue-in-go-mna</guid>
      <description>&lt;h2&gt;
  
  
  Building a Job Application Tracker with AI-Powered Resume Analysis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Ahmad Fauzan Alghifari&lt;br&gt;
&lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/fazghfr/jatify-backend" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Job hunting is exhausting. Keeping track of where you applied, what stage each application is at, and whether your resume is even a good — it's a lot to manage manually.&lt;/p&gt;

&lt;p&gt;So I built a tool for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jatify&lt;/strong&gt; is a job application tracker with a backend built in Go, backed by PostgreSQL, and powered by AI resume analysis through OpenRouter. The idea is simple: you upload your resume, add job listings you're targeting, and the system analyzes how well your resume is — all asynchronously, so you're never waiting around for a response.&lt;/p&gt;

&lt;p&gt;This post covers the backend architecture: how the layers are structured, how the job queue works, and a full breakdown of every available API endpoint. The whole thing is deployed on a Linux VPS, containerized with Docker, and ships automatically via GitHub Actions CI/CD — so every push to main goes straight to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Project Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project Title:&lt;/strong&gt; Job Application Tracker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objective:&lt;/strong&gt; Implement a system to track job applications, with AI-powered resume analysis as the primary additional feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Summary:&lt;/strong&gt; Users interact directly with a frontend application that builds and sends requests to a REST-based backend.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. System Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Service Decomposition
&lt;/h3&gt;

&lt;p&gt;The backend is structured into distinct layers, each with a clear responsibility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Handler Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Processes incoming HTTP requests from the frontend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contains the core business logic for each feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interacts with data entities such as the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines the data entities used in the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Job Queue / Processing&lt;/strong&gt;*&lt;/td&gt;
&lt;td&gt;Facilitates the job queue system and concurrent processing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language / Framework:&lt;/strong&gt; Go (Gin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory / Queue:&lt;/strong&gt; PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Linux VPS, Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database Design (Job Tracker Features)
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Database Design (Job Queue Feature)
&lt;/h3&gt;

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




&lt;h2&gt;
  
  
  3. Job Queue Workflows
&lt;/h2&gt;

&lt;p&gt;The Job Queue is used here because the AI resume analysis feature is implemented via an external endpoint (OpenRouter). Since the response from OpenRouter cannot be predicted by the system, the analysis process may have variable response times, experience failures, or time out.&lt;/p&gt;

&lt;p&gt;Data within the job queue system is stored across two entities.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Concurrency
&lt;/h3&gt;

&lt;p&gt;Queue processing is handled by &lt;strong&gt;N concurrent workers (goroutines)&lt;/strong&gt;. Each worker waits to be "woken up". When woken, a worker searches for jobs with a &lt;code&gt;pending&lt;/code&gt; status to process.&lt;/p&gt;

&lt;p&gt;Workers are woken up under two conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new job is added to the queue&lt;/li&gt;
&lt;li&gt;After a set timeout period&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each time a worker is woken, it picks the first available pending job — ensuring no job is skipped or given priority over another.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Enqueue
&lt;/h3&gt;

&lt;p&gt;The following is a high-level visualization of the process for adding jobs to the queue.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3.3 Requeue
&lt;/h3&gt;

&lt;p&gt;The following is a high-level visualization of the process for re-adding failed jobs back into the queue.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  4. Available API Endpoints
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/auth/register&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Register a new user account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/auth/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Log in and receive a JWT token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/auth/logout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Log out of the active session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  User
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/user/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve the profile data of the currently logged-in user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Application Statuses
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/statuses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve a list of all available application statuses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Job Applications
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new job application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve all applications belonging to the current user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve the details of a single application by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update application data (including status changes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete an application (soft delete)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Job Listings
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new job listing entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve all job listings belonging to the current user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/jobs/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve the details of a single job listing by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/jobs/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update job listing data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/jobs/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a job listing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Resumes
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upload a new resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve all resumes belonging to the current user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve the details of a single resume by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update resume data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a resume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Resume Analysis
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id/analyze&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initiate an AI analysis of a resume against a specific job listing (queued)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id/analysis/:jobid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve the analysis result for a specific resume and job listing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/resumes/:id/analyses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve all analysis results for a single resume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Job Queue System — Dead Letter Queue (DLQ)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/dlq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retrieve a list of all jobs that failed processing in the Dead Letter Queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/dlq/requeue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Re-queue multiple failed jobs at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/dlq/:uuid/requeue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Re-queue a single failed job by UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/dlq/:uuid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permanently delete a single job from the Dead Letter Queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>ai</category>
      <category>restapi</category>
    </item>
    <item>
      <title>The latest() Bug That Silently Duplicated Transaction IDs in Production</title>
      <dc:creator>Ahmad Fauzan Alghifari</dc:creator>
      <pubDate>Mon, 20 Apr 2026 02:05:56 +0000</pubDate>
      <link>https://dev.to/fazghfr/the-latest-bug-that-silently-duplicated-transaction-ids-in-production-42ip</link>
      <guid>https://dev.to/fazghfr/the-latest-bug-that-silently-duplicated-transaction-ids-in-production-42ip</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Using &lt;code&gt;Model::latest()-&amp;gt;first()&lt;/code&gt; to get the "latest" record by ID is wrong. &lt;code&gt;latest()&lt;/code&gt; orders by &lt;code&gt;created_at&lt;/code&gt;, not by the value of your ID column. When two rows are inserted within the same second, both reads return the same record, and you get duplicate IDs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I built a simple sequential ID generator for transaction IDs. The logic was straightforward: fetch the latest record, read its code, increment the number, return the next one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XX00000001"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$latest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$latest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$latest&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^XX\d{8}$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"XX"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;str_pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;STR_PAD_LEFT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. Passed local and dev branch testing. Got merged into production with no issues. Then one day, a user reported duplicate IDs on different transactions.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What Happened?
&lt;/h2&gt;

&lt;p&gt;My assumption was that &lt;code&gt;latest()&lt;/code&gt; would always return the record with the highest ID. Logically, the latest record should have the highest code, right?&lt;/p&gt;

&lt;p&gt;Wrong. &lt;code&gt;latest()&lt;/code&gt; in Laravel is just a shorthand for &lt;code&gt;orderBy('created_at', 'desc')&lt;/code&gt;. It doesn't care about your ID column, it orders by &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means the correctness of the whole function depends entirely on &lt;code&gt;created_at&lt;/code&gt; being precise enough to differentiate between records. And it wasn't.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;created_at&lt;/code&gt; column was storing timestamps in &lt;code&gt;DD:MM:YY hh:mm:ss&lt;/code&gt; format. The smallest unit is &lt;strong&gt;seconds&lt;/strong&gt;. If two rows are inserted within the same second — whether by two users simultaneously, or by a backend loop calling this function in rapid succession, both calls to &lt;code&gt;latest()-&amp;gt;first()&lt;/code&gt; return the same record. Both read the same code. Both return the same next ID.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;created_at&lt;/th&gt;
&lt;th&gt;ID generated&lt;/th&gt;
&lt;th&gt;note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20:04:01&lt;/td&gt;
&lt;td&gt;XX00000001&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20:04:01&lt;/td&gt;
&lt;td&gt;XX00000002&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20:04:01&lt;/td&gt;
&lt;td&gt;XX00000002&lt;/td&gt;
&lt;td&gt;DUPLICATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20:04:01&lt;/td&gt;
&lt;td&gt;XX00000002&lt;/td&gt;
&lt;td&gt;DUPLICATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20:04:02&lt;/td&gt;
&lt;td&gt;XX00000003&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20:04:02&lt;/td&gt;
&lt;td&gt;XX00000003&lt;/td&gt;
&lt;td&gt;DUPLICATE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bug wasn't in the increment logic. It was in the assumption that "latest by time" equals "latest by value."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Instead of relying on &lt;code&gt;created_at&lt;/code&gt; ordering, query for the actual maximum code value directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XX00000001"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$maxValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LIKE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'XX%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$maxValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$maxValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"XX"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;str_pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;STR_PAD_LEFT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max('code')&lt;/code&gt; operates on the actual data value, not on metadata like &lt;code&gt;created_at&lt;/code&gt;. Regardless of when a record was inserted, this always returns the highest code in the table, which is exactly what we need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing worth noting:&lt;/strong&gt; this fix doesn't eliminate the race condition entirely. Two requests can still call this function simultaneously, both read the same &lt;code&gt;max&lt;/code&gt; before either one writes. For a truly bulletproof solution, you'd want a database-level lock (&lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;), a &lt;code&gt;UNIQUE&lt;/code&gt; constraint on the column to let the DB reject duplicates, or just use &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; and format on read. The &lt;code&gt;max()&lt;/code&gt; fix is a significant improvement, but it's not atomic. &lt;/p&gt;




&lt;h2&gt;
  
  
  What I Should Have Done
&lt;/h2&gt;

&lt;p&gt;Looking back, there were two things that would have caught this before it hit production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write automated tests that simulate concurrent calls.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bug was triggered by calling this function inside a loop — something that can't be reproduced through the UI or by a non-technical tester. A simple test that calls the function in a loop ten times and asserts all returned IDs are unique would have caught this immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Look at the database earlier.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent too long staring at the code when the code itself was innocent. The real issue was in the data structure — specifically, the precision of the &lt;code&gt;created_at&lt;/code&gt; column. Checking the actual column type and sample data earlier would have pointed me in the right direction much faster.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The most frustrating bugs are the ones where the code does exactly what you told it to. The problem is what you assumed it would do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>timestamp</category>
      <category>laravel</category>
      <category>backend</category>
      <category>sql</category>
    </item>
    <item>
      <title>How I Fixed a Correlated Subquery That Was Behaving Like an N+1 Problem in Production</title>
      <dc:creator>Ahmad Fauzan Alghifari</dc:creator>
      <pubDate>Tue, 14 Apr 2026 07:36:39 +0000</pubDate>
      <link>https://dev.to/fazghfr/how-i-fixed-a-correlated-subquery-that-was-behaving-like-an-n1-problem-in-production-abn</link>
      <guid>https://dev.to/fazghfr/how-i-fixed-a-correlated-subquery-that-was-behaving-like-an-n1-problem-in-production-abn</guid>
      <description>&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;A user reported seeing no data after an absurdly long loading time in one of the procurement menus. I was maintaining an e-procurement system for a state-owned enterprise subsidiary in the mining sector and this wasn't just a UI glitch. It completely blocked the user's business process. &lt;/p&gt;

&lt;p&gt;From the network tab, the problematic request was just sitting there with endless pending status, until it hit the server's timeout and resulted in 504, effectively sending no data to the user.&lt;/p&gt;

&lt;p&gt;The culprit turned out to be a correlated subquery behaving exactly like an N+1 problem: common, easy to miss, and quietly fatal in production. In this post, I'll walk through how I identified the root cause and fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an N+1 Query Problem?
&lt;/h2&gt;

&lt;p&gt;Imagine you have to fetch 500 data from the database. And then, for each of that row, you run another query to compute something like aggregations. That's 1 (the initial fetching) query plus 500 query for each computation. In production, because the data will keep growing, this would be a fatal problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CLASSIC ORM N+1 EXAMPLE&lt;/span&gt;
&lt;span class="c1"&gt;// 1 query&lt;/span&gt;
&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// N queries — one per order&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hits the database every single iteration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This commonly happens when we use ORM loops like in laravel. But N+1 problem does not happen only to ORMs. Something similar could also happen to the database query itself. Which is exactly what happened in my case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happened
&lt;/h2&gt;

&lt;p&gt;This is a snippet of a long scope code that was implemented with laravel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="no"&gt;WHEN&lt;/span&gt; &lt;span class="nb"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;po_items&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nf"&gt;THEN&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="nf"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;
     &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;po_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="no"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sum_val&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you see the problem? Yup. Its computing an aggregation function SUM() within the select statement. So for each data that comes from the &lt;code&gt;FROM&lt;/code&gt; keyword, let's assume we have 1000 rows, it will run that exact SUM() query for another 1000 times. This will work on small data, but in production environment and database, this query won't survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;leftjoin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;po_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sum_val&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'y'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;po_id&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;po_item_aggs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;po_item_aggs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;po_id&lt;/span&gt;

&lt;span class="c1"&gt;-- accessing it directly&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;po_item_aggs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum_val&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what I did. Instead of doing the aggregations and the subquery inside the select statement, I moved it into another Join operations. This alone removes the computation for each row from the data source. So instead of doing 1000+ query computation, we only did one query for the computation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqci5m53juvglfp2fu37w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqci5m53juvglfp2fu37w.png" alt="Before fix — request pending, 504 timeout" width="418" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response time&lt;/td&gt;
&lt;td&gt;~2 minutes / timeout&lt;/td&gt;
&lt;td&gt;~15 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP status&lt;/td&gt;
&lt;td&gt;504 Gateway Timeout&lt;/td&gt;
&lt;td&gt;200 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User experience&lt;/td&gt;
&lt;td&gt;Blank table, blocked workflow&lt;/td&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The request that was hanging for over two minutes — or not returning at all — now resolves in around 15 seconds. That's not just a performance improvement, it's the difference between a system that works and one that doesn't.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>backend</category>
      <category>sql</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
