<?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: timothy ogbemudia</title>
    <description>The latest articles on DEV Community by timothy ogbemudia (@glamboyosa).</description>
    <link>https://dev.to/glamboyosa</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%2F3961198%2Fb1e6fa4e-b34b-43c5-9762-37f273b06903.JPG</url>
      <title>DEV Community: timothy ogbemudia</title>
      <link>https://dev.to/glamboyosa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/glamboyosa"/>
    <language>en</language>
    <item>
      <title>How to Build a PostgreSQL Backed Job Queue in Go</title>
      <dc:creator>timothy ogbemudia</dc:creator>
      <pubDate>Sun, 31 May 2026 13:37:34 +0000</pubDate>
      <link>https://dev.to/glamboyosa/how-to-build-a-postgresql-backed-job-queue-in-go-5an</link>
      <guid>https://dev.to/glamboyosa/how-to-build-a-postgresql-backed-job-queue-in-go-5an</guid>
      <description>&lt;p&gt;When you build a web application, not every task should happen inside the user's request.&lt;/p&gt;

&lt;p&gt;Some work is slow. Some work can fail. Some work should happen later. Sending emails, resizing images, processing webhooks, generating reports, and retrying third party APIs are all good examples.&lt;/p&gt;

&lt;p&gt;These tasks are usually handled by a background job system.&lt;/p&gt;

&lt;p&gt;In this article, we will use a Go project I built called &lt;a href="https://github.com/glamboyosa/swig" rel="noopener noreferrer"&gt;Swig&lt;/a&gt; as a practical example of how a PostgreSQL backed job queue can work.&lt;/p&gt;

&lt;p&gt;The goal is not only to understand Swig. The goal is also to learn Go concepts that show up in real backend systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interfaces&lt;/li&gt;
&lt;li&gt;Goroutines&lt;/li&gt;
&lt;li&gt;Contexts&lt;/li&gt;
&lt;li&gt;Transactions&lt;/li&gt;
&lt;li&gt;JSON serialization&lt;/li&gt;
&lt;li&gt;Driver abstraction&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;li&gt;PostgreSQL row locks&lt;/li&gt;
&lt;li&gt;PostgreSQL &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL advisory locks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you should understand how a background job queue can be built with Go and PostgreSQL, and why PostgreSQL is more than just a place to store rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Job Queue?
&lt;/h2&gt;

&lt;p&gt;A job queue is a system that stores work to be done later.&lt;/p&gt;

&lt;p&gt;Your application adds a job to the queue. A worker takes a job from the queue and runs it.&lt;/p&gt;

&lt;p&gt;For example, when a user signs up, your application might create the user immediately, then add a job like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_welcome_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome!"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A background worker later picks up that job and sends the email.&lt;/p&gt;

&lt;p&gt;This keeps the user request fast. The signup route does not need to wait for the email provider before returning a response.&lt;/p&gt;

&lt;p&gt;A job queue usually needs to answer a few important questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where are jobs stored?&lt;/li&gt;
&lt;li&gt;How do workers find jobs?&lt;/li&gt;
&lt;li&gt;How do we stop two workers from processing the same job?&lt;/li&gt;
&lt;li&gt;How do we retry failed jobs?&lt;/li&gt;
&lt;li&gt;How do we shut workers down safely?&lt;/li&gt;
&lt;li&gt;How do we keep job creation consistent with application data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swig answers those questions with Go and PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use PostgreSQL for a Queue?
&lt;/h2&gt;

&lt;p&gt;Many job queues use Redis, RabbitMQ, SQS, Kafka, or another dedicated queueing system. Those are all useful tools.&lt;/p&gt;

&lt;p&gt;But many applications already depend on PostgreSQL. If your app already has Postgres, you may not want to operate another service just to run background jobs.&lt;/p&gt;

&lt;p&gt;PostgreSQL gives us several features that are surprisingly useful for queues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables for durable job storage&lt;/li&gt;
&lt;li&gt;Transactions for atomic writes&lt;/li&gt;
&lt;li&gt;Row locks for safe concurrent processing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SKIP LOCKED&lt;/code&gt; for letting workers claim different jobs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; for waking workers when new jobs arrive&lt;/li&gt;
&lt;li&gt;Advisory locks for leader election&lt;/li&gt;
&lt;li&gt;JSONB for flexible job payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swig is a Go job queue that uses these PostgreSQL features directly.&lt;/p&gt;

