@fjell/registry
v4.4.81
Published
Dependency injection and service location system for the Fjell ecosystem
Maintainers
Readme
Fjell Registry
A comprehensive dependency injection and service location system for the Fjell ecosystem. The Registry provides a centralized way to register, scope, and retrieve service instances based on type hierarchies and contextual scopes.
Core Concepts
Instance
An Instance represents any registered service or component in the system. It consists of:
- Coordinate: Defines the service's identity (key types + scopes)
- Registry: Reference to the registry managing this instance
interface Instance<S extends string, L1 extends string = never, ...> {
coordinate: Coordinate<S, L1, L2, L3, L4, L5>;
registry: Registry;
}Coordinate
A Coordinate uniquely identifies an instance using:
- Key Type Array (KTA): Hierarchical type identifiers (e.g.,
['User', 'Profile']) - Scopes: Context qualifiers (e.g.,
['firestore'],['postgresql'])
This allows multiple implementations of the same service:
// Same User service, different storage backends
const firestoreUser = createCoordinate(['User'], ['firestore']);
const postgresUser = createCoordinate(['User'], ['postgresql']);Registry
The central service locator that:
- Has a mandatory type identifier (e.g., 'services', 'data', 'cache')
- Creates and registers instances atomically (no circular dependencies)
- Retrieves instances by type and scope
- Maintains a hierarchical tree of services
interface Registry {
readonly type: string; // Mandatory type identifier
createInstance: <...>(...) => Instance<...>;
register: (...) => void; // Deprecated
get: (...) => Instance | null;
getCoordinates: () => Coordinate[]; // Discover all registered coordinates
instanceTree: InstanceTree;
}
// Create registries with their type
const serviceRegistry = createRegistry('services');
const dataRegistry = createRegistry('data');
const cacheRegistry = createRegistry('cache');RegistryHub
A higher-level registry that manages multiple Registry instances. The RegistryHub serves as a central hub where different registries can be organized automatically using their type property.
Key Features:
- Automatic Registry Organization: Registers registries using their built-in type property
- Unified Access: Single point of access to instances across all registries
- Type Safety: Maintains type safety while providing cross-registry access
- Lifecycle Management: Register, unregister, and manage multiple registries
interface RegistryHub {
registerRegistry: (registry: Registry) => void; // Uses registry.type automatically
get: (type: string, kta: string[], options?: { scopes?: string[] }) => Instance | null;
getRegistry: (type: string) => Registry | null;
getRegisteredTypes: () => string[];
getAllCoordinates: () => CoordinateWithRegistry[]; // Discover coordinates across all registries
unregisterRegistry: (type: string) => boolean;
}Architecture:
RegistryHub
├── Registry (type: 'services') → Auth, User, Payment services
├── Registry (type: 'data') → Repositories, Data layers
├── Registry (type: 'cache') → Cache implementations
└── Registry (type: 'integrations') → External APIs, webhooksInstanceFactory
A factory function that creates instances:
type InstanceFactory<S extends string, L1, L2, L3, L4, L5> = (
registry: Registry,
coordinate: Coordinate<S, L1, L2, L3, L4, L5>
) => Instance<S, L1, L2, L3, L4, L5>;Benefits:
- ✅ No circular dependencies (factory receives populated registry + coordinate)
- ✅ Dependency injection friendly (factory can access other services)
- ✅ Atomic creation and registration
- ✅ Type-safe instance creation
Architecture
createRegistry('services').createInstance(['User'], ['firestore'], factory)
↓
1. Creates Coordinate { kta: ['User'], scopes: ['firestore'] }
2. Calls factory(registry, coordinate)
3. Validates returned instance
4. Registers instance automatically
5. Returns ready-to-use instanceRegistry Structure:
Registry (type: 'services')
├── Instance Tree (by Key Types)
│ ├── User
│ │ ├── [ScopedInstance] scope: ['firestore'] ← Created via factory
│ │ └── [ScopedInstance] scope: ['postgresql'] ← Created via factory
│ └── User.Profile
│ ├── [ScopedInstance] scope: ['firestore']
│ └── [ScopedInstance] scope: ['postgresql']
└── Atomic Create+Register LogicRegistryHub Structure:
RegistryHub
├── Registry (type: 'services')
│ ├── User (multiple scopes)
│ ├── Auth (multiple scopes)
│ └── Payment (multiple scopes)
├── Registry (type: 'data')
│ ├── UserRepository (multiple scopes)
│ └── OrderRepository (multiple scopes)
└── Registry (type: 'cache')
├── UserCache (multiple scopes)
└── SessionCache (multiple scopes)Usage Patterns
1. Recommended: Registry-Managed Creation ✅
const registry = createRegistry('services');
// Create and register instances atomically - no circular dependency!
const userService = registry.createInstance(['User'], ['firestore'], (registry, coordinate) => {
// Your instance implementation here
return {
coordinate,
registry,
// ... your service implementation
};
});
// Instance is automatically registered and ready to use
const retrievedService = registry.get(['User'], { scopes: ['firestore'] });
console.log(userService === retrievedService); // true2. RegistryHub: Managing Multiple Registries ✅
const hub = createRegistryHub();
// Create domain-specific registries with their types
const serviceRegistry = createRegistry('services');
const dataRegistry = createRegistry('data');
const cacheRegistry = createRegistry('cache');
// Register registries in the hub - no type parameter needed!
hub.registerRegistry(serviceRegistry); // Uses 'services' from registry.type
hub.registerRegistry(dataRegistry); // Uses 'data' from registry.type
hub.registerRegistry(cacheRegistry); // Uses 'cache' from registry.type
// Create instances in specific registries
const authService = serviceRegistry.createInstance(['Auth'], ['jwt'], authFactory);
const userRepo = dataRegistry.createInstance(['User'], ['firestore'], repoFactory);
const userCache = cacheRegistry.createInstance(['User'], ['redis'], cacheFactory);
// Access instances through the hub - unified interface
const auth = hub.get('services', ['Auth'], { scopes: ['jwt'] });
const user = hub.get('data', ['User'], { scopes: ['firestore'] });
const cache = hub.get('cache', ['User'], { scopes: ['redis'] });
// Hub management
console.log(hub.getRegisteredTypes()); // ['services', 'data', 'cache']
const specificRegistry = hub.getRegistry('services');
hub.unregisterRegistry('cache'); // Remove if needed2a. RegistryHub: Cross-Registry Coordinate Discovery ✅
// Discover all coordinates across ALL registries in the hub
const allCoordinates = hub.getAllCoordinates();
console.log(`Found ${allCoordinates.length} total services`);
// Each coordinate includes its registry type
allCoordinates.forEach(({ coordinate, registryType }) => {
console.log(`${registryType}: ${coordinate.kta.join('.')} [${coordinate.scopes.join(', ')}]`);
});
// Group coordinates by registry type for analysis
const byRegistry = allCoordinates.reduce((acc, item) => {
if (!acc[item.registryType]) acc[item.registryType] = [];
acc[item.registryType].push(item.coordinate);
return acc;
}, {} as Record<string, Coordinate[]>);
// Environment-based analysis across all registries
const productionServices = allCoordinates.filter(c =>
c.coordinate.scopes.includes('production')
);
const productionByRegistry = productionServices.reduce((acc, c) => {
acc[c.registryType] = (acc[c.registryType] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log('Production services by registry:', productionByRegistry);
// Output: { services: 2, data: 3, cache: 1 }Use Cases for hub.getAllCoordinates():
- System-Wide Health Monitoring: Check services across all registry domains
- Architecture Analysis: Understand service distribution across domains
- Environment Validation: Ensure complete deployment across all registry types
- Cross-Registry Dependencies: Map service relationships between domains
- Documentation Generation: Auto-generate complete system catalogs
3. Multiple Implementations with Scopes
const registry = createRegistry('data');
// Firestore implementation
const firestoreUser = registry.createInstance(['User'], ['firestore'], (registry, coordinate) => ({
coordinate,
registry,
save: async (user) => { /* firestore logic */ },
find: async (query) => { /* firestore logic */ }
}));
// PostgreSQL implementation
const postgresUser = registry.createInstance(['User'], ['postgresql'], (registry, coordinate) => ({
coordinate,
registry,
save: async (user) => { /* postgresql logic */ },
find: async (query) => { /* postgresql logic */ }
}));
// Context-aware retrieval
const prodService = registry.get(['User'], { scopes: ['firestore'] });
const devService = registry.get(['User'], { scopes: ['postgresql'] });4. Hierarchical Services
const registry = createRegistry('services');
// Register nested services
const profileService = registry.createInstance(['User', 'Profile'], ['firestore'], factoryFunction);
const settingsService = registry.createInstance(['User', 'Settings'], ['postgresql'], factoryFunction);
// Retrieve nested services
const userProfile = registry.get(['User', 'Profile'], { scopes: ['firestore'] });5. Cross-Registry Dependencies with RegistryHub
const hub = createRegistryHub();
const serviceRegistry = createRegistry('services');
const dataRegistry = createRegistry('data');
// Registries automatically use their type property
hub.registerRegistry(serviceRegistry); // → 'services'
hub.registerRegistry(dataRegistry); // → 'data'
// Service that depends on data layer
const orderService = serviceRegistry.createInstance(['Order'], ['business'], (registry, coordinate) => {
// Access data layer through hub
const userRepo = hub.get('data', ['User'], { scopes: ['firestore'] });
const orderRepo = hub.get('data', ['Order'], { scopes: ['firestore'] });
return {
coordinate,
registry,
createOrder: async (orderData) => {
const user = await userRepo.operations.findOne(orderData.userId);
return orderRepo.operations.create({ ...orderData, user });
}
};
});6. ~~Legacy Registration~~ (Deprecated)
// OLD WAY - has circular dependency issue
const registry = createRegistry('legacy');
const userService = createInstance(registry, createCoordinate(['User'], ['firestore'])); // ❌ Circular!
registry.register(['User'], userService, { scopes: ['firestore'] });
// Use registry.createInstance() instead! ✅7. Coordinate Discovery with getCoordinates()
The Registry provides introspection capabilities to discover all registered instances:
const registry = createRegistry('services');
// Register various services
registry.createInstance(['User'], ['firestore'], userFirestoreFactory);
registry.createInstance(['User'], ['postgresql'], userPostgresFactory);
registry.createInstance(['Cache', 'Redis'], ['production'], redisFactory);
registry.createInstance(['Cache', 'Memory'], ['development'], memoryFactory);
// Discover all registered coordinates
const coordinates = registry.getCoordinates();
console.log(`Found ${coordinates.length} registered services`);
// Analyze registration patterns
coordinates.forEach(coord => {
console.log(`Service: ${coord.kta.join('.')} | Scopes: ${coord.scopes.join(', ')}`);
});
// Group by environment for health checks
const productionServices = coordinates.filter(c => c.scopes.includes('production'));
const developmentServices = coordinates.filter(c => c.scopes.includes('development'));
// Service discovery - find all cache implementations
const cacheServices = coordinates.filter(c => c.kta.includes('Cache'));Use Cases for getCoordinates():
- Health Monitoring: Verify all expected services are registered
- Development Tools: Debug service registration in development
- Service Discovery: Find available implementations at runtime
- Environment Validation: Ensure proper scope configuration
- Documentation Generation: Auto-generate service catalogs
8. Cascade Pattern
The Registry enables automatic service discovery:
// System receives an item and automatically finds the right service
function saveItem(item: Item) {
const registry = createRegistry('services');
const service = registry.get(item.getKeyTypes(), {
scopes: ['cache', 'fast']
}) || registry.get(item.getKeyTypes(), {
scopes: ['database']
});
return service.operations.save(item);
}RegistryHub Patterns
1. Domain-Driven Registry Organization
const hub = createRegistryHub();
// Domain registries with explicit types
const userDomainRegistry = createRegistry('user-domain');
const orderDomainRegistry = createRegistry('order-domain');
const paymentDomainRegistry = createRegistry('payment-domain');
const notificationDomainRegistry = createRegistry('notification-domain');
// Register using their built-in types
hub.registerRegistry(userDomainRegistry); // → 'user-domain'
hub.registerRegistry(orderDomainRegistry); // → 'order-domain'
hub.registerRegistry(paymentDomainRegistry); // → 'payment-domain'
hub.registerRegistry(notificationDomainRegistry); // → 'notification-domain'
// Each domain manages its own services
const userService = hub.get('user-domain', ['User'], { scopes: ['api'] });
const orderService = hub.get('order-domain', ['Order'], { scopes: ['business'] });
const paymentGateway = hub.get('payment-domain', ['Gateway'], { scopes: ['stripe'] });2. Environment-Based Registry Management
const hub = createRegistryHub();
if (process.env.NODE_ENV === 'production') {
const prodDataRegistry = createRegistry('data');
const redisRegistry = createRegistry('cache');
hub.registerRegistry(prodDataRegistry);
hub.registerRegistry(redisRegistry);
} else {
const devDataRegistry = createRegistry('data');
const memoryRegistry = createRegistry('cache');
hub.registerRegistry(devDataRegistry);
hub.registerRegistry(memoryRegistry);
}
// Same code works across environments
const userRepo = hub.get('data', ['User']);
const userCache = hub.get('cache', ['User']);3. Module-Based Registry Organization
// Each module/library provides its own typed registry
import { createUserModuleRegistry } from '@myapp/user-module';
import { createOrderModuleRegistry } from '@myapp/order-module';
import { createPaymentModuleRegistry } from '@myapp/payment-module';
const hub = createRegistryHub();
// These functions return registries with proper types
const userRegistry = createUserModuleRegistry(); // Registry with type 'users'
const orderRegistry = createOrderModuleRegistry(); // Registry with type 'orders'
const paymentRegistry = createPaymentModuleRegistry(); // Registry with type 'payments'
// Auto-register using their types
hub.registerRegistry(userRegistry); // → 'users'
hub.registerRegistry(orderRegistry); // → 'orders'
hub.registerRegistry(paymentRegistry); // → 'payments'
// Cross-module integration
const orderService = hub.get('orders', ['OrderService']);
const paymentProcessor = hub.get('payments', ['Processor'], { scopes: ['stripe'] });Library Integration Patterns
fjell-lib Integration
// In fjell-lib
export const createServiceRegistry = () => createRegistry('fjell-lib');
// In applications
import { createServiceRegistry } from '@fjell/lib';
const hub = createRegistryHub();
hub.registerRegistry(createServiceRegistry()); // → 'fjell-lib'fjell-cache Integration
// In fjell-cache
export const createCacheRegistry = () => createRegistry('fjell-cache');
// In applications
import { createCacheRegistry } from '@fjell/cache';
const hub = createRegistryHub();
hub.registerRegistry(createCacheRegistry()); // → 'fjell-cache'fjell-client-api Integration
// In fjell-client-api
export const createClientApiRegistry = () => createRegistry('fjell-client-api');
// In applications
import { createClientApiRegistry } from '@fjell/client-api';
const hub = createRegistryHub();
hub.registerRegistry(createClientApiRegistry()); // → 'fjell-client-api'Design Benefits
- Unified Service Location: Single pattern across all Fjell libraries
- Multiple Implementations: Support Firestore, PostgreSQL, etc. for same types
- Contextual Scoping: Environment-aware service selection
- Hierarchical Organization: Natural type hierarchy support
- Dependency Injection: Clean separation of configuration and usage
- Service Introspection: Discover all registered coordinates with
getCoordinates() - Cross-Registry Discovery: System-wide coordinate discovery with
getAllCoordinates() - Self-Documenting Registries: Registry type is built-in, no external tracking needed
- Cross-Registry Access: Unified interface for accessing instances across multiple registries
- Error Prevention: Impossible to register a registry under the wrong type
Migration Strategy
Phase 1: Core Libraries
- ✅ fjell-registry (base implementation + RegistryHub with typed registries)
- 🔄 fjell-lib (create registry with type 'fjell-lib')
- 🔄 fjell-lib-sequelize (create registry with type 'fjell-lib-sequelize')
- 🔄 fjell-lib-firestore (create registry with type 'fjell-lib-firestore')
Phase 2: Service Libraries
- 🔄 fjell-cache (create registry with type 'fjell-cache')
- 🔄 fjell-client-api (create registry with type 'fjell-client-api')
Phase 3: UI Libraries (Future)
- fjell-express-router (create registry with type 'fjell-express-router')
- fjell-providers (create registry with type 'fjell-providers')
Phase 4: RegistryHub Integration
- Applications use RegistryHub to organize all typed registries
- Libraries export factory functions that create properly typed registries
- Cross-module dependency management through hub
Configuration Examples
Multi-Database Setup with RegistryHub
const hub = createRegistryHub();
// Create typed registries
const servicesRegistry = createRegistry('services');
const firestoreDataRegistry = createRegistry('firestore-data');
const postgresDataRegistry = createRegistry('postgres-data');
// Register Firestore services
firestoreDataRegistry.createInstance(['User'], ['production'], firestoreUserFactory);
firestoreDataRegistry.createInstance(['Order'], ['production'], firestoreOrderFactory);
// Register PostgreSQL services
postgresDataRegistry.createInstance(['User'], ['development'], postgresUserFactory);
postgresDataRegistry.createInstance(['Analytics'], [], postgresAnalyticsFactory);
// Register business services
servicesRegistry.createInstance(['UserService'], ['business'], userServiceFactory);
servicesRegistry.createInstance(['OrderService'], ['business'], orderServiceFactory);
// Register all typed registries
hub.registerRegistry(servicesRegistry); // → 'services'
hub.registerRegistry(firestoreDataRegistry); // → 'firestore-data'
hub.registerRegistry(postgresDataRegistry); // → 'postgres-data'
// Context-aware retrieval through hub
const userService = hub.get('services', ['UserService']);
const userRepo = hub.get('firestore-data', ['User'], { scopes: ['production'] });
const analyticsRepo = hub.get('postgres-data', ['Analytics']);Service Composition with RegistryHub
const hub = createRegistryHub();
// Create and register typed registries
const dataRegistry = createRegistry('data');
const servicesRegistry = createRegistry('services');
const integrationsRegistry = createRegistry('integrations');
hub.registerRegistry(dataRegistry); // → 'data'
hub.registerRegistry(servicesRegistry); // → 'services'
hub.registerRegistry(integrationsRegistry); // → 'integrations'
// Complex service with cross-registry dependencies
const orderService = servicesRegistry.createInstance(['Order'], ['business'], (registry, coordinate) => {
return {
coordinate,
registry,
createOrder: async (orderData) => {
const userRepo = hub.get('data', ['User']);
const paymentService = hub.get('integrations', ['Payment']);
const inventoryService = hub.get('services', ['Inventory']);
// Business logic using services from different typed registries
const user = await userRepo.operations.findOne(orderData.userId);
const payment = await paymentService.processPayment(orderData.payment);
const inventory = await inventoryService.reserveItems(orderData.items);
return { user, payment, inventory };
}
};
});This architecture provides the foundation for a scalable, maintainable service ecosystem where components can be developed independently but work together seamlessly. The RegistryHub with typed registries extends this capability by enabling better organization and automatic management of multiple registries across different domains or modules.
Memory Profiling and Performance
Memory Overhead Testing
The Fjell Registry includes comprehensive memory profiling to ensure optimal performance in production environments. The memory tests measure the actual memory footprint of core infrastructure components and provide automated documentation.
Running Memory Tests
# Run memory tests and generate documentation
npm run test:memoryMemory Test Coverage
The memory tests analyze:
- Registry Infrastructure: Memory overhead of creating Registry and RegistryHub instances
- Instance Creation: Memory cost per instance including coordinates and storage
- Tree Structure: Memory efficiency of the instance tree data structures
- Scoped Instances: Additional memory overhead for scoped instance management
- Scaling Behavior: Memory usage from 10 to 100,000 instances with detailed scaling analysis
Comprehensive Scaling Tests
Memory tests include scaling analysis across multiple instance counts:
- Small Scale: 10, 20, 50, 100, 200 instances
- Medium Scale: 500, 1,000, 2,000, 5,000 instances
- Large Scale: 10,000, 20,000, 50,000, 100,000 instances
Memory Constraints
Current memory constraints ensure optimal performance:
- Registry Creation: ≤ 83 kB per registry
- Instance Creation: ≤ 2.4 kB per instance
- Coordinate Objects: ≤ 1.5 kB per coordinate
- Tree Nodes: ≤ 3.9 kB per tree node
- Complex Instances: ≤ 4.9 kB for multi-level instances
Generated Documentation
Memory tests automatically generate ./docs/memory.md with:
- Detailed memory usage analysis with human-readable units (kB, MB)
- Comprehensive scaling analysis from 10 to 100,000 instances
- Performance characteristics and scaling efficiency metrics
- Memory growth patterns and per-instance consistency analysis
- Efficiency metrics and optimization recommendations
- Troubleshooting guide for memory issues
- Constraint definitions and thresholds
Key Performance Characteristics
- Linear Scaling: Memory usage scales predictably from 10 to 100,000 instances
- Consistent Per-Instance Cost: ~1.9-2.4 kB per instance across all scales
- Efficient Storage: Optimized tree structures minimize overhead
- High-Volume Performance: 100,000 instances use only ~189 MB (excellent memory efficiency)
- Scope Efficiency: Minimal additional memory for scoped instances
- Fast Creation: Instance creation remains fast even at large scales
- Human-Readable Reporting: All memory measurements displayed in kB/MB format
Use these metrics to monitor memory usage in production and ensure the registry infrastructure remains performant as your application scales from prototype to enterprise levels.
