A Few Design Patterns

Event Sourcing Pattern

The Event Sourcing pattern stores all changes to an application state as a sequence of events, making it possible to reconstruct past states and provide a complete audit trail. Happen's causality-focused approach makes this pattern particularly elegant.

// Create an event store node
const eventStore = createNode("event-store");

// Store domain events
eventStore.on(type => type.startsWith("domain-"), (event) => {
  // Store event in append-only log
  storeEvent(event);
  return { stored: true };
});

// Function to reconstruct state from events
function rebuildState() {
  let state = initialState();
  for (const event of retrieveEvents()) {
    // Apply each event to evolve the state
    state = applyEvent(state, event);
  }
  return state;
}

// Function to get state at a specific point in time
function getStateAt(timestamp) {
  let state = initialState();
  for (const event of retrieveEvents()) {
    if (event.metadata.timestamp <= timestamp) {
      state = applyEvent(state, event);
    }
  }
  return state;
}

This pattern leverages Happen's natural event flow to create an immutable record of all domain changes, enabling powerful historical analysis and debugging.

Command Query Responsibility Segregation (CQRS)

CQRS separates operations that modify state (commands) from operations that read state (queries), allowing each to be optimized independently.

// Command handling node
const orderCommandNode = createNode("order-commands");

// Process commands that modify state
orderCommandNode.on("create-order", (event) => {
  // Validate command
  validateOrderData(event.payload);
  
  // Execute command logic
  const orderId = generateOrderId();
  
  // Emit domain event representing the result
  orderCommandNode.broadcast({
    type: "domain-order-created",
    payload: { orderId, ...event.payload, createdAt: Date.now() }
  });
  
  return { success: true, orderId };
});

// Query handling node
const orderQueryNode = createNode("order-queries");

// Update query model when domain events occur
orderQueryNode.on("domain-order-created", (event) => {
  // Update query-optimized state
  orderQueryNode.state.set(state => {
    const orders = state.orders || {};
    return {
      ...state,
      orders: {
        ...orders,
        [event.payload.orderId]: { 
          ...event.payload, 
          status: "pending" 
        }
      }
    };
  });
  
  return { updated: true };
});

// Handle queries
orderQueryNode.on("get-order", (event) => {
  const { orderId } = event.payload;
  // Return query result directly
  return orderQueryNode.state.get(state => state.orders?.[orderId] || null);
});

This pattern showcases Happen's ability to separate different concerns (writing vs. reading) while maintaining the causal relationships between them.

Observer Pattern

The observer pattern lets nodes observe and react to events without the sender needing to know about the observers, promoting loose coupling and modular design.

// Subject node that emits events
const userNode = createNode("user-service");

// Process user profile updates and notify observers
userNode.on("update-user-profile", (event) => {
  const { userId, profileData } = event.payload;
  
  // Update the user profile
  userNode.state.set(state => {
    const users = state.users || {};
    return {
      ...state,
      users: {
        ...users,
        [userId]: {
          ...(users[userId] || {}),
          ...profileData,
          updatedAt: Date.now()
        }
      }
    };
  });
  
  // Emit an event for observers
  userNode.broadcast({
    type: "user-profile-updated",
    payload: {
      userId,
      updatedFields: Object.keys(profileData),
      timestamp: Date.now()
    }
  });
  
  return { success: true };
});

// Observer nodes that react to events
const notificationNode = createNode("notification-service");
const analyticsNode = createNode("analytics-service");
const searchIndexNode = createNode("search-index-service");

// Each observer reacts independently
notificationNode.on("user-profile-updated", (event) => {
  // Send notification about profile update
  if (event.payload.updatedFields.includes("email")) {
    sendEmailChangeNotification(event.payload.userId);
  }
  return { notified: true };
});

analyticsNode.on("user-profile-updated", (event) => {
  // Track profile update for analytics
  recordProfileUpdateMetrics(event.payload);
  return { tracked: true };
});

searchIndexNode.on("user-profile-updated", (event) => {
  // Update search index with new profile data
  refreshUserSearchIndex(event.payload.userId);
  return { indexed: true };
});

This pattern demonstrates Happen's natural support for decoupled, event-driven architectures where components can react to system events without direct dependencies.

Strategy Pattern

The strategy pattern allows selecting an algorithm at runtime, which Happen implements naturally through event handlers and dynamic routing.

// Create a pricing strategy node
const pricingStrategyNode = createNode("pricing-strategy");

// Handle pricing requests with different strategies
pricingStrategyNode.on("calculate-price", (event) => {
  const { items, strategy, customerInfo } = event.payload;
  
  // Select strategy based on the event payload
  let total;
  
  switch(strategy) {
    case "volume-discount":
      total = calculateVolumeDiscount(items);
      break;
    case "premium-customer":
      total = calculatePremiumPrice(items, customerInfo?.tier);
      break;
    case "regular":
    default:
      total = calculateRegularPrice(items);
  }
  
  // Return pricing result
  return { 
    total, 
    appliedStrategy: strategy, 
    calculatedAt: Date.now() 
  };
});

// Strategy implementations
function calculateRegularPrice(items) {
  return items.reduce((total, item) => total + item.price * item.quantity, 0);
}

function calculateVolumeDiscount(items) {
  let total = 0;
  for (const item of items) {
    let price = item.price;
    if (item.quantity > 10) {
      // 10% discount
      price = price * 0.9;
    }
    total += price * item.quantity;
  }
  return total;
}

function calculatePremiumPrice(items, customerTier) {
  const baseTotal = calculateRegularPrice(items);
  const discountRate = customerTier === "gold" ? 0.15 : 0.1;
  return baseTotal * (1 - discountRate);
}

This pattern shows how Happen supports dynamic behavior selection without requiring complex class hierarchies or inheritance.

Mediator Pattern

The mediator pattern provides centralized coordination between multiple components, reducing direct dependencies and simplifying complex workflows.

// Create a mediator node
const workflowMediator = createNode("workflow-mediator");

// Handle workflow initiation
workflowMediator.on("start-onboarding", async (event) => {
  const { userId } = event.payload;
  
  // Step 1: Create user account
  const accountResult = await workflowMediator.send(accountNode, {
    type: "create-account",
    payload: { userId, ...event.payload.userData }
  }).return();
  
  if (!accountResult.success) {
    return { 
      success: false, 
      stage: "account-creation",
      reason: accountResult.reason
    };
  }
  
  // Step 2: Set up permissions
  const permissionResult = await workflowMediator.send(permissionNode, {
    type: "assign-default-roles",
    payload: { 
      userId, 
      accountId: accountResult.accountId 
    }
  }).return();
  
  if (!permissionResult.success) {
    return { 
      success: false, 
      stage: "permission-setup",
      reason: permissionResult.reason
    };
  }
  
  // Step 3: Send welcome email
  await workflowMediator.send(notificationNode, {
    type: "send-welcome-email",
    payload: { 
      userId, 
      email: event.payload.userData.email 
    }
  });
  
  // Notify about completion
  workflowMediator.broadcast({
    type: "user-onboarded",
    payload: { userId, status: "completed" }
  });
  
  return { 
    success: true, 
    userId,
    accountId: accountResult.accountId
  };
});

This pattern showcases Happen's ability to orchestrate complex workflows while keeping individual components focused on their specific responsibilities.

Last updated