&lt;p&gt;The tradeoff is important. A PostgreSQL backed queue is not trying to replace Kafka for event streaming or RabbitMQ for complex routing. It is trying to make common application background jobs simple, reliable, and easy to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swig's Basic Architecture
&lt;/h2&gt;

&lt;p&gt;At a high level, Swig has five parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;swig_jobs&lt;/code&gt; table in PostgreSQL&lt;/li&gt;
&lt;li&gt;Go workers that process jobs&lt;/li&gt;
&lt;li&gt;A worker registry that maps job names to worker types&lt;/li&gt;
&lt;li&gt;A driver layer that supports both &lt;code&gt;pgx&lt;/code&gt; and &lt;code&gt;database/sql&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A leader loop for shared maintenance work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The basic flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app calls &lt;code&gt;AddJob&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Swig serializes the job payload to JSON.&lt;/li&gt;
&lt;li&gt;Swig inserts a row into &lt;code&gt;swig_jobs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;PostgreSQL sends a notification that a job was created.&lt;/li&gt;
&lt;li&gt;A Go worker wakes up and tries to claim one pending job.&lt;/li&gt;
&lt;li&gt;PostgreSQL row locks ensure only one worker claims that row.&lt;/li&gt;
&lt;li&gt;The worker runs the job.&lt;/li&gt;
&lt;li&gt;Swig marks the job as completed or failed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sounds simple, but the details matter.&lt;/p&gt;

&lt;p&gt;The hard parts are concurrency, failure, connection lifecycle, and shutdown. That is where Go and PostgreSQL work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Representing Jobs in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;A simplified version of Swig's job table looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;scheduled_for&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;worker_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row is one job.&lt;/p&gt;

&lt;p&gt;The important columns are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kind&lt;/code&gt;: the type of job, such as &lt;code&gt;send_email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queue&lt;/code&gt;: the queue the job belongs to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload&lt;/code&gt;: the JSON data needed to run the job&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: whether the job is pending, processing, completed, or failed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt;: the job's priority inside the queue&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempts&lt;/code&gt;: how many times the job has been tried&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scheduled_for&lt;/code&gt;: when the job is allowed to run&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instance_id&lt;/code&gt;: which Swig instance claimed the job&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;worker_id&lt;/code&gt;: which worker claim is processing the job&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;locked_at&lt;/code&gt;: when the job was claimed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_error&lt;/code&gt;: the last error returned by the worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table is the source of truth.&lt;/p&gt;

&lt;p&gt;That is a key design point. PostgreSQL notifications can wake workers, but the notifications are not the durable queue. The rows in &lt;code&gt;swig_jobs&lt;/code&gt; are the durable queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining a Worker in Go
&lt;/h2&gt;

&lt;p&gt;In Swig, a worker is a Go type that knows how to process one kind of job.&lt;/p&gt;

&lt;p&gt;Here is a simple email worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EmailWorker&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"to"`&lt;/span&gt;
    &lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"subject"`&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"body"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;JobName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"send_email"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sending email to %s with subject %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JobName&lt;/code&gt; tells Swig what kind of job this worker handles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Process&lt;/code&gt; contains the actual work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The struct fields are also the job arguments.&lt;/p&gt;

&lt;p&gt;When you enqueue an &lt;code&gt;EmailWorker&lt;/code&gt;, Swig serializes the struct into JSON and stores it in PostgreSQL. Later, a worker claims the row, unmarshals the JSON back into an &lt;code&gt;EmailWorker&lt;/code&gt;, and calls &lt;code&gt;Process&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a simple API, but it teaches an important Go lesson: your data model and your behavior can live together in a small type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Concept: Interfaces
&lt;/h2&gt;

&lt;p&gt;Go interfaces describe behavior.&lt;/p&gt;

&lt;p&gt;Swig does not need to know the exact concrete type of every worker. It only needs to know that a worker can provide a job name and process a job.&lt;/p&gt;

&lt;p&gt;Conceptually, that behavior looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Worker&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;JobName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a type has those methods, it can behave like a worker.&lt;/p&gt;

&lt;p&gt;In Go, a type does not need to explicitly declare that it implements an interface. If the methods match, the type satisfies the interface.&lt;/p&gt;

