<?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: Grafikui</title>
    <description>The latest articles on DEV Community by Grafikui (@grafikui).</description>
    <link>https://dev.to/grafikui</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%2F3233613%2Fd1d8257e-fdaa-44c9-9cb7-18cb7c301e53.jpeg</url>
      <title>DEV Community: Grafikui</title>
      <link>https://dev.to/grafikui</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/grafikui"/>
    <language>en</language>
    <item>
      <title>I built a carbon linter for Terraform PRs — here's the full stack</title>
      <dc:creator>Grafikui</dc:creator>
      <pubDate>Mon, 04 May 2026 08:05:22 +0000</pubDate>
      <link>https://dev.to/grafikui/i-built-a-carbon-linter-for-terraform-prs-heres-the-full-stack-3g5p</link>
      <guid>https://dev.to/grafikui/i-built-a-carbon-linter-for-terraform-prs-heres-the-full-stack-3g5p</guid>
      <description>&lt;p&gt;Most cloud sustainability tools are built for sustainability officers.&lt;/p&gt;

&lt;p&gt;They pull three-month-old billing data, run it through a proprietary model, and produce a PDF that engineers never see.&lt;/p&gt;

&lt;p&gt;By the time you know your &lt;code&gt;us-east-1&lt;/code&gt; cluster emits twice as much as &lt;code&gt;us-west-2&lt;/code&gt; would have, it's been running for a quarter. The architecture is locked in. The carbon is already burnt.&lt;/p&gt;

&lt;p&gt;The only moment you can actually change what gets deployed is the pull request.&lt;/p&gt;

&lt;p&gt;So I built a tool that lives there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What GreenOps CLI does
&lt;/h2&gt;

&lt;p&gt;It's a GitHub Action that reads a &lt;code&gt;terraform show -json&lt;/code&gt; plan output and posts a carbon and cost diff directly on the PR, across AWS, Azure, and GCP, before anyone clicks merge.&lt;/p&gt;

&lt;p&gt;Here's what it posted on a real PR this week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
🌱 GreenOps Infrastructure Impact

Metric                    Monthly Total
Scope 2 — Operational     21.23 kg CO₂e
Scope 3 — Embodied         4.17 kg CO₂e
Total Lifecycle           25.40 kg CO₂e
Water Consumption          22.5 L
Infrastructure Cost       $280.32/month

Potential savings: −20.75 kg CO₂e/month (97.7%)
💡 3 recommendations found

Resource               Type         Region     Action
aws_instance.web       m5.xlarge    us-east-1  → eu-north-1
aws_instance.api       m5.large     us-east-1  → eu-north-1
aws_db_instance.main   db.m5.large  us-east-1  → eu-north-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 97.7% saving exists because &lt;code&gt;eu-north-1&lt;/code&gt; (Stockholm) runs on near-zero carbon electricity at 8.8g CO₂e/kWh. &lt;code&gt;us-east-1&lt;/code&gt; sits at 384.5g CO₂e/kWh. The same infrastructure. Forty-four times the emissions. GreenOps surfaces that before you ship it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The math is open
&lt;/h2&gt;

&lt;p&gt;Every number comes from a static &lt;code&gt;factors.json&lt;/code&gt; ledger you can read in ten minutes. The formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W_cpu    = W_idle + (W_max − W_idle) × utilisation
W_memory = memory_gb × 0.392 W/GB     ← CCF v3 standard
W_total  = W_cpu + W_memory
energy_kwh = W_total × PUE × 730h / 1000
co2e_g     = energy_kwh × grid_intensity_gco2e_per_kwh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;m5.large&lt;/code&gt; in &lt;code&gt;us-east-1&lt;/code&gt; at 50% utilisation this produces &lt;strong&gt;5,308g CO₂e/month&lt;/strong&gt;. That value is asserted in &lt;code&gt;engine.test.ts&lt;/code&gt;. You can verify it yourself from &lt;code&gt;factors.json&lt;/code&gt; in under five minutes.&lt;/p&gt;

&lt;p&gt;PUE differs by provider: AWS 1.13, Azure 1.125, GCP 1.10. Memory power draw is constant, DRAM draws near-constant power regardless of CPU utilisation, which is why memory-optimised instances carry disproportionately higher emissions than their vCPU count suggests.&lt;/p&gt;

