This document describes the dependency injection (DI) system used throughout backtest-kit to manage service instantiation and dependencies. The DI system provides a centralized mechanism for service registration, lazy initialization, and singleton management across 50+ services organized into distinct categories.
For information about how services use context propagation, see Context Propagation. For details on the service layer architecture and responsibilities, see Layer Responsibilities.
The DI system in backtest-kit uses a Symbol-based registry pattern with factory functions to manage service lifecycle. The core mechanism consists of three components:
This approach enables loose coupling between services, simplifies testing through mockability, and ensures singleton behavior for stateful services like schema registries and connection managers.
The TYPES object contains Symbol-based identifiers for all services in the system. Symbols provide globally unique, collision-free identifiers that cannot be accidentally overridden.
The TYPES registry is divided into logical categories matching the service layer architecture. Each symbol is created using JavaScript's Symbol() constructor with a descriptive string identifier for debugging purposes.
Service registration occurs in src/lib/core/provide.ts:1-143 during module initialization. Each service class is registered with a factory function that instantiates it.
The registration follows a consistent pattern across all service categories:
// Schema Services Registration (lines 75-83)
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
| Service Category | Registration Lines | Number of Services | Purpose |
|---|---|---|---|
| Base Services | 56-58 | 1 | Logging infrastructure |
| Context Services | 60-63 | 2 | Execution and method context propagation |
| Connection Services | 65-73 | 7 | Client instance factory and caching |
| Schema Services | 75-83 | 7 | Configuration storage and retrieval |
| Core Services | 85-89 | 3 | Domain logic for strategies, exchanges, frames |
| Global Services | 91-96 | 4 | Entry point facades for subsystems |
| Command Services | 98-102 | 3 | Workflow orchestration for execution modes |
| Logic Private Services | 104-108 | 3 | Internal algorithms for backtest, live, walker |
| Logic Public Services | 110-114 | 3 | Public API wrappers for logic services |
| Markdown Services | 116-126 | 9 | Report generation for various aspects |
| Validation Services | 128-138 | 9 | Registration-time validation |
| Template Services | 140-142 | 1 | Code generation for optimizer |
Service resolution uses the inject<T>() function to create lazy, memoized accessors for service instances. The accessor pattern ensures services are instantiated only when first accessed and reused thereafter.
The following demonstrates how a public API function accesses services through the DI container:
// From src/function/add.ts:52-64
export function addStrategy(strategySchema: IStrategySchema) {
backtest.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
strategySchema,
});
backtest.strategyValidationService.addStrategy(
strategySchema.strategyName,
strategySchema
);
backtest.strategySchemaService.register(
strategySchema.strategyName,
strategySchema
);
}
In this example:
backtest.loggerService - First access triggers LoggerService instantiationbacktest.strategyValidationService - Lazy instantiation of validation servicebacktest.strategySchemaService - Lazy instantiation of schema serviceServices are organized into 12 distinct categories, each serving a specific architectural layer. The following table provides a comprehensive mapping of all 50+ services:
| Category | Services | Files | Responsibility |
|---|---|---|---|
| Base | LoggerService |
src/lib/services/base/LoggerService.ts | Centralized logging with configurable logger injection |
| Context | ExecutionContextServiceMethodContextService |
src/lib/services/context/ | AsyncLocalStorage-based context propagation for temporal isolation |
| Schema | ExchangeSchemaServiceStrategySchemaServiceFrameSchemaServiceWalkerSchemaServiceSizingSchemaServiceRiskSchemaServiceOptimizerSchemaService |
src/lib/services/schema/ | ToolRegistry-based storage for registered configurations |
| Validation | ExchangeValidationServiceStrategyValidationServiceFrameValidationServiceWalkerValidationServiceSizingValidationServiceRiskValidationServiceOptimizerValidationServiceConfigValidationServiceColumnValidationService |
src/lib/services/validation/ | Registration-time validation with memoized results |
| Connection | ExchangeConnectionServiceStrategyConnectionServiceFrameConnectionServiceSizingConnectionServiceRiskConnectionServiceOptimizerConnectionServicePartialConnectionService |
src/lib/services/connection/ | Factory pattern with memoization for client instances |
| Core | ExchangeCoreServiceStrategyCoreServiceFrameCoreService |
src/lib/services/core/ | Domain logic for fundamental operations |
| Global | SizingGlobalServiceRiskGlobalServiceOptimizerGlobalServicePartialGlobalService |
src/lib/services/global/ | Entry point facades with logging and delegation |
| Command | LiveCommandServiceBacktestCommandServiceWalkerCommandService |
src/lib/services/command/ | Workflow orchestration for execution modes |
| Logic Private | BacktestLogicPrivateServiceLiveLogicPrivateServiceWalkerLogicPrivateService |
src/lib/services/logic/private/ | Internal algorithms not exposed externally |
| Logic Public | BacktestLogicPublicServiceLiveLogicPublicServiceWalkerLogicPublicService |
src/lib/services/logic/public/ | Controlled wrappers with additional validation |
| Markdown | BacktestMarkdownServiceLiveMarkdownServiceScheduleMarkdownServicePerformanceMarkdownServiceWalkerMarkdownServiceHeatMarkdownServicePartialMarkdownServiceOutlineMarkdownServiceRiskMarkdownService |
src/lib/services/markdown/ | Event-driven report generation with bounded queues |
| Template | OptimizerTemplateService |
src/lib/services/template/ | Code generation for LLM-based strategy optimization |
The DI system initialization occurs in src/lib/index.ts:240 via the init() function call. This initializes the DI container and prepares it for lazy service instantiation.
Key Points:
provide() calls execute during src/lib/core/provide.ts module initializationinject() calls in src/lib/index.ts:61-223 create lazy accessors but do not instantiate servicesServices depend on each other through constructor injection or method calls. The DI system manages these dependencies transparently through lazy evaluation.
Connection services demonstrate the most complex dependency patterns, as they create client instances that depend on multiple other services:
| Client Type | Created By | Key Dependencies |
|---|---|---|
ClientStrategy |
StrategyConnectionService |
ExecutionContextService, MethodContextService, ExchangeCoreService, RiskGlobalService, SizingGlobalService, PartialGlobalService |
ClientExchange |
ExchangeConnectionService |
ExecutionContextService, MethodContextService, ExchangeSchemaService |
ClientRisk |
RiskConnectionService |
ExecutionContextService, MethodContextService, RiskSchemaService, PersistRiskAdapter |
ClientSizing |
SizingConnectionService |
ExecutionContextService, MethodContextService, SizingSchemaService |
ClientFrame |
FrameConnectionService |
MethodContextService, FrameSchemaService, FrameCoreService |
ClientPartial |
PartialConnectionService |
ExecutionContextService, MethodContextService, PersistPartialAdapter |
The DI system implements memoization at two levels:
The inject() function returns a lazy accessor that, when called, executes the factory function once and caches the result. This ensures singleton behavior for all services:
// Conceptual implementation (actual code in src/lib/core/di.ts)
function inject<T>(symbol: Symbol): () => T {
let cached: T | undefined;
return () => {
if (cached === undefined) {
const factory = container.get(symbol);
cached = factory();
}
return cached;
};
}
Connection services use the memoize() utility from functools-kit to cache client instances with composite keys:
// Example from StrategyConnectionService
private _memoized = memoize(
(symbol: string, strategyName: string, backtest: boolean) => {
// Complex client instantiation logic
return new ClientStrategy(...);
}
);
getClient(symbol: string, strategyName: string, backtest: boolean) {
return this._memoized(symbol, strategyName, backtest);
}
This pattern ensures:
ClientStrategy instance for symbol="BTC/USDT", strategyName="my-strategy", backtest=truesymbol="BTC/USDT", strategyName="my-strategy", backtest=false (live mode)symbol="ETH/USDT", strategyName="my-strategy", backtest=true (different symbol)The DI system provides several key benefits for the backtest-kit architecture:
Services can be mocked or stubbed during testing by replacing the factory function:
// Test setup example
provide(TYPES.exchangeSchemaService, () => createMockExchangeSchema());
Services are created only when first accessed, reducing startup time and memory usage. This is particularly important for:
The DI system guarantees singleton behavior for stateful services without requiring manual singleton implementation:
The DI system enforces clear boundaries between service layers:
New services can be added by:
provide() call to src/lib/core/provide.tsinject() call to src/lib/index.tsNo existing code requires modification when adding new services.