--- id: wiki-2026-0508-workflow-integrity title: Workflow Integrity category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Workflow Integrity, Pipeline Integrity, Process Integrity] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [workflow, integrity, reliability, idempotency, saga, durable-execution] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: Temporal / Inngest / Restate --- # Workflow Integrity ## 매 한 줄 > **"매 step 의 fail 해도, 매 workflow 의 correct"**. 매 distributed workflow 의 integrity property — 매 idempotency, 매 atomicity, 매 ordering, 매 exactly-once effect 의 통한 매 partial failure 의 resilience. 매 2026 의 표준 tooling: Temporal / Inngest / Restate / Cloudflare Durable Objects — 매 durable execution 의 paradigm. ## 매 핵심 ### 매 integrity properties - **Idempotency**: 매 same input 의 retry 가 매 single effect. - **Atomicity**: 매 step 의 all-or-nothing — 매 partial commit 의 X. - **Ordering**: 매 causally-dependent step 의 sequence 의 preserve. - **Exactly-once effect**: 매 at-least-once delivery + idempotent handler. - **Compensation**: 매 saga — 매 forward fail 의 rollback action. ### 매 failure modes - **Crash mid-step**: 매 worker 의 die — 매 durable log 의 resume. - **Duplicate delivery**: 매 message broker 의 redeliver — 매 idempotency key 의 dedupe. - **Out-of-order**: 매 parallel branch 의 racing — 매 versioned write. - **Network partition**: 매 split-brain — 매 fencing token 의 exclude stale leader. ### 매 응용 1. 매 payment processing — 매 charge + ledger + email 의 saga. 2. 매 onboarding workflow — 매 user create + Stripe customer + Slack notify. 3. 매 ETL pipeline — 매 extract + transform + load 의 retry safe. 4. 매 LLM agent loop — 매 tool call 의 durable resume. ## 💻 패턴 ### Idempotency key (HTTP) ```typescript import { Hono } from "hono"; const app = new Hono(); const seen = new Map(); // 매 prod 의 Redis app.post("/charge", async (c) => { const key = c.req.header("Idempotency-Key"); if (!key) return c.json({ error: "Idempotency-Key required" }, 400); const cached = seen.get(key); if (cached) return c.json(cached.body, cached.status); const result = await stripe.charges.create(await c.req.json()); seen.set(key, { status: 200, body: result }); return c.json(result); }); ``` ### Temporal workflow (durable execution) ```typescript import { proxyActivities, defineSignal, setHandler } from "@temporalio/workflow"; const { chargeCard, sendEmail, recordLedger } = proxyActivities({ startToCloseTimeout: "30s", retry: { maximumAttempts: 5, initialInterval: "1s", backoffCoefficient: 2 }, }); export async function processOrderWorkflow(orderId: string, amount: number) { // 매 step 1: charge — 매 retry 의 idempotent (Stripe idempotency key) const charge = await chargeCard({ orderId, amount }); // 매 step 2: ledger — 매 fail 시 charge 의 refund (compensation) try { await recordLedger({ orderId, chargeId: charge.id, amount }); } catch (err) { await proxyActivities({ ...defaults, scheduleToCloseTimeout: "1m" }) .refundCard({ chargeId: charge.id }); throw err; } // 매 step 3: email — 매 fail 의 non-blocking (best effort) await sendEmail({ orderId }).catch(() => {}); return { orderId, chargeId: charge.id }; } ``` ### Saga compensation pattern ```typescript class Saga { private compensations: Array<() => Promise> = []; async run(forward: () => Promise, backward: (result: T) => Promise): Promise { const result = await forward(); this.compensations.push(() => backward(result)); return result; } async rollback() { for (const comp of this.compensations.reverse()) { try { await comp(); } catch (e) { logger.error("매 compensation failed", e); } } } } async function bookTrip() { const saga = new Saga(); try { const flight = await saga.run(() => bookFlight(), f => cancelFlight(f.id)); const hotel = await saga.run(() => bookHotel(), h => cancelHotel(h.id)); const car = await saga.run(() => bookCar(), c => cancelCar(c.id)); return { flight, hotel, car }; } catch (err) { await saga.rollback(); throw err; } } ``` ### Inngest function (event-driven durable) ```typescript import { inngest } from "./client"; export const onUserSignup = inngest.createFunction( { id: "user-signup", retries: 3 }, { event: "user/created" }, async ({ event, step }) => { const customer = await step.run("create-stripe-customer", () => stripe.customers.create({ email: event.data.email }) ); await step.run("save-customer-id", () => db.user.update({ where: { id: event.data.userId }, data: { stripeId: customer.id } }) ); await step.sleep("wait-1h", "1h"); await step.run("send-welcome-email", () => resend.emails.send({ to: event.data.email, subject: "환영합니다", html: "..." }) ); } ); ``` ### Outbox pattern (transactional message) ```typescript async function createOrderWithOutbox(order: Order) { await db.$transaction([ db.order.create({ data: order }), db.outbox.create({ data: { topic: "order.created", payload: order, status: "pending", }}), ]); // 매 separate worker 의 outbox 의 poll, 매 message broker 의 publish } // Worker async function flushOutbox() { const pending = await db.outbox.findMany({ where: { status: "pending" }, take: 100 }); for (const row of pending) { await broker.publish(row.topic, row.payload); await db.outbox.update({ where: { id: row.id }, data: { status: "sent", sentAt: new Date() } }); } } ``` ### Fencing token (split-brain prevention) ```typescript async function acquireLockWithFencing(key: string, ttl: number): Promise { const token = await redis.incr(`fencing:${key}`); // 매 monotonic const ok = await redis.set(`lock:${key}`, token, "PX", ttl, "NX"); if (!ok) throw new Error("매 lock held"); return token; } async function writeWithFence(key: string, value: unknown, fenceToken: number) { // 매 storage layer 의 fence token 의 check — 매 stale leader 의 reject. await db.execute(sql` UPDATE resource SET value = ${value}, fence = ${fenceToken} WHERE key = ${key} AND fence < ${fenceToken} `); } ``` ### Exactly-once via dedupe table ```sql CREATE TABLE message_dedupe ( message_id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 매 handler 의 inside transaction INSERT INTO message_dedupe(message_id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING message_id; -- 매 RETURNING 의 empty 면 already processed. ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | 매 simple webhook | Idempotency key + dedupe table | | 매 multi-step business workflow | Temporal / Inngest — durable execution | | 매 cross-service transaction | Saga + compensation | | 매 transactional msg dispatch | Outbox pattern mandatory | | 매 leader election + write | Fencing token mandatory | **기본값**: Inngest (small / event-driven) → Temporal (large / mission-critical) → Saga (cross-service). ## 🔗 Graph - 부모: [[Distributed Systems]] - 변형: [[Saga]] - 응용: [[Agent_Loop]] - Adjacent: [[Idempotency]] · [[Durable_Execution]] · [[Event Sourcing Pattern|Event_Sourcing]] · [[CQRS]] ## 🤖 LLM 활용 **언제**: 매 workflow design 의 review, 매 failure mode enumeration, 매 saga compensation 의 draft. **언제 X**: 매 fencing token 의 storage check 의 hand-write — 매 race subtle. ## ❌ 안티패턴 - **Retry without idempotency**: 매 double-charge. - **Atomicity by hope**: 매 try/catch 의 의지 — 매 crash 의 ignore. - **Ordering via timestamp**: 매 clock skew — 매 logical clock 의 use. - **Saga without compensation**: 매 partial state forever. - **Outbox 의 skip**: 매 DB commit + publish 의 separate — 매 lost message. ## 🧪 검증 / 중복 - Verified (Temporal docs, Inngest engineering blog, Pat Helland "Life beyond Distributed Transactions"). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full content with idempotency/Temporal/saga/outbox/fencing patterns |