惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Forbes - Security
Forbes - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Palo Alto Networks Blog
Martin Fowler
Martin Fowler
T
Threatpost
D
Docker
S
Schneier on Security
M
MIT News - Artificial intelligence
G
Google Developers Blog
L
LINUX DO - 热门话题
J
Java Code Geeks
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
V
Vulnerabilities – Threatpost
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
人人都是产品经理
人人都是产品经理
Spread Privacy
Spread Privacy
T
Tailwind CSS Blog
爱范儿
爱范儿
阮一峰的网络日志
阮一峰的网络日志
U
Unit 42
C
CERT Recently Published Vulnerability Notes
The GitHub Blog
The GitHub Blog
Simon Willison's Weblog
Simon Willison's Weblog
NISL@THU
NISL@THU
MongoDB | Blog
MongoDB | Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Heimdal Security Blog
Recorded Future
Recorded Future
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
P
Privacy International News Feed
P
Proofpoint News Feed
O
OpenAI News
B
Blog
腾讯CDC
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
T
Tor Project blog
H
Hacker News: Front Page
Project Zero
Project Zero
Hugging Face - Blog
Hugging Face - Blog
C
Cisco Blogs
S
Security Affairs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Saga orchestrator in Python
Diego Guerrero · 2026-06-25 · via DEV Community

Building a Saga orchestrator in Python: why existing tools weren't enough and what I learned designing one from scratch

Distributed workflows break in a specific, painful way. You charge a payment,
reserve inventory, then try to create a shipping label — and the shipping API
times out. The payment went through. The inventory is reserved. But the order
never completed.

Now what?

The naive fix is nested try/except blocks with manual cleanup calls. Every
developer has written this code. It works until the cleanup call fails, or the
process crashes between steps, or two workers process the same message
simultaneously. Then you have inconsistent state, angry customers, and no
clear path to recovery.

# The naive approach everyone writes first
async def process_order(order):
    try:
        payment = await charge_payment(order)
        try:
            reservation = await reserve_inventory(order)
            try:
                shipping = await ship_order(order)
            except Exception:
                await release_inventory(reservation)  # what if THIS fails?
                raise
        except Exception:
            await refund_payment(payment)  # what if THIS fails?
            raise
    except Exception:
        # now what?
        raise

sagakit solves this with the Saga pattern: each step in a workflow declares an
explicit compensation handler. If a later step fails, sagakit automatically
runs the compensations in reverse order — releasing inventory, refunding the
payment — with retries, idempotency guarantees, and structured logging built
in. One docker run redis is all the infrastructure you need.


Why not existing tools?

Before building sagakit, I looked at the existing options.

Temporal is the gold standard for durable workflow orchestration. It
handles long-running workflows, versioning, and failure recovery with
guarantees that sagakit doesn't offer. But it requires a dedicated cluster, a
separate worker process, and significant operational investment. For a Python
backend engineer who needs to coordinate 3-5 steps reliably, it's a freight
train for a commute.

Celery is designed for task queues — background jobs, periodic tasks,
fan-out processing. It doesn't have a native model for compensating
transactions. You can approximate it, but you end up building the compensation
logic yourself anyway, without the primitives to do it safely.

Manual try/except is what everyone writes first (see the code above). It
fails silently when the cleanup call itself fails, offers no protection against
duplicate processing when a message is redelivered, and becomes unmaintainable
past three steps.

I wanted something that required only Redis — which most Python backends
already run — made compensation logic explicit and co-located with the step it
undoes, and felt like idiomatic async Python. That's sagakit.


Three design decisions worth explaining

1. Orchestration over choreography

Sagas can be implemented two ways. In choreography, each service reacts
autonomously to events — payment-service publishes payment.charged,
inventory-service listens and reserves, and so on. No central coordinator
exists.

sagakit uses orchestration: a central executor drives each step explicitly, in
order. The entire workflow is defined in one place and readable top to bottom.

The tradeoff is real — orchestration introduces a coordinator that choreography
avoids. But for a library targeting clarity and testability, having the
workflow in one file is worth it. You can read a saga definition and
immediately understand what it does, what it compensates, and in what order.
With choreography, that understanding is reconstructed from events scattered
across services.

2. Idempotency keys don't include the attempt number

The original design used saga_id:step_name:attempt_number as the idempotency
key. It seemed logical — each attempt is a distinct event, so each gets a
distinct key.

The problem: if charge_payment fails on attempt 1 and retries as attempt 2,
the key changes. When the step passes ctx.idempotency_key to Stripe, Stripe
sees two different keys and treats them as two separate charges. The customer
gets billed twice.

The fix was simple but required changing the design mid-project: drop
attempt_number from the key. All retries of the same step share
saga_id:step_name. Stripe — and any other external system that accepts an
idempotency key — sees the same identifier across all attempts and returns the
same result without re-executing the side effect.