&lt;p&gt;This is one of the reasons interfaces are so useful in Go. They let you design around behavior instead of inheritance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering Workers Without Sharing State
&lt;/h2&gt;

&lt;p&gt;Swig has a worker registry. The registry maps a job name to a worker type.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWorkerRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, when a job row says &lt;code&gt;kind = 'send_email'&lt;/code&gt;, Swig can look up the registered worker and run it.&lt;/p&gt;

&lt;p&gt;There is a subtle concurrency issue here.&lt;/p&gt;

&lt;p&gt;If the registry stored the exact &lt;code&gt;&amp;amp;EmailWorker{}&lt;/code&gt; pointer and reused it for every job, multiple goroutines could unmarshal payloads into the same Go value at the same time.&lt;/p&gt;

&lt;p&gt;That would be shared mutable state, and shared mutable state is where many concurrency bugs come from.&lt;/p&gt;

&lt;p&gt;Swig now avoids that by using a factory approach internally. Registration captures the worker type, and each claimed job gets a fresh worker instance before JSON is unmarshaled.&lt;/p&gt;

&lt;p&gt;The API stays simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But internally, Swig can create a new &lt;code&gt;EmailWorker&lt;/code&gt; for each job.&lt;/p&gt;

&lt;p&gt;This is a useful Go pattern. You can keep the public API simple while making the internal lifecycle safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a Job
&lt;/h2&gt;

&lt;p&gt;Here is what adding a job looks like from the user side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;swigClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Welcome!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"Thanks for signing up."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside Swig, the process is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;argsJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workerWithArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
    INSERT INTO swig_jobs (kind, queue, payload, priority, scheduled_for, status)
    VALUES ($1, $2, $3, $4, $5, 'pending')
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argsJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This uses Go's standard &lt;code&gt;encoding/json&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;The worker struct becomes JSON. PostgreSQL stores that JSON in a &lt;code&gt;JSONB&lt;/code&gt; column. Later, a worker unmarshals it back into a fresh worker instance.&lt;/p&gt;

&lt;p&gt;This is a common pattern in job systems. The job payload must cross a process boundary, so it needs a serializable format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactional Job Enqueueing
&lt;/h2&gt;

&lt;p&gt;One of the best reasons to use PostgreSQL for jobs is transactional enqueueing.&lt;/p&gt;

&lt;p&gt;Imagine a user signs up. You want to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert the user.&lt;/li&gt;
&lt;li&gt;Queue a welcome email.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If those happen separately, you can get inconsistent states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user is created, but the email job is not queued.&lt;/li&gt;
&lt;li&gt;The email job is queued, but the user is not created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a transaction, both succeed or both fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
    INSERT INTO users (email)
    VALUES ($1)
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swigClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddJobWithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EmailWorker&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Welcome!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"Thanks for joining."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is powerful because the application data and the background job commit together.&lt;/p&gt;

&lt;p&gt;If the transaction rolls back, the user is not created and the job is not queued.&lt;/p&gt;

&lt;p&gt;If the transaction commits, both are visible.&lt;/p&gt;

&lt;p&gt;This is much harder to guarantee when your database and queue are separate systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Concept: Context
&lt;/h2&gt;

&lt;p&gt;Most Swig methods accept &lt;code&gt;context.Context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Swig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contexts are used for cancellation, timeouts, and deadlines.&lt;/p&gt;

&lt;p&gt;That matters in a job queue because many operations can block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database queries&lt;/li&gt;
&lt;li&gt;Waiting for notifications&lt;/li&gt;
&lt;li&gt;Processing long jobs&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, you can create a timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the database operation takes longer than five seconds, the context can cancel it.&lt;/p&gt;

&lt;p&gt;In Go services, passing context through your call stack is a standard habit. It gives callers a way to say, "This work is no longer needed."&lt;/p&gt;

&lt;p&gt;Swig uses contexts for worker lifecycles too. When &lt;code&gt;Stop&lt;/code&gt; is called, Swig cancels worker contexts so blocked database calls and notification waits can return.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Part: Multiple Workers
&lt;/h2&gt;

&lt;p&gt;A queue gets interesting when many workers run at the same time.&lt;/p&gt;

