<?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: Sabir</title>
    <description>The latest articles on DEV Community by Sabir (@sabirpm).</description>
    <link>https://dev.to/sabirpm</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%2F3950985%2F4fe8b049-cdc2-4ab3-95f4-8809debeef6c.jpg</url>
      <title>DEV Community: Sabir</title>
      <link>https://dev.to/sabirpm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sabirpm"/>
    <language>en</language>
    <item>
      <title>Building TaskForge: Translating Enterprise Chaos into an Open-Source Scheduler</title>
      <dc:creator>Sabir</dc:creator>
      <pubDate>Mon, 25 May 2026 16:17:19 +0000</pubDate>
      <link>https://dev.to/sabirpm/building-taskforge-translating-enterprise-chaos-into-an-open-source-scheduler-3ma4</link>
      <guid>https://dev.to/sabirpm/building-taskforge-translating-enterprise-chaos-into-an-open-source-scheduler-3ma4</guid>
      <description>&lt;p&gt;If you build enterprise software, you know the pain: you spend months solving complex architectural challenges, navigating network partitions, and building highly resilient systems, and you can never show it to anyone because it is locked behind corporate NDAs.&lt;/p&gt;

&lt;p&gt;At my day job, I worked heavily with a distributed job scheduler backed by Cassandra. Navigating those massive asynchronous workflows, database bottlenecks, and unpredictable worker crashes taught me invaluable lessons about distributed systems. I was incredibly proud of the architectural patterns I had mastered, but when I went to update my portfolio, I realized I had zero public proof of my backend engineering depth.&lt;/p&gt;

&lt;p&gt;So, I decided to build a brand new, open-source project to demonstrate those concepts.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;TaskForge&lt;/strong&gt;. This isn’t a clone of my previous work. It is a fresh implementation inspired by the failure modes I had learned to handle. It gave me something rare: a completely free playground. Instead of navigating rigid legacy architectures and corporate red tape, I had a blank canvas. I took the opportunity to experiment with different DevOps constraints, build out a strict monorepo, and completely change the database engine.&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%2F667uz2jg3hhmq5gvz44p.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%2F667uz2jg3hhmq5gvz44p.png" alt="TaskForge Console" width="799" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a look under the hood of how I built it, the intentional tradeoffs I made, and the boss fights I encountered along the way.&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%2Fzscl4qvc3wt7ffl3z1vk.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%2Fzscl4qvc3wt7ffl3z1vk.png" alt="TaskForge Architecture" width="798" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Engine: A Strict State Machine
&lt;/h2&gt;

&lt;p&gt;At the heart of TaskForge is a highly structured lifecycle. Every job in the system moves through a strict state machine inside PostgreSQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PENDING:&lt;/strong&gt; The job is scheduled in the DB and waiting for its time to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PROCESSING:&lt;/strong&gt; The Scheduler has reserved the job and published it to RabbitMQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RUNNING:&lt;/strong&gt; A Worker has claimed the job from the queue and is executing the business logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COMPLETED / FAILED:&lt;/strong&gt; The terminal states (success, or max attempts exhausted).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Great Pivot &amp;amp; The Atomic Claim
&lt;/h2&gt;

&lt;p&gt;The biggest architectural shift I made for this new implementation was moving away from Cassandra. Cassandra is a beast for decentralized, high-throughput systems, but trying to achieve a global, atomic lock on a specific job without creating a massive bottleneck in a masterless database is a headache.&lt;/p&gt;

&lt;p&gt;For TaskForge, I pivoted to PostgreSQL. I wanted to utilize the ACID guarantees of a relational database to handle race conditions safely.&lt;/p&gt;

&lt;p&gt;When the background Scheduler sweeps the database for due jobs, it uses &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt;. This is a critical pattern: it allows multiple scheduler instances to safely sweep for &lt;code&gt;PENDING&lt;/code&gt; jobs in parallel without blocking each other.&lt;/p&gt;

&lt;p&gt;Once the scheduler pushes the job to RabbitMQ, the Node.js Worker picks it up. If five workers pull the same duplicate message from the queue, we have to guarantee they don’t execute the same claim. The worker executes a targeted conditional update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The Atomic Worker Claim
const { rows } = await pool.query(
  `UPDATE jobs
   SET status = 'RUNNING',
       attempts = attempts + 1,
       locked_at = NOW(),
       locked_by = $2
   WHERE id = $1
     AND status = 'PROCESSING'
     AND run_at &amp;lt;= NOW()
   RETURNING *`,
  [jobId, WORKER_ID]
);
const job = rows[0];

