Micro-Kernel Architecture: Implementing Inter-Process Communication in Node.js
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, andContextmanager. - 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?
- Middleware Injection: The Kernel intercepts every
call. It automates Tracing (OpenTelemetry), Logging, and Argument Validation (Zod) before the target function ever runs. - Swappability: You can hot-swap the implementation of
inventory.check_stockwithout 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:
-
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 }); }); -
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:
- Trace Causality: If Inventory Plugin throws an error, the stack trace links back to the Order Plugin's action.
- 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.