&lt;p&gt;The full methodology is at &lt;a href="https://getgreenops.com/methodology" rel="noopener noreferrer"&gt;getgreenops.com/methodology&lt;/a&gt;. MIT-licensed. If you disagree with an assumption, open a PR.&lt;/p&gt;




&lt;h2&gt;
  
  
  It also posts inline suggestions
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;post-suggestions: true&lt;/code&gt;, GreenOps posts a committable suggestion directly on the &lt;code&gt;instance_type&lt;/code&gt; line in the PR diff.&lt;/p&gt;

&lt;p&gt;The developer sees the suggestion in the Files changed tab. One click on "Commit suggestion" and the instance type is updated in place. No separate PR. No separate ticket. The carbon reduction is captured at the same moment the infrastructure decision is made.&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%2Fpmzv8o1lari4icwr1x5h.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%2Fpmzv8o1lari4icwr1x5h.png" alt="GreenOps carbon analysis comment on a GitHub PR showing Scope 2 CO2e, Scope 3 embodied carbon, water consumption, cost, and three region-shift recommendations with 97.7% potential savings" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsivc3cj53jyapp5jqmsx.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%2Fsivc3cj53jyapp5jqmsx.png" alt="GreenOps dashboard showing lifecycle CO2e trend chart, emissions prevented metric, and recent PR runs table with per-repository carbon data" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It tracks history on the dashboard
&lt;/h2&gt;

&lt;p&gt;Every run posts to &lt;a href="https://getgreenops.com" rel="noopener noreferrer"&gt;getgreenops.com&lt;/a&gt; via an optional API key. The dashboard shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total lifecycle CO₂e saved across all PRs&lt;/li&gt;
&lt;li&gt;Trend chart over the last 90 days&lt;/li&gt;
&lt;li&gt;Per-repository leaderboard&lt;/li&gt;
&lt;li&gt;Per-run resource breakdown — every instance, every recommendation, every skipped resource with the reason why&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two lines of YAML
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreenOps Carbon Analysis&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omrdev1/greenops-cli@v0.7.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plan-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;post-suggestions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GREENOPS_API_KEY }}&lt;/span&gt;   &lt;span class="c1"&gt;# optional — enables dashboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires only &lt;code&gt;pull-requests: write&lt;/code&gt;. No cloud credentials. No outbound network calls from the CLI. Works in air-gapped and SOC 2 environments where SaaS carbon tools cannot be deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  You can also enforce carbon budgets
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;.greenops.yml&lt;/code&gt; to your repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;budgets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;max_lifecycle_co2e_kg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;max_pr_cost_increase_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;span class="na"&gt;fail_on_violation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fail_on_violation: true&lt;/code&gt; exits with code 1. The merge is blocked. The PR comment shows which constraint was breached and by how much.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters for CSRD
&lt;/h2&gt;

&lt;p&gt;The EU Corporate Sustainability Reporting Directive requires large companies to report Scope 2 and Scope 3 emissions from 2024 onwards, with SMEs following from 2026.&lt;/p&gt;

&lt;p&gt;Cloud infrastructure is one of the largest and fastest-growing sources of corporate Scope 2 emissions. Most companies have no mechanism to trace which team, which PR, which infrastructure decision drove which emission.&lt;/p&gt;

&lt;p&gt;GreenOps creates that audit trail — per PR, per repository, per engineer — at the point where the decision was made. The methodology is CCF v3 aligned. Every formula is documented. Every number is testable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current coverage
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ AWS — 40 instance types across 14 regions&lt;/li&gt;
&lt;li&gt;✅ Azure — 16 instance types across 8 regions&lt;/li&gt;
&lt;li&gt;✅ GCP — 15 instance types across 7 regions&lt;/li&gt;
&lt;li&gt;✅ Scope 2 operational + Scope 3 embodied + water consumption&lt;/li&gt;
&lt;li&gt;✅ ARM instance upgrade recommendations (Graviton, Ampere Altra, T2A)&lt;/li&gt;
&lt;li&gt;✅ Region shift recommendations based on grid carbon intensity&lt;/li&gt;
&lt;li&gt;⚠️ Lambda/serverless — flagged as unsupported, not silently ignored&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it in 5 minutes
&lt;/h2&gt;

