Advanced Patterns

Saga Pattern for Distributed Transactions

The Saga pattern manages distributed transactions through a sequence of local operations, each with compensating actions for failure scenarios. This is particularly valuable for maintaining consistency across distributed components.

// Create a saga coordinator
const orderSaga = createNode("order-saga");

// Start saga for order processing
orderSaga.on("process-order", async (event) => {
  const { orderId } = event.payload;
  const context = { orderId, steps: [] };
  
  try {
    // Step 1: Reserve inventory
    const inventoryResult = await orderSaga.send(inventoryNode, {
      type: "reserve-inventory",
      payload: event.payload
    }).return();
    
    if (!inventoryResult.success) {
      throw new Error(`Inventory reservation failed: ${inventoryResult.reason}`);
    }
    
    context.steps.push("inventory-reserved");
    
    // Step 2: Process payment
    const paymentResult = await orderSaga.send(paymentNode, {
      type: "process-payment",
      payload: event.payload
    }).return();
    
    if (!paymentResult.success) {
      throw new Error(`Payment failed: ${paymentResult.reason}`);
    }
    
    context.steps.push("payment-processed");
    context.paymentId = paymentResult.paymentId;
    
    // Step 3: Create shipment
    const shipmentResult = await orderSaga.send(shipmentNode, {
      type: "create-shipment",
      payload: {
        orderId,
        items: event.payload.items,
        address: event.payload.shippingAddress
      }
    }).return();
    
    if (!shipmentResult.success) {
      throw new Error(`Shipment creation failed: ${shipmentResult.reason}`);
    }
    
    // Complete the saga
    orderSaga.broadcast({
      type: "order-processed",
      payload: {
        orderId,
        status: "processed",
        shipmentId: shipmentResult.shipmentId
      }
    });
    
    return { 
      success: true, 
      orderId,
      shipmentId: shipmentResult.shipmentId
    };
    
  } catch (error) {
    // Execute compensating transactions in reverse order
    if (context.steps.includes("payment-processed")) {
      await orderSaga.send(paymentNode, {
        type: "refund-payment",
        payload: {
          paymentId: context.paymentId
        }
      }).return();
    }
    
    if (context.steps.includes("inventory-reserved")) {
      await orderSaga.send(inventoryNode, {
        type: "release-inventory",
        payload: event.payload
      }).return();
    }
    
    // Saga failed
    orderSaga.broadcast({
      type: "order-processing-failed",
      payload: {
        orderId,
        reason: error.message
      }
    });
    
    return { 
      success: false, 
      reason: error.message
    };
  }
});

This implementation shows how Happen's event-driven model naturally supports complex transactional scenarios that span multiple services.

Retry Pattern with Exponential Backoff

The Retry pattern handles transient failures by automatically retrying operations with increasing delays between attempts.

// Create a service node with retry capability
const paymentServiceNode = createNode("payment-service");

// Process payment with retry logic
paymentServiceNode.on("process-payment", async function processPayment(event, context) {
  // Initialize retry context if not exists
  context.retryCount = context.retryCount || 0;
  context.startTime = context.startTime || Date.now();
  
  try {
    // Attempt payment processing
    const result = await chargeCustomer(event.payload);
    
    // Success - return result
    return {
      success: true,
      transactionId: result.transactionId,
      amount: event.payload.amount,
      processedAt: Date.now()
    };
    
  } catch (error) {
    // Store error in context
    context.lastError = error;
    context.retryCount++;
    
    // Determine if we should retry
    const maxRetries = 5;
    const maxDuration = 2 * 60 * 1000; // 2 minutes
    const timeElapsed = Date.now() - context.startTime;
    
    if (context.retryCount < maxRetries && timeElapsed < maxDuration) {
      // Calculate exponential backoff delay
      const delay = Math.min(100 * Math.pow(2, context.retryCount), 5000);
      
      // Log retry attempt
      console.log(`Retrying payment (${context.retryCount}/${maxRetries}) after ${delay}ms delay`);
      
      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, delay));
      
      // Return self to retry
      return processPayment;
    }
    
    // Too many retries or timeout exceeded
    return {
      success: false,
      reason: "payment-failed-after-retries",
      attempts: context.retryCount,
      lastError: context.lastError.message
    };
  }
});

This pattern demonstrates Happen's function-returning mechanism to create sophisticated retry logic with minimal code.

Circuit Breaker Pattern

The Circuit Breaker pattern prevents cascading failures by stopping operations that are likely to fail.