attempt_number still exists in SagaContext for logging and observability.
It just doesn't affect the key.

3. reject() uses XACK + XADD instead of XCLAIM

When a step fails and needs to be retried, the message must be returned to the
stream for reprocessing. The obvious Redis primitive is XCLAIM — reassign the
message to a consumer so it can retry.

The problem: XCLAIM keeps the message tied to the original consumer. If that
consumer is down, the message sits in the Pending Entries List indefinitely.
No other worker can claim it. The saga is silently stuck.

sagakit uses XACK + XADD instead. The original message is acknowledged
(removed from the PEL), and a new message with the same payload is published
to the stream. Any available consumer in the group can pick it up — the retry
is not tied to the worker that originally failed.

The re-published message carries a requeue_count attribute incremented on
each rejection, so the executor can detect pathological retry loops and route
to the DLQ after a threshold.


Seeing it in action

The happy path — all three steps complete successfully:

[info] payment.charged     amount=99.99 payment_id=pay_718836cb
[info] inventory.reserved  reservation_id=res_718836cb
[info] order.shipped       tracking_id=trk_718836cb

Status  : completed
Results :
  charge_payment    → pay_718836cb
  reserve_inventory → res_718836cb
  ship_order        → trk_718836cb

The failure path — ship_order fails, sagakit retries with exponential
backoff, exhausts attempts, then compensates in reverse order:

[info]    payment.charged     amount=99.99 payment_id=pay_6856bd2b
[info]    inventory.reserved  reservation_id=res_6856bd2b
[warning] step.retrying       attempt=1 delay=0.052s error='Shipping API down'
[warning] step.retrying       attempt=2 delay=0.170s error='Shipping API down'
[error]   step.exhausted_retries attempts=3 error='Shipping API down'
[info]    inventory.released  ← compensation
[info]    payment.refunded    ← compensation

Status    : compensated
Failed at : ship_order
Rolled back: reserve_inventory, charge_payment

Two things worth noticing in the failure output. First, the backoff is visible
in the logs — 52ms, then 170ms, with ±50% jitter applied. Second,
compensations run in strict reverse order: inventory before payment, because
that's the reverse of how they were acquired. sagakit guarantees this order
regardless of which step fails.

To reproduce: FAIL_AT_STEP=ship_order python run.py


Three things I learned building this

Compensation is not rollback

Coming from a relational database background, my first mental model of
compensation was "undo" — run the saga backwards and erase what happened.
That's not what compensation is.

A database rollback destroys state retroactively. It's as if the transaction
never happened — no trace, no intermediate state, no customer-visible effect.

Compensation is new forward-moving business logic that repairs the damage.
Refunding a payment is not the same as never charging it. The customer sees
two transactions on their bank statement. The inventory system received a
reservation and then a cancellation — it may have allocated physical space in
the interim. Other systems may have reacted to the original action before the
compensation ran.

This distinction has real consequences for how you write compensation handlers.
They cannot assume the system is in the same state it was when the forward step
ran. They must be written defensively, assuming that time has passed and other
things have happened.

Design decisions reveal their flaws during implementation

The idempotency key started as saga_id:step_name:attempt_number. It seemed
correct on paper — each attempt is a distinct event, so each gets a distinct
identifier.

The flaw only became visible when I thought through the implementation
concretely: if the key changes between retries, external systems like Stripe
see different keys and treat each attempt as a new transaction. A payment gets
charged twice.

The fix was one line of code. But it required updating the ADR, the
implementation, and the tests — and more importantly, it required catching the
assumption before it shipped.

This is why I wrote Architecture Decision Records before writing code. The ADR
for idempotency forced me to think through the key construction explicitly,
which is when the flaw surfaced. Without that document, the bug would have
lived in the code until a real payment was doubled.

Writing the decision before writing the code

I had never written an Architecture Decision Record before this project. My
previous approach was the common one: make a decision, write the code, maybe
add a comment explaining why.

The difference with ADRs is that you document not just what you decided, but
what you considered and rejected — and why. That forces a level of rigor that
commenting doesn't. You can't write "rejected because operationally heavy"
without first asking yourself: heavy compared to what? Heavy for whom?

Four ADRs later, the project has a paper trail of every major architectural
choice: why Sagas over 2PC, why Redis Streams over Kafka, how idempotency
works, what compensation guarantees are provided and which aren't. A new
contributor — or a future version of me — can read those documents and
understand not just what the system does, but why it exists in this shape.

I won't build a non-trivial system without them again.


Try it yourself

sagakit is pre-alpha — the API may change, and it hasn't been benchmarked in
production. But it works, it's tested, and the order-processing example runs
in under five minutes with a single docker compose up -d.

If you're building event-driven workflows in Python and the naive try/except
approach is starting to hurt, give it a try and let me know what breaks.

github.com/diegogue88/sagakit

What patterns do you use for distributed transactions in Python? I'd love to
hear what's working — and what isn't — in the comments.