&lt;p&gt;Fork &lt;a href="https://github.com/omrdev1/greenops-e2e-demo" rel="noopener noreferrer"&gt;github.com/omrdev1/greenops-e2e-demo&lt;/a&gt; — it has the workflow, a Terraform config with intentionally high-carbon instances, and runs without an AWS account. Open a PR and GreenOps runs automatically.&lt;/p&gt;

&lt;p&gt;Or add it to any existing Terraform repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreenOps Carbon Analysis&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omrdev1/greenops-cli@v0.7.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plan-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key needed for the PR comment. The dashboard is optional.&lt;/p&gt;




&lt;p&gt;→ &lt;a href="https://github.com/omrdev1/greenops-cli" rel="noopener noreferrer"&gt;github.com/omrdev1/greenops-cli&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://getgreenops.com" rel="noopener noreferrer"&gt;getgreenops.com&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://getgreenops.com/methodology" rel="noopener noreferrer"&gt;getgreenops.com/methodology&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by Omar — Co-founder, &lt;a href="https://grafikui.com" rel="noopener noreferrer"&gt;Grafikui&lt;/a&gt;. Open source, MIT-licensed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>sustainability</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>How I added carbon tracking to GitHub Actions (now with AWS, Azure, and GCP)</title>
      <dc:creator>Grafikui</dc:creator>
      <pubDate>Fri, 20 Mar 2026 18:07:57 +0000</pubDate>
      <link>https://dev.to/grafikui/how-i-added-carbon-tracking-to-github-actions-and-why-devops-should-care-194n</link>
      <guid>https://dev.to/grafikui/how-i-added-carbon-tracking-to-github-actions-and-why-devops-should-care-194n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Updated March 2026:&lt;/strong&gt; v0.5.x adds multi-cloud support (AWS + Azure + GCP), Scope 3 embodied carbon, water consumption tracking, inline PR suggestions, and policy budgets. Original post covered AWS-only v0.2.x.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Most carbon tracking tools are dashboards built for sustainability officers. They pull three-month-old billing data and produce a PDF. Engineers never see it.&lt;/p&gt;

&lt;p&gt;This post is about shifting that left — into the pull request, before infrastructure is provisioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with post-hoc measurement
&lt;/h2&gt;

&lt;p&gt;By the time AWS's Carbon Footprint Tool tells you that your &lt;code&gt;us-east-1&lt;/code&gt; cluster emits twice as much as &lt;code&gt;us-west-2&lt;/code&gt; would have, it has been running for a quarter. The architecture is locked in, the carbon is burnt.&lt;/p&gt;

&lt;p&gt;The only intervention point that changes what gets deployed is the PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GreenOps CLI does
&lt;/h2&gt;

&lt;p&gt;It's a GitHub Action that reads a &lt;code&gt;terraform show -json&lt;/code&gt; plan output and posts a carbon and cost diff directly on the PR — across AWS, Azure, and GCP.&lt;/p&gt;

&lt;p&gt;Here's what a developer sees on a multi-cloud plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;🌱 GreenOps Infrastructure Impact

Metric                     | Monthly Total
Scope 2 — Operational CO2e | 12.27kg
Scope 3 — Embodied CO2e    | 3.13kg
Total Lifecycle CO2e       | 15.40kg
Water Consumption          | 12.8L
Infrastructure Cost        | $210.97/month

Potential Scope 2 Savings: -11.88kg CO2e/month (96.8%) | -$13.14/month
💡 Found 3 optimization recommendations.

Resource                           | Instance        | Region      | Scope 2 | Action
aws_instance.web                   | m5.large        | us-east-1   | 4.31kg  | UPGRADE
azurerm_linux_virtual_machine.api  | Standard_D2s_v3 | eastus      | 4.24kg  | UPGRADE
google_compute_instance.worker     | n2-standard-2   | us-central1 | 3.71kg  | UPGRADE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cloud credentials required. No outbound network calls from the CLI. The emission factors are a static JSON file you can read in ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the math works
&lt;/h2&gt;