// Create a node with circuit breaker capability
const apiGatewayNode = createNode("api-gateway");

// Circuit state storage
const circuits = {
  payment: {
    state: "closed", // closed, open, half-open
    failures: 0,
    lastFailure: null,
    resetTimeout: null
  }
};

// Handler with circuit breaker logic
apiGatewayNode.on("call-payment-service", function processPaymentRequest(event, context) {
  const circuit = circuits.payment;
  
  // Check circuit state
  if (circuit.state === "open") {
    // Circuit is open - fail fast
    return {
      success: false,
      reason: "circuit-open",
      message: "Payment service is unavailable"
    };
  }
  
  // Process request with circuit awareness
  return makePaymentServiceCall;
});

// Function to make the actual service call
async function makePaymentServiceCall(event, context) {
  const circuit = circuits.payment;
  
  try {
    // Attempt to call the payment service
    const result = await callPaymentService(event.payload);
    
    // Success - reset circuit if needed
    if (circuit.state === "half-open") {
      circuit.state = "closed";
      circuit.failures = 0;
    }
    
    // Return successful result
    return result;
    
  } catch (error) {
    // Update circuit on failure
    circuit.failures++;
    circuit.lastFailure = Date.now();
    
    // Check if we should open the circuit
    if (circuit.state === "closed" && circuit.failures >= 5) {
      // Open the circuit
      circuit.state = "open";
      
      // Schedule reset to half-open
      circuit.resetTimeout = setTimeout(() => {
        circuit.state = "half-open";
      }, 30000); // 30 second timeout
    }
    
    // Return error with circuit status
    return {
      success: false,
      reason: "payment-service-error",
      message: error.message,
      circuitState: circuit.state
    };
  }
}

This pattern shows how Happen can be extended to implement sophisticated resilience patterns that protect systems from cascading failures.

Supervisor Pattern

The Supervisor pattern manages component lifecycles and handles failures through a hierarchical oversight structure.

// Create a supervisor node
const supervisorNode = createNode("system-supervisor");

// Register handler for all error events
supervisorNode.on(type => type.endsWith(".error"), (event) => {
  // Extract service name from event type
  const service = event.type.split('.')[0];
  
  // Update service state
  supervisorNode.state.set(state => {
    const services = state.services || {};
    const serviceState = services[service] || {
      errors: 0,
      lastError: 0,
      lastReset: 0,
      restarts: 0
    };
    
    // Update error count
    const updatedServiceState = {
      ...serviceState,
      errors: serviceState.errors + 1,
      lastError: Date.now()
    };
    
    // Return updated state
    return {
      ...state,
      services: {
        ...services,
        [service]: updatedServiceState
      }
    };
  });
  
  // Get updated service state
  const serviceState = supervisorNode.state.get(state => 
    (state.services && state.services[service]) || { errors: 0, lastReset: 0 }
  );
  
  // Check restart threshold
  if (serviceState.errors >= 5 && Date.now() - serviceState.lastReset < 60000) {
    // Service is failing too frequently - trigger restart
    supervisorNode.broadcast({
      type: "service.restart-required",
      payload: {
        service,
        reason: "excessive-errors",
        errorCount: serviceState.errors
      }
    });
  }
  
  return { supervised: true };
});

// Handle service restart
supervisorNode.on("service.restart-required", (event) => {
  const { service } = event.payload;
  
  console.log(`Restarting service: ${service}`);
  
  // Broadcast restart event
  supervisorNode.broadcast({
    type: "infrastructure.restart-service",
    payload: {
      service,
      triggeredBy: "supervisor",
      reason: event.payload.reason
    }
  });
  
  // Update service state
  supervisorNode.state.set(state => {
    const services = state.services || {};
    const serviceState = services[service] || {};
    
    return {
      ...state,
      services: {
        ...services,
        [service]: {
          ...serviceState,
          errors: 0,
          lastReset: Date.now(),
          restarts: (serviceState.restarts || 0) + 1
        }
      }
    };
  });
  
  return { action: "restart", service };
});

This pattern demonstrates Happen's ability to implement hierarchical oversight and automated recovery mechanisms using its core primitives.

State Machine Workflow

The State Machine pattern models entities that transition between discrete states according to well-defined rules.

// Create a state machine node
const orderStateMachine = createNode("order-state-machine");

// Define allowed transitions
const allowedTransitions = {
  "draft": ["submitted"],
  "submitted": ["processing", "canceled"],
  "processing": ["shipped", "canceled"],
  "shipped": ["delivered"],
  "canceled": []
};

