Services
Infrastructure management with dependency injection.
What Are Services?
Services manage infrastructure - database connections, API clients, utilities. They handle the "how" of connecting to external systems so actions can focus on business logic.
Service Structure
const databaseService = service({
name: "database",
// Register: Define HOW to create dependencies
register: (container) => {
container.singleton("db", () => new MongoDB(process.env.DB_URI));
container.singleton("userRepo", (c) => new UserRepository(c.resolve("db")));
},
// Boot: WHEN to initialize (agent startup)
boot: async (container) => {
const db = container.resolve("db");
await db.connect();
console.log("✅ Database connected");
},
});
The Container
Services use a dependency injection container for shared resource management:
const container = createContainer();
// singleton() - Create once, reuse everywhere
container.singleton("apiClient", () => new ApiClient());
// register() - Create new instance each time
container.register("requestId", () => crypto.randomUUID());
// instance() - Store pre-created object
container.instance("config", { apiKey: "secret123" });
// Usage in actions
const client = ctx.container.resolve("apiClient");
Without Services: Connection Chaos
// ❌ Actions manage their own connections (slow, repetitive)
const sendMessageAction = action({
handler: async ({ channelId, message }) => {
// Create new client every time!
const client = new Discord.Client({ token: process.env.DISCORD_TOKEN });
await client.login(); // Slow connection each time
await client.channels.get(channelId).send(message);
await client.destroy();
},
});
With Services: Shared Infrastructure
// ✅ Service manages connection once, actions reuse it
const discordService = service({
name: "discord",
register: (container) => {
container.singleton("discordClient", () => new Discord.Client({ token: process.env.DISCORD_TOKEN }));
},
boot: async (container) => {
await container.resolve("discordClient").login(); // Connect once at startup
},
});
const sendMessageAction = action({
handler: async ({ channelId, message }, ctx) => {
const client = ctx.container.resolve("discordClient"); // Already connected!
await client.channels.get(channelId).send(message);
},
});
Service Lifecycle
const redisService = service({
name: "redis",
// Phase 1: REGISTER - Define factory functions
register: (container) => {
container.singleton("redisConfig", () => ({
host: process.env.REDIS_HOST || "localhost",
port: process.env.REDIS_PORT || 6379,
}));
container.singleton("redisClient", (c) => new Redis(c.resolve("redisConfig")));
},
// Phase 2: BOOT - Actually connect/initialize
boot: async (container) => {
const client = container.resolve("redisClient");
await client.connect();
console.log("✅ Redis connected");
},
});
// Execution order:
// 1. All services register() (define dependencies)
// 2. All services boot() (initialize connections)
// 3. Ensures dependencies available when needed
Service Examples
Multi-Component Service
const tradingService = service({
name: "trading",
register: (container) => {
container.singleton("alpacaClient", () => new Alpaca({
key: process.env.ALPACA_KEY,
secret: process.env.ALPACA_SECRET,
}));
container.singleton("portfolio", (c) => new PortfolioTracker(c.resolve("alpacaClient")));
container.singleton("riskManager", () => new RiskManager({ maxPosition: 0.1 }));
},
boot: async (container) => {
await container.resolve("alpacaClient").authenticate();
await container.resolve("portfolio").sync();
console.log("💰 Trading ready");
},
});
// Actions use all components
const buyStock = action({
handler: async ({ symbol, quantity }, ctx) => {
const client = ctx.container.resolve("alpacaClient");
const riskManager = ctx.container.resolve("riskManager");
if (riskManager.canBuy(symbol, quantity)) {
return await client.createOrder({ symbol, qty: quantity, side: "buy" });
}
throw new Error("Risk limits exceeded");
},
});
Environment-Based Configuration
const storageService = service({
name: "storage",
register: (container) => {
if (process.env.NODE_ENV === "production") {
container.singleton("storage", () => new S3Storage({ bucket: process.env.S3_BUCKET }));
} else {
container.singleton("storage", () => new LocalStorage({ path: "./uploads" }));
}
},
boot: async (container) => {
await container.resolve("storage").initialize();
console.log(`📁 ${process.env.NODE_ENV === "production" ? "S3" : "Local"} storage ready`);
},
});
Service Dependencies
// Base service
const databaseService = service({
name: "database",
register: (container) => {
container.singleton("db", () => new MongoDB(process.env.DB_URI));
},
boot: async (container) => {
await container.resolve("db").connect();
},
});
// Dependent service
const cacheService = service({
name: "cache",
register: (container) => {
container.singleton("redis", () => new Redis(process.env.REDIS_URL));
container.singleton("cacheManager", (c) => new CacheManager({
fastCache: c.resolve("redis"),
slowCache: c.resolve("db"), // From databaseService
}));
},
boot: async (container) => {
await container.resolve("redis").connect();
await container.resolve("cacheManager").initialize();
},
});
// Extension using both services
const dataExtension = extension({
name: "data",
services: [databaseService, cacheService],
actions: [
action({
name: "get-user",
handler: async ({ userId }, ctx) => {
const cache = ctx.container.resolve("cacheManager");
return await cache.getOrFetch(`user:${userId}`, () =>
ctx.container.resolve("db").collection("users").findOne({ _id: userId })
);
},
}),
],
});
Best Practices
Single Responsibility
// ✅ Good - focused on one domain
const databaseService = service({ name: "database" /* only DB connection */ });
const cacheService = service({ name: "cache" /* only caching */ });
// ❌ Bad - mixed responsibilities
const everythingService = service({ name: "everything" /* DB + cache + API + logging */ });
Graceful Error Handling
const apiService = service({
name: "external-api",
boot: async (container) => {
try {
await container.resolve("apiClient").healthCheck();
console.log("✅ External API ready");
} catch (error) {
console.warn("⚠️ API unavailable, features limited");
// Don't crash agent - let actions handle gracefully
}
},
});
Resource Cleanup
const databaseService = service({
register: (container) => {
container.singleton("db", () => {
const db = new MongoDB(process.env.DB_URI);
process.on("SIGINT", async () => {
await db.close();
process.exit(0);
});
return db;
});
},
});
Common Issues
Missing Dependencies
// Error: "Token 'databaseClient' not found"
// ❌ Problem
const action = action({
handler: async (args, ctx) => {
const db = ctx.container.resolve("databaseClient"); // Not registered!
},
});
// ✅ Solution
const service = service({
register: (container) => {
container.singleton("databaseClient", () => new Database());
// ^^^^^^^^^^^^^^ Must match resolve token
},
});
Circular Dependencies
// ✅ Solution - break cycles with coordinator pattern
const coordinatorService = service({
register: (container) => {
container.singleton("a", () => new A());
container.singleton("b", () => new B());
},
boot: async (container) => {
// Wire relationships after creation
const coordinator = new Coordinator(container.resolve("a"), container.resolve("b"));
coordinator.wireComponents();
},
});
Key Takeaways
- Services manage infrastructure - API clients, databases, utilities
- Dependency injection container - Shared resources across all actions
- Two-phase lifecycle - Register (define) then boot (initialize)
- Separation of concerns - Infrastructure separate from business logic
- Resource efficiency - One connection shared across all actions
See Also
- Extensions - Feature package layer
- Extensions vs Services - Decision guide