&lt;p&gt;Imagine three workers all asking PostgreSQL for the next pending job. We do not want all three workers to process the same job.&lt;/p&gt;

&lt;p&gt;A naive approach might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&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;'pending'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the worker updates the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&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;'processing'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has a race condition. Two workers can select the same job before either one updates it.&lt;/p&gt;

&lt;p&gt;Swig avoids this with PostgreSQL row locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL Concept: &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;PostgreSQL can lock rows selected inside a transaction.&lt;/p&gt;

&lt;p&gt;The key phrase is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; means "lock this row because I plan to update it."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SKIP LOCKED&lt;/code&gt; means "if another worker already locked a row, skip it and find another one."&lt;/p&gt;

&lt;p&gt;This is perfect for a queue.&lt;/p&gt;

&lt;p&gt;Worker A locks job 1. Worker B skips job 1 and locks job 2. Worker C skips jobs 1 and 2 and locks job 3.&lt;/p&gt;

&lt;p&gt;No central coordinator is needed.&lt;/p&gt;

&lt;p&gt;A helpful Go analogy is &lt;code&gt;sync.Mutex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A Go mutex protects shared memory inside one process. PostgreSQL row locks protect shared rows across many transactions, processes, and machines.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;SKIP LOCKED&lt;/code&gt; is not literally a Go mutex, but it plays a similar coordination role at the database level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atomic Job Claiming
&lt;/h2&gt;

&lt;p&gt;Swig uses an atomic update pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&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;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;worker_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&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;id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&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;'pending'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;scheduled_for&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&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;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query does several things at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds a pending job.&lt;/li&gt;
&lt;li&gt;Skips jobs already locked by other workers.&lt;/li&gt;
&lt;li&gt;Marks the selected job as processing.&lt;/li&gt;
&lt;li&gt;Records which worker claimed it.&lt;/li&gt;
&lt;li&gt;Returns the job data to Go.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That atomic claim is the heart of the queue.&lt;/p&gt;

&lt;p&gt;The important part is that the selection and state change happen together. Workers are not doing a separate &lt;code&gt;SELECT&lt;/code&gt; and then hoping the later &lt;code&gt;UPDATE&lt;/code&gt; is still safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Concept: Goroutines
&lt;/h2&gt;

&lt;p&gt;Swig starts worker loops as goroutines.&lt;/p&gt;

&lt;p&gt;In Go, a goroutine is a lightweight concurrent function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queueType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A queue can start several workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxWorkers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queueType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worker runs independently. PostgreSQL coordinates which job each worker gets.&lt;/p&gt;

&lt;p&gt;This is a nice division of responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go handles concurrency with goroutines.&lt;/li&gt;
&lt;li&gt;PostgreSQL handles safe job claiming with locks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the reasons Go works well for background processing. You can express concurrency directly, while still relying on the database for cross-process coordination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Concept: WaitGroup and Graceful Shutdown
&lt;/h2&gt;

&lt;p&gt;When a service shuts down, it should wait for workers to finish cleanly.&lt;/p&gt;

&lt;p&gt;Go's &lt;code&gt;sync.WaitGroup&lt;/code&gt; helps with this.&lt;/p&gt;

&lt;p&gt;The pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;processJobs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swig uses a WaitGroup to track active worker goroutines.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;Stop&lt;/code&gt; is called, Swig cancels worker contexts, closes the shutdown signal, waits for workers, cleans up jobs owned by the instance, and releases leader resources.&lt;/p&gt;

&lt;p&gt;Swig also uses &lt;code&gt;sync.Once&lt;/code&gt; to make shutdown idempotent.&lt;/p&gt;

&lt;p&gt;That means calling &lt;code&gt;Stop&lt;/code&gt; more than once should not panic because of a double channel close.&lt;/p&gt;

&lt;p&gt;This is a small detail, but it matters. Shutdown paths are often where production systems behave differently from happy path demos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waking Workers with &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If workers constantly query the database for jobs, they waste resources when the queue is empty.&lt;/p&gt;

&lt;p&gt;PostgreSQL has a feature called &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A connection can listen on a channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;LISTEN&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another session can send a notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;NOTIFY&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"id":"job-id"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swig creates a trigger so PostgreSQL sends a notification after a job is inserted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;swig_jobs_notify_trigger&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_job_created&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means workers can sleep when there is no work and wake when a new job arrives.&lt;/p&gt;

