<?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: Eugene Berger</title>
    <description>The latest articles on DEV Community by Eugene Berger (@f1bonacc1).</description>
    <link>https://dev.to/f1bonacc1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4016321%2F3d287342-c15e-4296-9044-3b1f7d782e16.jpg</url>
      <title>DEV Community: Eugene Berger</title>
      <link>https://dev.to/f1bonacc1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/f1bonacc1"/>
    <language>en</language>
    <item>
      <title>Durable and HA task queues and DAG workflows in Go</title>
      <dc:creator>Eugene Berger</dc:creator>
      <pubDate>Tue, 07 Jul 2026 22:34:48 +0000</pubDate>
      <link>https://dev.to/f1bonacc1/durable-and-ha-task-queues-and-dag-workflows-in-go-12dh</link>
      <guid>https://dev.to/f1bonacc1/durable-and-ha-task-queues-and-dag-workflows-in-go-12dh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oe5ye680gjr3vig4awb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oe5ye680gjr3vig4awb.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;em&gt;ebind is an MIT-licensed Go library that gives you a persistent task queue and a durable workflow engine on a single dependency - NATS JetStream - which it can embed right inside your process.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every background-job story in Go seems to start the same way: pick a queue library, then stand up the infrastructure it drags along. Redis for Asynq or Machinery. Postgres for River. A whole cluster if you want Temporal-style workflows. The job code is ten lines; the deployment diagram is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/F1bonacc1/ebind" rel="noopener noreferrer"&gt;ebind&lt;/a&gt; is my attempt at a different trade: &lt;strong&gt;one dependency - NATS JetStream - carries the queue, the workflow state, and the event bus.&lt;/strong&gt; And because NATS is a Go library as well as a server, ebind can boot it &lt;em&gt;inside your process&lt;/em&gt;: a single embedded node for dev and small deployments, or a full 3-node JetStream cluster in-process for HA testing. No Redis. No Postgres. No sidecar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enqueue a function, not a string
&lt;/h2&gt;

&lt;p&gt;Most queue libraries make you register a string name and hand-roll payload structs. ebind's API is function-first - you pass the function itself:&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="c"&gt;// Any function shaped like (context.Context, ...args) (T, error) or (context.Context, ...args) error.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;SendEmail&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;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&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="kt"&gt;string&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;span class="c"&gt;// ... actually send ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"msg-id-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;reg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MustRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SendEmail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Producer and worker can be different binaries - this is a durable queue, not an RPC.&lt;/span&gt;
&lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SendEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msgID&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;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Await&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="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// typed result, not interface{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood a registry maps canonical names (derived via &lt;code&gt;runtime.FuncForPC&lt;/code&gt;) to &lt;code&gt;reflect.Value&lt;/code&gt;s, and argument types are validated &lt;em&gt;client-side, before publish&lt;/em&gt; - pass an &lt;code&gt;int&lt;/code&gt; where a &lt;code&gt;string&lt;/code&gt; goes and &lt;code&gt;Enqueue&lt;/code&gt; fails immediately, not on some worker at 3 a.m.&lt;/p&gt;

&lt;p&gt;Why reflection instead of generics? Go has no variadic generics. An API where &lt;code&gt;Enqueue(c, MyFunc, a, b, c)&lt;/code&gt; type-checks &lt;code&gt;a, b, c&lt;/code&gt; against &lt;code&gt;MyFunc&lt;/code&gt;'s signature simply cannot be expressed with generics today. The &lt;code&gt;reflect.Value.Call&lt;/code&gt; cost is a few hundred nanoseconds - noise next to any real handler doing I/O.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows that survive restarts
&lt;/h2&gt;

&lt;p&gt;The workflow layer adds durable DAGs on top: steps, dependencies, retries - with all state in a NATS KV bucket, so a workflow outlives the process that submitted it.&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;dag&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;FetchUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepOpts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"enrich"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enrich&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepOption&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"combine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RefOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enriched&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Submit&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;wf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;profile&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;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Await&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Profile&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;wf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&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;c&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;a.Ref()&lt;/code&gt; wires data flow &lt;em&gt;and&lt;/em&gt; dependency: &lt;code&gt;combine&lt;/code&gt; receives &lt;code&gt;fetch&lt;/code&gt;'s result, and cascade-skips if &lt;code&gt;fetch&lt;/code&gt; fails. &lt;code&gt;RefOrDefault&lt;/code&gt; substitutes a fallback when an optional step fails. Handlers can add steps dynamically mid-run (&lt;code&gt;workflow.FromContext(ctx).Step(...)&lt;/code&gt;), steps can be pinned to specific workers ("this step needs the GPU box", "run this on the same worker that ran the download"), and whole DAGs can be paused, resumed, or canceled durably.&lt;/p&gt;