&lt;p&gt;Three dimensions are tracked per resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope 2 — Operational (CPU power × grid intensity):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W = W_idle + (W_max - W_idle) × utilisation
energy_kwh = W × PUE × 730h / 1000
co2e_g = energy_kwh × grid_intensity_gco2e_per_kwh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PUE differs by provider: AWS 1.13, Azure 1.125, GCP 1.10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope 3 — Embodied (hardware manufacturing, prorated):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;embodied_g/month = (1,200,000g / 35,040h / 48 vCPUs) × vcpus × 730h
ARM discount: × 0.80  [Graviton, Ampere Altra, T2A]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Water consumption (data centre cooling):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;water_litres = (W × 730h / 1000) × WUE_litres_per_kwh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;m5.large&lt;/code&gt; in &lt;code&gt;us-east-1&lt;/code&gt; at 50% utilisation, Scope 2 produces 4,313.57g CO2e/month. That value is asserted in &lt;code&gt;engine.test.ts&lt;/code&gt;. You can verify it yourself from &lt;code&gt;factors.json&lt;/code&gt; in under five minutes.&lt;/p&gt;

&lt;p&gt;The full methodology — every formula, every assumption, every data source — is in &lt;code&gt;METHODOLOGY.md&lt;/code&gt; and MIT-licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding it to your workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Terraform Plan&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;terraform init&lt;/span&gt;
    &lt;span class="s"&gt;terraform plan -out=tfplan&lt;/span&gt;
    &lt;span class="s"&gt;terraform show -json tfplan &amp;gt; plan.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreenOps Carbon Lint&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omrdev1/greenops-cli@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plan-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with AWS, Azure, and GCP plans — provider is detected automatically from resource types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: inline suggestion comments
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;post-suggestions: true&lt;/code&gt;, GreenOps posts a one-click committable suggestion directly on the &lt;code&gt;instance_type&lt;/code&gt; / &lt;code&gt;size&lt;/code&gt; / &lt;code&gt;machine_type&lt;/code&gt; line in the PR diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreenOps Carbon Lint&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;omrdev1/greenops-cli@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plan-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;post-suggestions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer clicks "Commit suggestion" and the change is applied without leaving the PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: policy budgets
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;.greenops.yml&lt;/code&gt; to your repo root to block merges that exceed a carbon or cost threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;budgets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;max_pr_co2e_increase_kg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;max_pr_cost_increase_usd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;span class="na"&gt;fail_on_violation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fail_on_violation: true&lt;/code&gt; exits with code 1. The merge is blocked. The PR comment shows which constraint was breached and by how much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EC2, RDS, Azure VMs, and GCP Compute only. No Lambda, no ECS, no AKS.&lt;/li&gt;
&lt;li&gt;71 instance types across the three providers. Unsupported instances are flagged as &lt;code&gt;⚠ UNKNOWN&lt;/code&gt; rather than silently showing zero.&lt;/li&gt;
&lt;li&gt;Provider alias regions may not resolve — affected resources skip with a &lt;code&gt;known_after_apply&lt;/code&gt; reason.&lt;/li&gt;
&lt;li&gt;Annual average grid intensity. No real-time marginal emissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the above are tracked in open issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source the methodology
&lt;/h2&gt;

&lt;p&gt;Every other tool in this space uses proprietary emission factors. That doesn't survive a CSRD audit where a compliance officer needs to trace exactly how a number was produced.&lt;/p&gt;

&lt;p&gt;If you disagree with an assumption, open a PR and change it.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/omrdev1/greenops-cli" rel="noopener noreferrer"&gt;github.com/omrdev1/greenops-cli&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>gcp</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>Saga Engine Go: Type-Safe Distributed Transactions with Zero Infrastructure</title>
      <dc:creator>Grafikui</dc:creator>
      <pubDate>Tue, 27 Jan 2026 21:05:15 +0000</pubDate>
      <link>https://dev.to/grafikui/saga-engine-go-type-safe-distributed-transactions-with-zero-infrastructure-ke2</link>
      <guid>https://dev.to/grafikui/saga-engine-go-type-safe-distributed-transactions-with-zero-infrastructure-ke2</guid>
      <description>&lt;h3&gt;
  
  
  The Go port of Saga Engine. Compile-time step safety via generics, PostgreSQL persistence, and a 15-minute hard limit. No Temporal cluster required.
&lt;/h3&gt;