&lt;p&gt;But there is an important PostgreSQL detail: &lt;code&gt;LISTEN&lt;/code&gt; is session-scoped.&lt;/p&gt;

&lt;p&gt;That means a worker must wait for notifications on the same database session that executed &lt;code&gt;LISTEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This matters when you use a connection pool. If &lt;code&gt;LISTEN&lt;/code&gt; happens on one pooled connection, but &lt;code&gt;WaitForNotification&lt;/code&gt; happens on another pooled connection, the worker may not receive the notification.&lt;/p&gt;

&lt;p&gt;Swig handles this by creating a dedicated listener for each worker.&lt;/p&gt;

&lt;p&gt;The listener owns one database session. That same session runs &lt;code&gt;LISTEN&lt;/code&gt;, waits for notifications, and gets closed during shutdown.&lt;/p&gt;

&lt;p&gt;This is a great example of a backend engineering lesson: abstractions like connection pools are useful, but some database features still depend on the lifecycle of a specific connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leader Election with Advisory Locks
&lt;/h2&gt;

&lt;p&gt;Some queue maintenance tasks should only be done by one instance.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrying failed jobs&lt;/li&gt;
&lt;li&gt;Recovering stale processing jobs&lt;/li&gt;
&lt;li&gt;Cleaning old job history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swig uses PostgreSQL advisory locks for leader election:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_try_advisory_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the result is true, that Swig instance becomes the leader.&lt;/p&gt;

&lt;p&gt;Advisory locks are application-defined locks managed by PostgreSQL. They are useful because you can coordinate distributed processes without adding a separate lock service.&lt;/p&gt;

&lt;p&gt;But advisory locks are also session-scoped.&lt;/p&gt;

&lt;p&gt;That means the lock is held by a specific PostgreSQL session. If that session ends, PostgreSQL releases the lock.&lt;/p&gt;

&lt;p&gt;Swig uses a dedicated advisory-lock connection for leadership. The leader runs maintenance work while that connection remains open.&lt;/p&gt;

&lt;p&gt;When Swig shuts down, it releases the advisory lock from the same session that acquired it.&lt;/p&gt;

&lt;p&gt;If an instance does not become leader, it keeps retrying in the background. If the current leader dies and PostgreSQL releases the lock, another Swig instance can acquire leadership later.&lt;/p&gt;

&lt;p&gt;That gives Swig a simple failover model without ZooKeeper, etcd, or a separate coordinator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Concept: Driver Abstraction
&lt;/h2&gt;

&lt;p&gt;Swig supports both &lt;code&gt;pgx&lt;/code&gt; and &lt;code&gt;database/sql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgx&lt;/code&gt; is a PostgreSQL focused driver. &lt;code&gt;database/sql&lt;/code&gt; is Go's standard database abstraction.&lt;/p&gt;

&lt;p&gt;Instead of writing the queue twice, Swig defines a driver interface.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Driver&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;QueryRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="n"&gt;Row&lt;/span&gt;
    &lt;span class="n"&gt;WithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;NewListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;TryAdvisoryLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lockID&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AdvisoryLock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The driver interface hides the concrete database library from the queue logic.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pgx&lt;/code&gt; implementation can use &lt;code&gt;pgxpool&lt;/code&gt;. The &lt;code&gt;database/sql&lt;/code&gt; implementation can use &lt;code&gt;*sql.DB&lt;/code&gt; and &lt;code&gt;lib/pq&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The core queue code only depends on behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you execute SQL?&lt;/li&gt;
&lt;li&gt;Can you run a transaction?&lt;/li&gt;
&lt;li&gt;Can you create a listener?&lt;/li&gt;
&lt;li&gt;Can you try to acquire an advisory lock?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a common Go design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the behavior your core package needs.&lt;/li&gt;
&lt;li&gt;Write small adapters for concrete dependencies.&lt;/li&gt;
&lt;li&gt;Keep the core logic independent from specific libraries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Interfaces are especially useful at boundaries. Database drivers, HTTP clients, storage backends, and queues are all good places to use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Failed Jobs
&lt;/h2&gt;

&lt;p&gt;Jobs can fail.&lt;/p&gt;

&lt;p&gt;An email provider might be down. A network request might timeout. A third party API might rate limit you.&lt;/p&gt;

