Core Concepts

Events all the way down.

Happen is built on a minimalist foundation of just two fundamental primitives: Nodes and Events. These primitives combine to create all the power of the framework without unnecessary complexity.

At the infrastructure level, these primitives are powered by NATS, a high-performance messaging system that provides the backbone for Happen's distributed capabilities.

Nodes: The Foundation of Happen Systems

Nodes are the primary actors in a Happen system. They are independent, autonomous components that can:

  • Receive and process events

  • Maintain internal state

  • Transform state

  • Emit new events

Conceptually, nodes can represent anything: services, entities, agents, processes, or even abstract concepts. This flexibility allows Happen systems to model complex domains with remarkable fidelity.

Unlike traditional components that often require elaborate setup and configuration, Happen nodes are intentionally lightweight. They embody a biological inspiration—like cells in an organism or neurons in a brain—simple individual units that collectively create sophisticated behaviors through their interactions.

Creating a Node

Creating a node in Happen is achieved by calling the createNode() function with an identifier and optional config object:

const orderProcessor = createNode("order-processor", {});

Every node has a unique identity that distinguishes it within the system:

  • Identities are cryptographically verifiable

  • Nodes act autonomously based on their internal logic

  • A node's behavior is determined by how it processes events and transforms state

  • Nodes don't need to know about the overall system structure to function

This autonomy means nodes can be developed, tested, and reasoned about independently, dramatically simplifying system development and maintenance.

Events: The Lifeblood of the System

Events are structured messages that flow between nodes, forming the lifeblood of any Happen system. They embody the principle that communication should be explicit, intentional, and meaningful.

Unlike traditional method calls or function invocations, events in Happen represent meaningful occurrences in the domain—things that have happened that might be of interest to other parts of the system. This perspective shift encourages developers to model their systems in terms of meaningful domain events rather than technical procedures.

Under the hood, Happen uses NATS to transport these events between nodes, benefiting from its high-performance, reliable messaging capabilities.

Event Anatomy

Events consist of three fundamental parts:

  1. Type - A string identifier indicating the event's purpose

  2. Payload - Domain-specific data relevant to the event

  3. Context - Causal and system information about the event

{
  type: "order-created",
  payload: {
    orderId: "ORD-123",
    items: [
      {
        productId: "PROD-456",
        quantity: 2,
        price: 29.99
      }
    ],
    total: 59.98
  },
  context: {
    causal: {
      id: "evt-789", // Unique event identifier
      sender: "checkout-node", // Node that created the event
      causationId: "evt-456", // Event that caused this one
      correlationId: "txn-123", // Transaction/process identifier
      path: ["user-node", "checkout-node"] // Event path
    },
    // Additional context...
  }
}

Event Processing: The Event Continuum

Nodes process events through a pure functional flow model we call the "Event Continuum." This approach treats event handling as a chain of functions where each function determines what happens next through its return value:

// Register an event handler - the entry point to the flow
orderNode.on("process-order", validateOrder);

// First function in the flow
function validateOrder(event, context) {
  // Validate order
  const validation = validateOrderData(event.payload);
  if (!validation.valid) {
    return { success: false, reason: "Invalid order" };
  }
  
  // Store validation result
  context.validatedOrder = {
    ...event.payload,
    validated: true,
    validatedAt: Date.now()
  };
  
  // Return the next function to execute
  return processPayment;
}

// Next function in the flow
function processPayment(event, context) {
  // Process payment using data from context
  const paymentResult = processTransaction(event.payload.payment);
  
  // Store in context
  context.payment = paymentResult;
  
  if (!paymentResult.success) {
    // Return a value to complete with failure
    return { success: false, reason: "Payment failed" };
  }
  
  // Return next function
  return createShipment;
}

// Final function returning a result value
function createShipment(event, context) {
  // Create shipment
  const shipment = generateShipment(event.payload.address);
  
  // Return final result (not a function)
  return {
    success: true,
    orderId: event.payload.orderId,
    trackingNumber: shipment.trackingNumber
  };
}

