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