Guide for registering and consuming services via the kernel DI container.
The PluginContext provides three core methods:
// Register a service
ctx.registerService(name: string, instance: any): void;
// Get a service (throws if not found)
ctx.getService<T>(name: string): T;
// Replace an existing service
ctx.replaceService(name: string, instance: any): void;
// Get all services
ctx.getServices(): Map<string, any>;Pass an already-created instance:
async init(ctx: PluginContext) {
const config = { apiKey: process.env.API_KEY };
ctx.registerService('config', config);
}Best for:
- Simple config objects
- Pre-existing instances
- No lazy initialization needed
Let the kernel manage creation and lifecycle:
import { ServiceLifecycle } from '@objectstack/core';
async init(ctx: PluginContext) {
const kernel = ctx.getKernel();
kernel.registerServiceFactory(
'db-pool',
(ctx) => createPool({ connectionString: process.env.DATABASE_URL }),
ServiceLifecycle.SINGLETON,
);
}Best for:
- Lazy initialization (created on first use)
- Lifecycle management
- Dependency injection between services
| Lifecycle | Behavior | Use Case |
|---|---|---|
SINGLETON |
One instance shared app-wide | Database connections, caches |
TRANSIENT |
New instance per getService() call |
Stateless utilities, formatters |
SCOPED |
One instance per scope (e.g., per request) | Request-scoped contexts, transactions |
kernel.registerServiceFactory(
'db-pool',
(ctx) => createPool({ connectionString: process.env.DATABASE_URL }),
ServiceLifecycle.SINGLETON,
);kernel.registerServiceFactory(
'request-logger',
(ctx) => new RequestLogger(ctx.logger),
ServiceLifecycle.TRANSIENT,
);kernel.registerServiceFactory(
'unit-of-work',
(ctx) => new UnitOfWork(ctx.getService('db-pool')),
ServiceLifecycle.SCOPED,
['db-pool'], // Dependencies — resolved before factory executes
);async start(ctx: PluginContext) {
const db = ctx.getService<IDataEngine>('objectql');
const cache = ctx.getService<ICacheService>('cache');
// Use services
const result = await db.object('account').find();
await cache.set('accounts', result);
}Check availability before calling:
async start(ctx: PluginContext) {
try {
const realtime = ctx.getService<IRealtimeService>('realtime');
realtime.publish('my-event', data);
} catch {
ctx.logger.debug('Realtime service not available — skipping');
}
}Wrap an existing service with instrumentation:
async start(ctx: PluginContext) {
const existingCache = ctx.getService('cache');
const instrumentedCache = new InstrumentedCache(existingCache);
ctx.replaceService('cache', instrumentedCache);
}| Service Key | Plugin Name | Package |
|---|---|---|
objectql (also data) |
com.objectstack.engine.objectql |
@objectstack/objectql |
driver.* |
com.objectstack.driver.* |
@objectstack/driver-* |
auth |
com.objectstack.auth |
@objectstack/plugin-auth |
metadata |
com.objectstack.metadata |
@objectstack/metadata |
realtime |
com.objectstack.service.realtime |
@objectstack/service-realtime |
cache |
com.objectstack.service.cache |
@objectstack/service-cache |
The REST plugin (com.objectstack.rest.api, @objectstack/rest) registers
no service — there is no rest service key.
ObjectKernel auto-injects in-memory fallbacks for core-criticality services not registered by any plugin during Phase 1.
Phase 1: init() completes for all plugins
↓
Kernel checks ServiceRequirementDef (@objectstack/spec/system):
'data' → required → ERROR if missing (no fallback; ObjectQLPlugin registers it)
'metadata' → core → auto-inject createMemoryMetadata() if missing
'cache' → core → auto-inject createMemoryCache() if missing
'queue' → core → auto-inject createMemoryQueue() if missing
'job' → core → auto-inject createMemoryJob() if missing
'i18n' → core → auto-inject createMemoryI18n() if missing
'auth' → core → no fallback factory — degraded-capability warning if missing
'realtime' → optional → skip, plugins should check availability
↓
Phase 2: start() begins — all core services available
The fallbacks are factories exported from @objectstack/core
(createMemoryCache, createMemoryMetadata, createMemoryQueue,
createMemoryJob, createMemoryI18n) — not classes.
| Level | Behavior |
|---|---|
required |
Kernel throws if missing — system cannot start |
core |
Auto-injected in-memory fallback if no plugin provides it |
optional |
Silently skipped — plugins must check before use |
const AnalyticsPlugin: Plugin = {
name: 'com.example.analytics',
async init(ctx: PluginContext) {
const db = ctx.getService('objectql'); // ❌ May not exist yet
ctx.registerService('analytics', new Analytics(db));
},
};const AnalyticsPlugin: Plugin = {
name: 'com.example.analytics',
dependencies: ['com.objectstack.engine.objectql'], // ✅ inits first
async init(ctx: PluginContext) {
const db = ctx.getService('objectql'); // ✅ Guaranteed registered
ctx.registerService('analytics', new Analytics(db));
},
};Never register null as a placeholder: registerService throws on a
duplicate key (so you cannot register the real instance later without
replaceService), and getService() treats a falsy value as missing and
throws — so consumers break either way.
async start(ctx: PluginContext) {
const realtime = ctx.getService('realtime'); // ❌ Throws if not available
realtime.publish('event', data);
}async start(ctx: PluginContext) {
try {
const realtime = ctx.getService('realtime');
realtime.publish('event', data);
} catch {
ctx.logger.debug('Realtime service not available'); // ✅ Graceful fallback
}
}async init(ctx: PluginContext) {
ctx.registerService('cache', new MemoryCache());
ctx.registerService('cache', new RedisCache());
// ❌ THROWS: "[Kernel] Service 'cache' already registered"
}async init(ctx: PluginContext) {
ctx.registerService('cache', new MemoryCache());
}
async start(ctx: PluginContext) {
const oldCache = ctx.getService('cache');
ctx.replaceService('cache', new RedisCache(oldCache)); // ✅ Explicit replacement
}- Use lowercase, hyphen-separated names — e.g.,
db-pool,request-logger - Use namespaces for multiple instances — e.g.,
driver.postgres,driver.mysql - Use descriptive names — e.g.,
auth-servicenotas - Avoid abbreviations — e.g.,
databasenotdb(unless well-known likedb-pool)
import { describe, it, expect } from 'vitest';
import { LiteKernel } from '@objectstack/core';
import MyPlugin from './plugin';
describe('Service Registration', () => {
it('registers service in init phase', async () => {
const kernel = new LiteKernel();
kernel.use(MyPlugin);
await kernel.bootstrap();
const service = kernel.getService('my-service');
expect(service).toBeDefined();
expect(service.name).toBe('MyService');
await kernel.shutdown();
});
it('throws when service not found', async () => {
const kernel = new LiteKernel();
await kernel.bootstrap();
expect(() => kernel.getService('non-existent')).toThrow();
await kernel.shutdown();
});
});- Register in init() — All service registration in Phase 1
- Consume in start() — Use getService() only in Phase 2
- Use try/catch for optional services — Don't assume availability
- Use descriptive service keys — Clear, namespaced names
- Declare dependencies — Let kernel handle initialization order
- Use factories for lazy init — Defer expensive creation
- Use scoped services for requests — Request-specific contexts
- Don't register null — Register a real instance or factory
- Use replaceService() explicitly — Don't re-register
- Document your services — What they do, what they depend on