if (!job) {
  // Another worker already claimed it, or it was cancelled.
  channel.ack(msg);
  return;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query returns the job only to the worker that won the conditional update. This safely prevents duplicate execution of the same database job claim (though because RabbitMQ guarantees at-least-once delivery, external side effects still require idempotency).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Message Broker: Gaps and Guarantees
&lt;/h2&gt;

&lt;p&gt;To ensure the workers don’t get overwhelmed, RabbitMQ is configured defensively. I utilized &lt;code&gt;prefetch(1)&lt;/code&gt; to ensure workers only ingest what they can immediately process, enabled publisher confirms, and routed poisoned messages to a &lt;strong&gt;Dead-Letter Queue (DLQ)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One of the most interesting challenges here is the &lt;strong&gt;DB-before-RabbitMQ&lt;/strong&gt; gap.&lt;/p&gt;

&lt;p&gt;In TaskForge, the Scheduler updates a job to &lt;code&gt;PROCESSING&lt;/code&gt; in Postgres before publishing it to RabbitMQ. If the publish fails due to a network blip, the job is stuck in &lt;code&gt;PROCESSING&lt;/code&gt; but is not in the queue. This is a deliberate at-least-once tradeoff. The system relies on a stale lease recovery sweeper that notices jobs stuck in &lt;code&gt;PROCESSING&lt;/code&gt; for too long and kicks them back to &lt;code&gt;PENDING&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering for Chaos
&lt;/h2&gt;

&lt;p&gt;The “happy path” in distributed systems is boring. Real engineering happens when things break.&lt;/p&gt;

&lt;p&gt;If a TaskForge worker fails to process a job due to a failing third-party API, it catches the error and calculates an exponential backoff delay. Notice that because we already incremented the attempts during the atomic claim above, the retry path simply reads the current attempts and kicks the job back to the penalty box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// The Penalty Box (Exponential Backoff)
const currentAttempts = jobState.attempts;
const delaySeconds = Math.pow(2, currentAttempts) * 5;
const retryResult = await pool.query(
  `UPDATE jobs
   SET status = 'PENDING',
       run_at = NOW() + ($1 * INTERVAL '1 second'),
       locked_at = NULL,
       locked_by = NULL
   WHERE id = $2
     AND locked_by = $3`,
  [delaySeconds, jobId, WORKER_ID]
);

if (retryResult.rowCount === 0) {
  channel.ack(msg);
  return;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if the worker container violently loses power mid-process? To prevent a “Zombie Worker” scenario, I intercept the operating system’s shutdown signals. Graceful shutdown stops new consumption and gives active jobs time to finish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// Intercepting the Reaper (Graceful Shutdown)
const shutdownConsumer = async (signal: string) =&amp;gt; {
  isShuttingDown = true;
  if (rabbitChannel &amp;amp;&amp;amp; consumerTag) {
    await rabbitChannel.cancel(consumerTag);
  }

  const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;
  while (activeJobs &amp;gt; 0 &amp;amp;&amp;amp; Date.now() &amp;lt; deadline) {
    await sleep(500);
  }

  if (rabbitChannel) await rabbitChannel.close();
  if (rabbitConnection) await rabbitConnection.close();
  await pool.end();

  process.exit(activeJobs === 0 ? 0 : 1);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the timeout hits before jobs finish, closing RabbitMQ causes those unacknowledged messages to be safely redelivered to another worker, while the stale lease recovery sweeper later reconciles the abandoned DB locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving It: How I Tested Failure
&lt;/h2&gt;

&lt;p&gt;You can’t claim a system is built around production failure modes without proving it. The strongest part of this project isn’t the code, it is the testing suite.&lt;/p&gt;

&lt;p&gt;I wrote integration tests using Vitest and Testcontainers to programmatically spin up real Postgres and RabbitMQ instances. I specifically wrote tests to break the system: simulating duplicate message deliveries, testing stale lock recovery, simulating child worker crashes, forcing RabbitMQ disconnects, and verifying the exponential backoff math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevOps Reality Check
&lt;/h2&gt;

&lt;p&gt;I set a stubborn goal for this project: I wanted the entire stack deployed to the cloud for free.&lt;/p&gt;

&lt;p&gt;This introduced some serious constraints. I deployed the Next.js UI to Vercel and the Postgres DB to Neon’s serverless platform. But the backend Workers and RabbitMQ require long-lived TCP connections - serverless wouldn’t cut it. I had to provision a free-tier AWS EC2 t3.micro instance.&lt;/p&gt;

&lt;p&gt;A t3.micro gives you exactly 1GiB of RAM. I had to completely gut my local docker-compose.yml for production. Because Postgres was now hosted remotely on Neon, I ripped the DB container out of the compose file, dynamically injected the Neon connection strings via environment variables, and configured a 2GiB Linux Swap File directly on the EC2's SSD. Without that swap file, the OS would have instantly OOM-killed RabbitMQ the moment memory spiked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intentional Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Any system architecture is a series of compromises. It is important to be transparent about what was left on the table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At-Least-Once Delivery&lt;/strong&gt;: The system favors at-least-once over exactly-once execution. Next steps for a true production environment would include implementing a robust Outbox Pattern to close the DB-to-Queue publish gap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Demo Friction&lt;/strong&gt;: The public observer UI uses simple rate-limiting rather than robust authentication. This was an intentional choice to make the project instantly accessible for anyone wanting to test the system under load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt;: Currently, the system relies on dashboard-grade health checks and audit logs rather than full Prometheus/Grafana metric scraping.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A Note on AI in the Workflow
&lt;/h2&gt;

&lt;p&gt;My focus throughout this project was on architectural decision-making, distributed systems logic, and edge case handling. The backend logic, locking mechanisms, and state machine were crafted line by line, with every argument and condition placed with deliberate precision.&lt;/p&gt;

&lt;p&gt;For the frontend, I used AI coding agents to scaffold React/Tailwind boilerplate, style the observer UI, and handle the visual polish. The goal was simply to get a clean, functional front door in place quickly. Delegating routine frontend styling freed up time to focus on the parts of the system that required more careful thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building a greenfield project inspired by the battle scars of your day job is an incredibly rewarding experience. TaskForge isn’t just a portfolio piece; it is a failure-tested blueprint of how I approach system architecture when the training wheels come off.&lt;/p&gt;

&lt;p&gt;You can watch the system handle load in real-time on the &lt;strong&gt;&lt;a href="https://taskforge-console.vercel.app" rel="noopener noreferrer"&gt;Live Console&lt;/a&gt;&lt;/strong&gt;, or dive into the architecture in the &lt;strong&gt;&lt;a href="https://github.com/anonlegionoke/taskforge" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>programming</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