&lt;p&gt;When a worker returns an error, Swig records the error and either retries the job or marks it as failed.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;swig_jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_error_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the queue a memory of what happened.&lt;/p&gt;

&lt;p&gt;Failed jobs can be retried with backoff. Jobs that exceed their maximum attempts can stay failed for inspection.&lt;/p&gt;

&lt;p&gt;In production systems, you often add more around this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dead letter queues&lt;/li&gt;
&lt;li&gt;Metrics&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;li&gt;Alerts&lt;/li&gt;
&lt;li&gt;Manual retry tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the foundation is simple: store the attempt count and error state in the job row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery Semantics: Be Careful with "Exactly Once"
&lt;/h2&gt;

&lt;p&gt;It is tempting to say a job queue processes jobs exactly once.&lt;/p&gt;

&lt;p&gt;In distributed systems, that is a dangerous claim.&lt;/p&gt;

&lt;p&gt;Swig prevents multiple workers from claiming the same pending job at the same time. That is important.&lt;/p&gt;

&lt;p&gt;But imagine this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Worker sends an email.&lt;/li&gt;
&lt;li&gt;Worker crashes before marking the job completed.&lt;/li&gt;
&lt;li&gt;The job is retried.&lt;/li&gt;
&lt;li&gt;The email might be sent again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means the safer language is:&lt;/p&gt;

&lt;p&gt;Swig provides atomic claiming and at-least-once processing.&lt;/p&gt;

&lt;p&gt;Because jobs can be retried, workers should be idempotent.&lt;/p&gt;

&lt;p&gt;Idempotent means running the same operation more than once has the same result as running it once.&lt;/p&gt;

&lt;p&gt;For example, an email job could use an idempotency key so the email provider does not send duplicates.&lt;/p&gt;

&lt;p&gt;This is one of the most important lessons in queue design. The queue can reduce duplicate processing, but workers still need to handle retries safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Project Teaches About Go
&lt;/h2&gt;

&lt;p&gt;Swig is a useful learning project because it touches many parts of Go backend development.&lt;/p&gt;

&lt;p&gt;It teaches interfaces because the database layer supports multiple drivers.&lt;/p&gt;

&lt;p&gt;It teaches goroutines because workers run concurrently.&lt;/p&gt;

&lt;p&gt;It teaches contexts because database calls, workers, and shutdown need cancellation.&lt;/p&gt;

&lt;p&gt;It teaches transactions because enqueueing jobs can be atomic with application writes.&lt;/p&gt;

&lt;p&gt;It teaches JSON serialization because job arguments need to cross a process boundary.&lt;/p&gt;

&lt;p&gt;It teaches error handling because failed jobs need retry behavior.&lt;/p&gt;

&lt;p&gt;It teaches lifecycle management because listeners, advisory locks, and worker goroutines all need to be opened and closed carefully.&lt;/p&gt;

&lt;p&gt;Most importantly, it teaches that backend systems are not just about the happy path.&lt;/p&gt;

&lt;p&gt;The interesting work is in concurrency, failure, connection ownership, shutdown, and consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A PostgreSQL backed queue is not the right answer for every system.&lt;/p&gt;

&lt;p&gt;If you need massive event streaming, Kafka may be a better fit. If you need a dedicated broker with routing features, RabbitMQ may be better. If you need a simple fast queue, Redis may be a good choice.&lt;/p&gt;

&lt;p&gt;But for many Go applications, PostgreSQL is already there.&lt;/p&gt;

&lt;p&gt;Using it for background jobs can be practical, simple, and powerful.&lt;/p&gt;

&lt;p&gt;Swig shows how far you can get with a small Go API and a few PostgreSQL features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store jobs in a table.&lt;/li&gt;
&lt;li&gt;Claim jobs with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Wake workers with dedicated &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; sessions.&lt;/li&gt;
&lt;li&gt;Coordinate leadership with dedicated advisory-lock sessions.&lt;/li&gt;
&lt;li&gt;Use transactions to keep app data and jobs consistent.&lt;/li&gt;
&lt;li&gt;Use goroutines and contexts to manage worker lifecycles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination makes a great project for learning Go, PostgreSQL, and distributed systems fundamentals.&lt;/p&gt;

</description>
      <category>go</category>
      <category>redis</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