&lt;p&gt;After shipping &lt;a href="https://dev.to/grafikui/sagas-in-nodejs-without-the-heavy-lifting-introducing-saga-engine-2gf6"&gt;Saga Engine for Node.js&lt;/a&gt;, the most common request was a Go version. Not a wrapper. A native implementation that leverages what Go actually gives you: generics, context propagation, and compile-time safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saga Engine Go&lt;/strong&gt; is that implementation. Same guarantees. Different language idioms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Different from the Node.js Version
&lt;/h2&gt;

&lt;p&gt;This isn't a line-by-line port. Go changes the design in meaningful ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type Safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runtime validation&lt;/td&gt;
&lt;td&gt;Compile-time via &lt;code&gt;Step[T]&lt;/code&gt; generics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AbortController&lt;/code&gt; (optional)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;context.Context&lt;/code&gt; (first-class)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single-threaded event loop&lt;/td&gt;
&lt;td&gt;Goroutines + race detector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step Results&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;any&lt;/code&gt; with runtime checks&lt;/td&gt;
&lt;td&gt;Generic type parameter &lt;code&gt;T&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Go version catches an entire class of bugs at compile time that the Node version can only catch at runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Core API: Generic Steps
&lt;/h2&gt;

&lt;p&gt;Every step is parameterized by its return type. No &lt;code&gt;interface{}&lt;/code&gt; casting. No runtime type assertions.&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;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&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;func&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;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&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="c"&gt;// Step 1: Reserve inventory (returns string)&lt;/span&gt;
    &lt;span class="n"&gt;reservationID&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;saga&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="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="s"&gt;"reserve-inventory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepDefinition&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;IdempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"order-123-inv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="o"&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;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="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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&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;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="o"&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;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;id&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&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;id&lt;/span&gt;&lt;span class="p"&gt;)&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="c"&gt;// Step 2: Charge payment (returns string)&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;saga&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="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="s"&gt;"charge-payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StepDefinition&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;IdempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"order-123-pay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="o"&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;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="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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Charge&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;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="o"&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;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;chargeID&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Refund&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;chargeID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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;The compensate function receives the exact type returned by execute. If &lt;code&gt;Execute&lt;/code&gt; returns &lt;code&gt;(string, error)&lt;/code&gt;, &lt;code&gt;Compensate&lt;/code&gt; receives &lt;code&gt;string&lt;/code&gt;. The compiler enforces this.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Context Mandate
&lt;/h2&gt;

&lt;p&gt;Go's &lt;code&gt;context.Context&lt;/code&gt; is the mechanism for timeout enforcement. The engine cancels the context when deadlines are exceeded. This only works if your functions cooperate.&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;// This respects cancellation&lt;/span&gt;
&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="o"&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;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="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="n"&gt;req&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&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;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&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;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// This ignores cancellation (timeouts become lies)&lt;/span&gt;
&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="o"&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;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="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="n"&gt;resp&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;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&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 15-minute execution limit and per-step timeouts are enforced via context cancellation. If you pass &lt;code&gt;ctx&lt;/code&gt; to every I/O call, it works. If you don't, the engine has no way to interrupt your function.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Hard Guarantees
&lt;/h2&gt;

&lt;p&gt;Same as the Node.js version, enforced at the library level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;Enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required. Returns &lt;code&gt;ErrIdempotencyRequired&lt;/code&gt; if keys are missing at transaction or step level.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State committed to PostgreSQL before the next step executes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pg_try_advisory_lock&lt;/code&gt; prevents double-execution across processes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time-Boxed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15-minute hard limit, checked before every step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failed compensations move to &lt;code&gt;dead_letter&lt;/code&gt; for manual audit via &lt;code&gt;saga-admin&lt;/code&gt; CLI.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. The JSON Serialization Contract
&lt;/h2&gt;

&lt;p&gt;On crash recovery, step results are reconstructed from PostgreSQL via &lt;code&gt;json.Unmarshal&lt;/code&gt;. This means your result types must follow Go's JSON serialization rules:&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;// Works: exported fields survive round-trip&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderResult&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;ID&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`json:"amount"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Broken on resume: unexported fields are silently zeroed&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;badResult&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;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="c"&gt;// json.Unmarshal cannot see this&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 a hard requirement, not a suggestion. If your step returns a struct with unexported fields, those fields will be zero-valued after a crash recovery. The saga will continue with corrupted state.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Error Handling the Go Way
&lt;/h2&gt;

