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:
Type - A string identifier indicating the event's purpose
Payload - Domain-specific data relevant to the event
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 onecorrelationId
: Groups events that are part of the same transaction or processpath
: 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:
Distributed Messaging: High-performance communication between nodes, even across network boundaries
Persistence: Durable storage of events and state through JetStream
Exactly-Once Processing: Guaranteed message delivery and processing semantics
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