// Process transition requests
orderStateMachine.on("transition-order", (event) => {
  const { orderId, toState } = event.payload;
  
  // Get current order state
  orderStateMachine.state.get(state => {
    const order = (state.orders || {})[orderId];
    
    if (!order) {
      return {
        success: false,
        reason: "order-not-found"
      };
    }
    
    // Check if transition is allowed
    const currentState = order.status;
    if (!allowedTransitions[currentState]?.includes(toState)) {
      return {
        success: false,
        reason: "invalid-transition",
        message: `Cannot transition from ${currentState} to ${toState}`
      };
    }
    
    // Apply the transition
    orderStateMachine.state.set(state => {
      const orders = state.orders || {};
      
      return {
        ...state,
        orders: {
          ...orders,
          [orderId]: {
            ...order,
            status: toState,
            transitionedAt: Date.now(),
            previousStatus: currentState
          }
        }
      };
    });
    
    // Broadcast state change event
    orderStateMachine.broadcast({
      type: "order-state-changed",
      payload: {
        orderId,
        fromState: currentState,
        toState,
        timestamp: Date.now()
      }
    });
    
    return {
      success: true,
      orderId,
      currentState: toState
    };
  });
});

This pattern shows how Happen's state management capabilities can be used to implement formal state machines with controlled transitions.

Reactive Aggregates

The Reactive Aggregates pattern combines event-driven reactivity with consistent state management for domain aggregates.

// Create an aggregate node
const customerAggregate = createNode("customer-aggregate");

// React to events and update aggregate state
customerAggregate.on("customer-registered", (event) => {
  const { customerId, email, name } = event.payload;
  
  // Create new aggregate state
  customerAggregate.state.set(state => {
    const customers = state.customers || {};
    
    return {
      ...state,
      customers: {
        ...customers,
        [customerId]: {
          id: customerId,
          email,
          name,
          status: "active",
          registeredAt: Date.now(),
          orders: []
        }
      }
    };
  });
  
  return { updated: true };
});

// Add order to customer aggregate
customerAggregate.on("order-created", (event) => {
  const { orderId, customerId, amount } = event.payload;
  
  // Update aggregate state
  customerAggregate.state.set(state => {
    const customers = state.customers || {};
    const customer = customers[customerId];
    
    if (!customer) {
      return state; // Customer not found, state unchanged
    }
    
    return {
      ...state,
      customers: {
        ...customers,
        [customerId]: {
          ...customer,
          orders: [
            ...customer.orders,
            {
              id: orderId,
              amount,
              createdAt: Date.now()
            }
          ],
          totalSpent: (customer.totalSpent || 0) + amount,
          lastOrderAt: Date.now()
        }
      }
    };
  });
  
  // Check if customer qualifies for premium status
  const customer = customerAggregate.state.get(state => 
    (state.customers || {})[customerId]
  );
  
  if (customer && customer.orders.length >= 5 && customer.status !== "premium") {
    // Upgrade to premium status
    customerAggregate.state.set(state => {
      const customers = state.customers || {};
      
      return {
        ...state,
        customers: {
          ...customers,
          [customerId]: {
            ...customers[customerId],
            status: "premium",
            upgradedAt: Date.now()
          }
        }
      };
    });
    
    // Broadcast premium status event
    customerAggregate.broadcast({
      type: "customer-premium-status-achieved",
      payload: {
        customerId,
        ordersCount: customer.orders.length,
        totalSpent: customer.totalSpent + amount
      }
    });
  }
  
  return { updated: true };
});

// Command handler for direct interactions with the aggregate
customerAggregate.on("update-customer-email", (event) => {
  const { customerId, email } = event.payload;
  
  // Validate customer exists
  const customer = customerAggregate.state.get(state => 
    (state.customers || {})[customerId]
  );
  
  if (!customer) {
    return {
      success: false,
      reason: "customer-not-found"
    };
  }
  
  // Update email
  customerAggregate.state.set(state => {
    const customers = state.customers || {};
    
    return {
      ...state,
      customers: {
        ...customers,
        [customerId]: {
          ...customers[customerId],
          email,
          emailUpdatedAt: Date.now()
        }
      }
    };
  });
  
  // Broadcast email changed event
  customerAggregate.broadcast({
    type: "customer-email-changed",
    payload: {
      customerId,
      previousEmail: customer.email,
      newEmail: email
    }
  });
  
  return {
    success: true,
    customerId
  };
});

This pattern demonstrates how Happen can combine reactive event handling with consistent state management in domain-driven designs.

Last updated