&lt;p&gt;Because results live in KV, waiting is decoupled from submitting: persist two IDs, and &lt;em&gt;any&lt;/em&gt; process on the cluster can pick up the wait later with &lt;code&gt;workflow.AwaitByID[Profile](ctx, wf, dagID, stepID)&lt;/code&gt; - even for results written before it started watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breakpoints: a debugger for your workflows
&lt;/h2&gt;

&lt;p&gt;The feature I haven't seen elsewhere: &lt;strong&gt;step breakpoints with debugger semantics.&lt;/strong&gt;&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;upload&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepOpts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"upload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Upload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepOption&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BreakBefore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BeforeUpload"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c"&gt;// Breakpoint labels are part of the DAG's structure, but ARMING them is a runtime decision:&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Submit&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;wf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithActiveBreakpoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BeforeUpload"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DAG runs until &lt;code&gt;upload&lt;/code&gt; is about to be dispatched, then stops that line - parallel branches keep running. You inspect intermediate results (they're in KV), then continue:&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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResumeBreakpoint&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;wf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"BeforeUpload"&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;ResumeBreakpoint&lt;/code&gt; is &lt;em&gt;continue&lt;/em&gt;, not &lt;em&gt;disable&lt;/em&gt;: the label stays armed, so a later step carrying it - including dynamically added ones - stops again. Blocked state lives in KV, so it survives restarts, and any process (or the bundled &lt;code&gt;ebctl&lt;/code&gt; CLI) can list and resume breakpoints. &lt;code&gt;BreakAfter&lt;/code&gt; is the complement: let a step finish and persist its result, but hold its dependents.&lt;/p&gt;

&lt;p&gt;If you've ever added a "pause step" to a pipeline just so you could look at an intermediate file before the expensive part runs, this is that - as a first-class primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler needs no leader
&lt;/h2&gt;

&lt;p&gt;The part I'm most pleased with architecturally: &lt;strong&gt;every worker runs the scheduler, and correctness doesn't depend on electing a leader.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All state mutations are compare-and-swap on KV revisions (&lt;code&gt;Update(key, val, expectedRevision)&lt;/code&gt;). Step enqueues are deduplicated by JetStream message ID (&lt;code&gt;&amp;lt;dag_id&amp;gt;:&amp;lt;step_id&amp;gt;&lt;/code&gt;). DAG events flow through a work-queue stream with explicit acks. Stack those three and racing schedulers become harmless: two instances that both decide "step X is ready" produce one enqueue; two writers racing on a step record - one wins, the other re-reads and retries. There &lt;em&gt;is&lt;/em&gt; a leader elector, but it's defense-in-depth for failover windows (plus a sweep that rescues stranded steps when leadership changes hands), not a correctness requirement.&lt;/p&gt;

&lt;p&gt;The claim isn't just asserted - the repo carries a chaos end-to-end suite that runs every supported operation on a 3-node in-process cluster with replicas=3 while killing and restarting nodes mid-workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations included
&lt;/h2&gt;

&lt;p&gt;Failed steps durably record &lt;em&gt;why&lt;/em&gt; they failed (&lt;code&gt;error_kind&lt;/code&gt; + message on the step record itself, full error in a 7-day DLQ stream), and a cobra-based operator CLI ships in the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ebctl dag &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--label&lt;/span&gt; billing        &lt;span class="c"&gt;# query workflow history by immutable labels&lt;/span&gt;
ebctl dag tree &amp;lt;dag-id&amp;gt;             &lt;span class="c"&gt;# render the DAG with per-step status&lt;/span&gt;
ebctl dag step get &amp;lt;dag-id&amp;gt; &amp;lt;step&amp;gt;  &lt;span class="c"&gt;# why did this step fail?&lt;/span&gt;
ebctl dlq &lt;span class="nb"&gt;ls&lt;/span&gt;                        &lt;span class="c"&gt;# dead-lettered tasks&lt;/span&gt;
ebctl dag watch                     &lt;span class="c"&gt;# live event feed, incl. breakpoint hits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;ebind is young (v0.6.0) and honest about its scope. It's at-least-once delivery with dedupe - handlers should be idempotent, as in every queue system that tells you the truth. It is not Temporal: there are no signals, sagas, or compensation yet (they're on the roadmap). And it's unapologetically NATS-native - if you can't run NATS, it's not for you; if you already do, you have everything ebind needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/f1bonacc1/ebind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo has &lt;a href="https://github.com/F1bonacc1/ebind/tree/main/examples" rel="noopener noreferrer"&gt;16 runnable examples&lt;/a&gt;, each self-contained with its own embedded NATS - from a basic queue to breakpoints and placement. I'd genuinely love feedback, especially on the reflection-based API and the no-leader scheduler design: &lt;a href="https://github.com/F1bonacc1/ebind" rel="noopener noreferrer"&gt;github.com/F1bonacc1/ebind&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>dag</category>
      <category>tooling</category>
      <category>workflows</category>
    </item>
  </channel>
</rss>
