install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/events-saga-choreography" ~/.claude/skills/intense-visions-harness-engineering-events-saga-choreography && rm -rf "$T"
manifest:
agents/skills/claude-code/events-saga-choreography/SKILL.mdsource content
Events: Saga Choreography
Coordinate distributed workflows through event chains and compensation events without an orchestrator.
When to Use
- You have a multi-step business transaction spanning multiple services
- You want loose coupling — services react to events without a central coordinator
- You need to roll back partial work using compensation events when a step fails
- Your workflow is relatively linear (A → B → C → done) with clear compensation paths
- Prefer choreography over orchestration for simpler flows; use orchestration for complex branching
Instructions
Order fulfillment saga (choreography):
ORDER CREATED → Payment Service: charge card → PAYMENT_PROCESSED → Inventory Service: reserve stock → INVENTORY_RESERVED → Shipping Service: create shipment → SHIPMENT_CREATED → done On failure at any step → emit compensation event → previous steps compensate: PAYMENT_FAILED → saga ends (nothing to compensate) INVENTORY_FAILED → emit PAYMENT_REFUND_REQUESTED → Payment Service refunds SHIPPING_FAILED → emit INVENTORY_RELEASE_REQUESTED + PAYMENT_REFUND_REQUESTED
Order service — publishes the saga trigger:
// 1. Create order and publish triggering event async function createOrder(data: CreateOrderInput): Promise<Order> { return prisma.$transaction(async (tx) => { const order = await tx.order.create({ data: { ...data, status: 'AWAITING_PAYMENT' }, }); await tx.outbox.create({ data: { eventType: 'order.created', aggregateId: order.id, payload: { eventId: crypto.randomUUID(), orderId: order.id, userId: order.userId, amount: order.total, items: order.items, }, }, }); return order; }); } // Listen for saga completion/failure events consumer.on('order.saga.completed', async ({ orderId }) => { await db.order.update({ where: { id: orderId }, data: { status: 'CONFIRMED' } }); }); consumer.on('order.saga.failed', async ({ orderId, reason }) => { await db.order.update({ where: { id: orderId }, data: { status: 'FAILED', failureReason: reason }, }); });
Payment service — listens and reacts:
consumer.on('order.created', async (event) => { const { orderId, userId, amount } = event; try { const charge = await stripe.charges.create({ amount: Math.round(amount * 100), currency: 'usd', customer: await getStripeCustomerId(userId), }); // Success → publish next saga event await publish('payment.processed', { eventId: crypto.randomUUID(), orderId, chargeId: charge.id, amount, }); } catch (err) { // Failure → publish compensation event await publish('payment.failed', { eventId: crypto.randomUUID(), orderId, reason: (err as Error).message, }); } }); // Compensation — triggered by downstream failure consumer.on('payment.refund.requested', async ({ orderId, chargeId }) => { await stripe.refunds.create({ charge: chargeId }); await publish('payment.refunded', { eventId: crypto.randomUUID(), orderId, chargeId }); });
Inventory service:
consumer.on('payment.processed', async ({ orderId, chargeId, items }) => { try { await reserveInventory(items); await publish('inventory.reserved', { eventId: crypto.randomUUID(), orderId, chargeId }); } catch (err) { // Cannot reserve → trigger payment compensation await publish('inventory.reservation.failed', { eventId: crypto.randomUUID(), orderId, chargeId, reason: (err as Error).message, }); // Payment service listens to this and refunds } }); consumer.on('inventory.release.requested', async ({ orderId, items }) => { await releaseInventory(items); await publish('inventory.released', { eventId: crypto.randomUUID(), orderId }); });
Details
Choreography vs. Orchestration:
| Choreography | Orchestration | |
|---|---|---|
| Control | Distributed (each service reacts) | Centralized (saga orchestrator commands) |
| Coupling | Low — services share events | Higher — orchestrator knows all steps |
| Visibility | Hard — no single view of progress | Easy — orchestrator tracks state |
| Complexity | Simple flows | Complex flows, conditional branches |
Idempotency is mandatory: Each saga step must be idempotent — events can be redelivered. Use event IDs and the
processed_events table (see events-idempotency skill).
Anti-patterns:
- Saga steps that have side effects before the event is committed — use outbox pattern for publishing
- Missing compensation events — if any step doesn't define a compensation, rollback becomes impossible
- Circular compensation chains — A compensates B, B compensates A → infinite loop
Observability challenge: With pure choreography, no single place shows the full saga state. Solutions:
- Correlation IDs on all events (same
across all events)sagaId - A saga tracker service that listens to all events and builds a state view
- Structured logging with the
field for log aggregation queriessagaId
Source
microservices.io/patterns/data/saga.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.