&lt;p&gt;All errors support &lt;code&gt;errors.Is()&lt;/code&gt; and &lt;code&gt;errors.As()&lt;/code&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&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;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrExecutionTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Workflow exceeded 15-minute limit&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;compErr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompensationFailedError&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;As&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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;compErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&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;"Compensation failed at step: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FailedStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;log&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;"Original error: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OriginalError&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;Seven sentinel errors, seven corresponding error types with structured fields. Standard Go error handling, no custom error-checking patterns to learn.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. PgBouncer Compatibility
&lt;/h2&gt;

&lt;p&gt;Advisory locks are session-scoped. This matters for connection pooling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connection Setup&lt;/th&gt;
&lt;th&gt;Compatible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*sql.DB&lt;/code&gt; (direct)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PgBouncer (session mode)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PgBouncer (transaction mode)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you run PgBouncer in transaction mode, lock ownership is lost between queries. The engine won't warn you. Your workflows will silently lose mutual exclusion.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Explicit Refusals
&lt;/h2&gt;

&lt;p&gt;Same philosophy as the Node.js version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No workflows &amp;gt; 15 minutes.&lt;/strong&gt; Use &lt;a href="https://temporal.io" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; for long-running processes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No auto-recovery from dead letters.&lt;/strong&gt; If compensation fails, a human investigates. &lt;code&gt;saga-admin retry &amp;lt;id&amp;gt;&lt;/code&gt; is intentionally manual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No distributed transactions.&lt;/strong&gt; Single-process, single-database. We coordinate side effects; we don't replace your DB's ACID properties.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. CLI: saga-admin
&lt;/h2&gt;

&lt;p&gt;Operational visibility without a dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the CLI&lt;/span&gt;
go build &lt;span class="nt"&gt;-o&lt;/span&gt; saga-admin ./cmd/saga-admin

&lt;span class="c"&gt;# List dead letter workflows&lt;/span&gt;
saga-admin &lt;span class="nt"&gt;-db&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; dead-letter

&lt;span class="c"&gt;# Investigate a specific workflow&lt;/span&gt;
saga-admin &lt;span class="nt"&gt;-db&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; show order-123

&lt;span class="c"&gt;# Retry after fixing the root cause&lt;/span&gt;
saga-admin &lt;span class="nt"&gt;-db&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; retry order-123

&lt;span class="c"&gt;# View aggregate stats&lt;/span&gt;
saga-admin &lt;span class="nt"&gt;-db&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"database/sql"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="n"&gt;saga&lt;/span&gt; &lt;span class="s"&gt;"github.com/grafikui/saga-engine-go"&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/lib/pq"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;db&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;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;storage&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;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPostgresStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transactions"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPostgresLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransactionOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"order-123-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;map&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;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"123"&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;Single PostgreSQL table. No migrations framework required. The schema is in the &lt;a href="https://github.com/grafikui/saga-engine-go#database-schema" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;




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

&lt;p&gt;Saga Engine Go brings the same crash-resilient saga execution to the Go ecosystem. Type-safe generics, context-based cancellation, and a single PostgreSQL dependency.&lt;/p&gt;

&lt;p&gt;If you're already using the Node.js version, the Go port follows the same mental model. If you're new to Saga Engine, pick whichever runtime your services are built on.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pkg.go.dev/github.com/grafikui/saga-engine-go" rel="noopener noreferrer"&gt;Go package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/saga-engine" rel="noopener noreferrer"&gt;Node.js version&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/grafikui/saga-engine-go" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Star on GitHub&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/grafikui/saga-engine-go" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://pkg.go.dev/github.com/grafikui/saga-engine-go" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgres</category>
      <category>microservices</category>
      <category>backend</category>
    </item>
    <item>
      <title>Sagas in Node.js Without the Heavy Lifting: Introducing Saga Engine</title>
      <dc:creator>Grafikui</dc:creator>
      <pubDate>Tue, 27 Jan 2026 15:34:15 +0000</pubDate>
      <link>https://dev.to/grafikui/sagas-in-nodejs-without-the-heavy-lifting-introducing-saga-engine-2gf6</link>
      <guid>https://dev.to/grafikui/sagas-in-nodejs-without-the-heavy-lifting-introducing-saga-engine-2gf6</guid>
      <description>&lt;p&gt;The Saga pattern is a lifesaver for data consistency across microservices, but implementation is often a choice between a mess of nested try/catch blocks or deploying a heavy orchestrator like Temporal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saga Engine&lt;/strong&gt; is a crash-resilient, Postgres-backed executor for Node.js. It provides the core benefits of a distributed transaction coordinator with &lt;strong&gt;zero additional infrastructure&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Logic: Clean, Atomic Workflows