This pure functional approach offers remarkable power and flexibility:

  • Any function can return another function to redirect the flow

  • Any function can return a non-function value to complete the flow

  • A shared context object allows communication between functions

  • Complex workflows emerge naturally from function composition

State and Persistence

Nodes can maintain state that persists between events. Happen provides a clean, functional approach to accessing and updating state through pattern matching and transformations.

Under the hood, this state is persisted in NATS JetStream's Key-Value store, providing durable, distributed state management without requiring custom persistence implementations.

Accessing State

// Access the entire node state
const orderState = orderNode.state.get();

// Transform state before returning
const orders = orderNode.state.get(state => state.backlog);

// Access and transform state
const correctedBatch = orderNode.state.get(state => {
  // Transform the state
  return state.orders.map(order => ({
    ...order,
    total: recalculateTotal(order)
  }));
});

Transforming State

You can transform state using a single function approach:

// Transform state without pattern matching
orderNode.state.set((state) => {
  // Update transformed state
  return {
    ...state,
    orders: {
      ...state.orders,
      "order-123": {
        ...state.orders["order-123"],
        status: "shipped",
        shippedAt: Date.now()
      }
    }
  };
});

For more focused updates, you can create specific transformers:

// Define transformer for shipping an order
const shipOrder = (orderId) => (state) => ({
  ...state,
  orders: {
    ...state.orders,
    [orderId]: {
      ...state.orders[orderId],
      status: "shipped",
      shippedAt: Date.now()
    }
  }
});

// Apply transformer
orderNode.state.set(shipOrder("order-123"));

The Causality Web

Underlying Happen's approach is the concept of pure causality, where every event is part of a causal chain. This is implemented through the metadata in each event:

  • causationId: References the event that directly caused this one

  • correlationId: Groups events that are part of the same transaction or process

  • path: Shows the journey the event has taken through the system

This causal relationship creates a complete web of events that defines the system's behavior without requiring explicit schema definitions, contracts, or capability discovery protocols.

Uniting Event Processing and State Transformation

What makes Happen truly powerful is how the Event Continuum and state transformations work together:

// Process event with the Event Continuum
orderNode.on("update-order-status", (event, context) => {
  // Validate status change
  const validationResult = validateStatusChange(
    event.payload.orderId,
    event.payload.status
  );
  
  if (!validationResult.valid) {
    return {
      success: false,
      reason: validationResult.reason
    };
  }
  
  // Store in context
  context.orderId = event.payload.orderId;
  context.newStatus = event.payload.status;
  
  // Apply state transformation
  return applyStatusChange;
});

// Function that applies state transformation
function applyStatusChange(event, context) {
  const { orderId, newStatus } = context;
  
  // Transform state
  orderNode.state.set(state => {
    const orders = state.orders || {};
    const order = orders[orderId];
    
    if (!order) {
      return state;
    }
    
    return {
      ...state,
      orders: {
        ...orders,
        [orderId]: {
          ...order,
          status: newStatus,
          updatedAt: Date.now()
        }
      }
    };
  });
  
  // Emit another event based on the state change
  orderNode.broadcast({
    type: "order-status-changed",
    payload: {
      orderId,
      status: newStatus
    }
  });
  
  // Return success result
  return {
    success: true,
    orderId,
    status: newStatus
  };
}

This unified approach allows you to seamlessly combine the reactive, event-driven flow with the consistent, state-focused transformations, giving you the best of both worlds.

NATS: The Underlying Fabric

While Happen's conceptual model is built around Nodes and Events, the framework leverages NATS as its underlying messaging fabric to provide:

  1. Distributed Messaging: High-performance communication between nodes, even across network boundaries

  2. Persistence: Durable storage of events and state through JetStream

  3. Exactly-Once Processing: Guaranteed message delivery and processing semantics

  4. Cross-Environment Operation: Unified communication across server, browser, and edge environments

This foundation ensures that Happen systems are not only conceptually clean but also robust and resilient in real-world distributed environments.

Now that you understand the core concepts of Happen, you're ready to explore how these primitives combine to create powerful communication patterns and sophisticated system behaviors.

In the next section, we'll explore event pattern matching and how it enables powerful selective event handling.

Last updated