Back to Blog

Micro-Kernel Architecture: Implementing Inter-Process Communication in Node.js

• By ObjectOS Engineering

Micro-Kernel Architecture: Implementing Inter-Process Communication

When we describe ObjectOS as an "Operating System," we aren't just using a metaphor. We are describing its runtime architecture. ObjectOS implements a Micro-Kernel pattern where the core runtime is minimal, and all business logic resides in isolated "Drivers" (Plugins).

This article analyzes the memory model, the Inter-Plugin Communication (IPC) mechanisms, and how we handle module isolation within a single Node.js process.

1. The Kernel vs. User Space

In a traditional Monolith, OrderService simply imports InventoryService.

// Traditional Tight Coupling
import { InventoryService } from '../inventory/service';
// If InventoryService changes its signature, OrderService breaks at compile time (best case) or runtime (worst case).

In ObjectOS, we enforce a strict boundary.

  • Kernel Space: The PluginLoader, EventBroker, and Context manager.
  • User Space: The code inside your plugins/ directory.

The Service Registry (V-Table equivalent)

Plugins provide services via string-based identifiers, similar to an OS System Call table or a V-Table.

// The Kernel maintains a map of function pointers
type ServiceHandler = (ctx: Context, payload: any) => Promise<any>;
const registry = new Map<string, ServiceHandler>();

// User Space: Registering a 'Syscall'
kernel.provide('inventory.check_stock', async (ctx, { sku }) => { ... });

// User Space: Invoking a 'Syscall'
const stock = await ctx.call('inventory.check_stock', { sku: 'A123' });

Why this indirection?

  1. Middleware Injection: The Kernel intercepts every call. It automates Tracing (OpenTelemetry), Logging, and Argument Validation (Zod) before the target function ever runs.
  2. Swappability: You can hot-swap the implementation of inventory.check_stock without restarting the callers.

2. In-Memory IPC: The Event Bus

Plugins communicate asynchronously via Events. We don't use a standard EventEmitter because it lacks Reliability and backpressure.

We implemented a custom Broker that supports:

  1. Transactional Outbox Pattern: When a plugin emits an event alongside a DB mutation, the event is not fired until the DB transaction commits.

    await ctx.transaction(async (trx) => {
        await trx.update('orders', ...);
        // Buffered effectively until commit
        ctx.emit('order.created', { id }); 
    });
  2. Wildcard Subscriptions: Plugins can subscribe to order.*. The matching logic uses an optimized Trie (Prefix Tree) lookup, making dispatch $O(k)$ where $k$ is the topic length, irrespective of the number of listeners.

3. Memory Isolation & The Context Object

Since we run in a single V8 Isolate (for performance), we cannot rely on OS processes for memory isolation. Instead, we use Request-Scoped Contexts and AsyncLocalStorage.

The Context Tree

Every request spawns a root Context. When Plugin A calls Plugin B, we fork the context.

Request(Context ID: 100)
├── Auth Plugin (Context ID: 100.1)
└── Order Plugin (Context ID: 100.2)
    └── emits 'order.created'
        └── Inventory Plugin (Context ID: 100.3) [Triggered by Event]

This inheritance chain allows us to:

  1. Trace Causality: If Inventory Plugin throws an error, the stack trace links back to the Order Plugin's action.
  2. Sandboxing: We can attach "Budgets" to contexts (e.g., Max SQL Queries = 50). If a plugin goes rogue, the Kernel kills that specific Context branch without crashing the server.

4. Conclusion: Monolith Performance, Microservice Discipline

By enforcing these architectural constraints—Service Registry indirection, Event-based IPC, and Context Isolation—ObjectOS achieves the strict decoupling usually reserved for Microservices, but with the zero-latency performance of function calls in a Monolith.