&lt;/h2&gt;

&lt;p&gt;We focus on the "&lt;em&gt;No Magic&lt;/em&gt;" philosophy. You define steps; the engine guarantees execution or compensation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Reserve inventory&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reserve-inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-123-inv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;compensate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: Process payment via internal gateway&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;process-payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-123-pay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;paymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;compensate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;paymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;),&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;&lt;strong&gt;What Happens on Failure?&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;process-payment&lt;/code&gt; fails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;process-payment&lt;/code&gt; execution stops immediately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;reserve-inventory.compensate()&lt;/code&gt; is automatically triggered using the result from its &lt;code&gt;execute&lt;/code&gt; step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The transaction is marked as &lt;code&gt;failed&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  2. Hard Guarantees
&lt;/h2&gt;

&lt;p&gt;These aren't suggestions; they are enforced at the code level.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;Enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required. Throws &lt;code&gt;IdempotencyRequiredError&lt;/code&gt; if keys are missing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;State is persisted to Postgres before the next step starts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres advisory locks prevent double-execution across pods.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time-Boxed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;15-minute hard limit&lt;/strong&gt;. Designed for high-integrity, short-lived workflows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failed compensations move to a &lt;code&gt;dead_letter&lt;/code&gt; state for manual audit.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  3. Explicit Refusals
&lt;/h2&gt;

&lt;p&gt;A professional tool is defined by its boundaries. &lt;strong&gt;Saga Engine&lt;/strong&gt; is not for everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Workflows &amp;gt; 15 Minutes:&lt;/strong&gt; Use Temporal. We provide the upgrade path, not the bloat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Auto-Recovery from Dead Letters:&lt;/strong&gt; If compensation fails, a human must investigate. Manual intervention via &lt;code&gt;npx saga-admin retry&lt;/code&gt; prevents cascading failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Distributed Transactions:&lt;/strong&gt; We coordinate side effects; we do not replace your DB's native ACID properties.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"bin"&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;"saga-admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bin/saga-admin.js"&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;h2&gt;
  
  
  4. How It Compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Saga Engine&lt;/th&gt;
&lt;th&gt;Temporal&lt;/th&gt;
&lt;th&gt;BullMQ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres Only&lt;/td&gt;
&lt;td&gt;Dedicated Cluster&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compensation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native &lt;code&gt;compensate()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Manual try/catch&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 Minutes&lt;/td&gt;
&lt;td&gt;1-2 Weeks&lt;/td&gt;
&lt;td&gt;2 Days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API Orchestration&lt;/td&gt;
&lt;td&gt;Long-running/Complex&lt;/td&gt;
&lt;td&gt;Job Queues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  5. Observability &amp;amp; Retries
&lt;/h2&gt;

&lt;p&gt;Plug into your existing stack with 13 built-in hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onCompensationFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;alerting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rollback failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;onDeadLetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;alerting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Manual intervention required: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&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;If a workflow enters &lt;code&gt;dead_letter&lt;/code&gt; due to a downstream service outage, fix the issue and use the CLI:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx saga-admin retry order-123&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start: Setup
&lt;/h2&gt;

&lt;p&gt;To keep your logic clean, move the initialization to your infrastructure layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PostgresStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PostgresLock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;saga-engine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgresStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostgresLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use this 'tx' instance in your services&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Saga Engine v1.0.0 is production-ready. It fills the gap for teams that need saga semantics without the infrastructure tax.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions? Feedback? Found a bug?&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/Grafikui/saga_engine/issues" rel="noopener noreferrer"&gt;Open an issue on GitHub&lt;/a&gt; or drop a comment below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Grafikui/saga_engine" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Star on GitHub&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/saga-engine" rel="noopener noreferrer"&gt;npm&lt;/a&gt; | &lt;a href="https://github.com/Grafikui/saga_engine" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://github.com/Grafikui/saga_engine/tree/main/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
