Service Provider System Deep Dive
Introduction to Service Providers
The Hypermodern service provider system represents a significant advancement in module architecture, providing structured lifecycle management, dependency injection, and clean separation of concerns. This system bridges the gap between module metadata (manifests) and actual implementation, ensuring proper initialization order and resource cleanup.
Core Architecture
The Three-Phase Lifecycle
Service providers follow a structured lifecycle that ensures proper dependency management:
graph TD
A[Module Loading] --> B[Registration Phase]
B --> C[Initialization Phase]
C --> D[Application Running]
D --> E[Termination Phase]
E --> F[Module Unloaded]
B --> B1[Register Services]
B --> B2[Configure Dependencies]
B --> B3[No External Dependencies]
C --> C1[Initialize Services]
C --> C2[Connect to External Resources]
C --> C3[Start Background Tasks]
E --> E1[Stop Background Tasks]
E --> E2[Close Connections]
E --> E3[Clean Up Resources]
Service Provider Base Class
abstract class ModuleServiceProvider {
final ModuleManifest manifest;
final ModuleConfiguration config;
// Lifecycle state
bool get isRegistered;
bool get isInitialized;
bool get isTerminated;
// Lifecycle methods
Future<void> register();
Future<void> initialize();
Future<void> terminate();
// Configuration access
T getConfig<T>(String key);
T getConfigOrDefault<T>(String key, T defaultValue);
bool hasConfig(String key);
}
Advanced Service Provider Patterns
Factory Pattern Implementation
class DatabaseServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register database factory
registerSingleton<DatabaseFactory>(() => DatabaseFactory(
defaultDriver: getConfig<String>('default_driver'),
connections: getConfigMap<Map<String, dynamic>>('connections'),
));
// Register connection manager
registerSingleton<ConnectionManager>(() => ConnectionManager(
factory: resolve<DatabaseFactory>(),
poolSize: getConfigInt('pool_size', defaultValue: 10),
));
// Register database service
registerSingleton<DatabaseService>(() => DatabaseService(
connectionManager: resolve<ConnectionManager>(),
));
}
@override
Future<void> initialize() async {
final connectionManager = resolve<ConnectionManager>();
await connectionManager.initializeConnections();
final dbService = resolve<DatabaseService>();
await dbService.runHealthCheck();
}
}
Plugin Architecture
class PluginServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register plugin registry
registerSingleton<PluginRegistry>(() => PluginRegistry());
// Register plugin loader
registerSingleton<PluginLoader>(() => PluginLoader(
pluginDirectory: getConfig<String>('plugin_directory'),
registry: resolve<PluginRegistry>(),
));
// Register plugin manager
registerSingleton<PluginManager>(() => PluginManager(
loader: resolve<PluginLoader>(),
registry: resolve<PluginRegistry>(),
));
}
@override
Future<void> initialize() async {
final pluginManager = resolve<PluginManager>();
// Load and initialize plugins
await pluginManager.loadPlugins();
await pluginManager.initializePlugins();
// Register plugin endpoints
await pluginManager.registerEndpoints();
}
@override
Future<void> terminate() async {
final pluginManager = tryResolve<PluginManager>();
if (pluginManager != null) {
await pluginManager.shutdownPlugins();
await pluginManager.unloadPlugins();
}
}
}
Event-Driven Service Provider
class EventServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register event bus
registerSingleton<EventBus>(() => EventBus(
maxListeners: getConfigInt('max_listeners', defaultValue: 100),
enableLogging: getConfigBool('enable_logging', defaultValue: false),
));
// Register event dispatcher
registerSingleton<EventDispatcher>(() => EventDispatcher(
eventBus: resolve<EventBus>(),
));
// Register event handlers
_registerEventHandlers();
}
void _registerEventHandlers() {
final handlers = getConfigList<String>('event_handlers');
for (final handlerClass in handlers) {
registerSingleton<EventHandler>(
() => _createEventHandler(handlerClass),
name: handlerClass,
);
}
}
@override
Future<void> initialize() async {
final eventBus = resolve<EventBus>();
final dispatcher = resolve<EventDispatcher>();
// Register all event handlers with the bus
final handlerNames = getConfigList<String>('event_handlers');
for (final handlerName in handlerNames) {
final handler = resolveNamed<EventHandler>(handlerName);
await eventBus.registerHandler(handler);
}
// Start event processing
await dispatcher.start();
}
@override
Future<void> terminate() async {
final dispatcher = tryResolve<EventDispatcher>();
if (dispatcher != null) {
await dispatcher.stop();
}
final eventBus = tryResolve<EventBus>();
if (eventBus != null) {
await eventBus.shutdown();
}
}
}
Service Container Deep Dive
Advanced Container Features
class AdvancedServiceContainer extends ServiceContainer {
// Scoped services
void registerScoped<T>(T Function() factory, {String? name}) {
// Implementation for request-scoped services
}
// Conditional registration
void registerIf<T>(
bool condition,
T Function() factory, {
bool singleton = false,
String? name,
}) {
if (condition) {
register<T>(factory, singleton: singleton, name: name);
}
}
// Decorator pattern
void decorate<T>(T Function(T original) decorator) {
final original = resolve<T>();
final decorated = decorator(original);
registerInstance<T>(decorated);
}
// Lazy initialization
void registerLazy<T>(Future<T> Function() factory, {String? name}) {
// Implementation for lazy-loaded services
}
}
Service Interception
class InterceptingServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register interceptor
registerSingleton<ServiceInterceptor>(() => LoggingInterceptor());
// Register service with interception
registerSingleton<UserService>(() => InterceptedUserService(
original: UserService(),
interceptor: resolve<ServiceInterceptor>(),
));
}
}
class InterceptedUserService implements UserService {
final UserService original;
final ServiceInterceptor interceptor;
InterceptedUserService({
required this.original,
required this.interceptor,
});
@override
Future<User> getUser(int id) async {
return await interceptor.intercept(
'getUser',
{'id': id},
() => original.getUser(id),
);
}
}
Configuration Management
Advanced Configuration Patterns
class ConfigurationServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register configuration sources
registerSingleton<ConfigurationSource>(() => EnvironmentConfigurationSource());
registerSingleton<ConfigurationSource>(() => FileConfigurationSource(
filePath: getConfig<String>('config_file_path'),
), name: 'file');
// Register configuration manager
registerSingleton<ConfigurationManager>(() => ConfigurationManager([
resolve<ConfigurationSource>(),
resolveNamed<ConfigurationSource>('file'),
]));
// Register typed configuration
registerSingleton<DatabaseConfig>(() => DatabaseConfig.fromMap(
resolve<ConfigurationManager>().getSection('database'),
));
}
}
class DatabaseConfig {
final String host;
final int port;
final String database;
final String username;
final String password;
final int maxConnections;
final Duration connectionTimeout;
DatabaseConfig({
required this.host,
required this.port,
required this.database,
required this.username,
required this.password,
required this.maxConnections,
required this.connectionTimeout,
});
factory DatabaseConfig.fromMap(Map<String, dynamic> config) {
return DatabaseConfig(
host: config['host'] as String,
port: config['port'] as int,
database: config['database'] as String,
username: config['username'] as String,
password: config['password'] as String,
maxConnections: config['max_connections'] as int? ?? 10,
connectionTimeout: Duration(
seconds: config['connection_timeout_seconds'] as int? ?? 30,
),
);
}
}
Configuration Validation and Transformation
class ValidatingServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Validate configuration before registration
await _validateConfiguration();
// Transform configuration
final transformedConfig = await _transformConfiguration();
// Register services with validated config
registerSingleton<MyService>(() => MyService(transformedConfig));
}
Future<void> _validateConfiguration() async {
final validator = ConfigurationValidator([
RequiredFieldRule('api_key'),
TypeRule('port', int),
RangeRule('port', min: 1, max: 65535),
PatternRule('email', r'^[^@]+@[^@]+\.[^@]+$'),
CustomRule('database_url', _validateDatabaseUrl),
]);
final result = await validator.validate(config.getAllValues());
if (!result.isValid) {
throw ConfigurationException(
'Configuration validation failed: ${result.errors.join(', ')}',
);
}
}
Future<Map<String, dynamic>> _transformConfiguration() async {
final transformer = ConfigurationTransformer([
EnvironmentVariableTransformer(),
SecretDecryptionTransformer(),
DefaultValueTransformer(),
]);
return await transformer.transform(config.getAllValues());
}
bool _validateDatabaseUrl(dynamic value) {
if (value is! String) return false;
return Uri.tryParse(value)?.scheme != null;
}
}
Testing Service Providers
Unit Testing
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:hypermodern/modules.dart';
class MockDatabaseService extends Mock implements DatabaseService {}
class MockConfigurationService extends Mock implements ConfigurationService {}
void main() {
group('UserServiceProvider', () {
late UserServiceProvider provider;
late ServiceContainer container;
late MockDatabaseService mockDatabase;
late MockConfigurationService mockConfig;
setUp(() {
container = ServiceContainer.instance;
container.clear();
mockDatabase = MockDatabaseService();
mockConfig = MockConfigurationService();
// Pre-register mocks
container.registerInstance<DatabaseService>(mockDatabase);
container.registerInstance<ConfigurationService>(mockConfig);
provider = UserServiceProvider(
manifest: _createTestManifest(),
config: _createTestConfig(),
);
});
test('should register all required services', () async {
await provider.register();
expect(container.isRegistered<UserService>(), isTrue);
expect(container.isRegistered<UserRepository>(), isTrue);
expect(container.isRegistered<UserValidator>(), isTrue);
});
test('should initialize services with correct dependencies', () async {
await provider.register();
await provider.initialize();
final userService = container.resolve<UserService>();
expect(userService, isNotNull);
// Verify dependencies were injected
verify(mockDatabase.connect()).called(1);
});
test('should handle initialization errors gracefully', () async {
when(mockDatabase.connect()).thenThrow(Exception('Connection failed'));
await provider.register();
expect(
() => provider.initialize(),
throwsA(isA<ServiceProviderException>()),
);
});
test('should clean up resources during termination', () async {
await provider.register();
await provider.initialize();
await provider.terminate();
verify(mockDatabase.disconnect()).called(1);
expect(provider.isTerminated, isTrue);
});
});
}
Integration Testing
void main() {
group('Service Provider Integration', () {
late ModuleManager manager;
late TestDatabase testDatabase;
setUp(() async {
testDatabase = await TestDatabase.create();
manager = ModuleManager.getInstance();
await manager.initialize();
});
tearDown(() async {
await manager.stop();
await testDatabase.cleanup();
});
test('should load and start multiple modules', () async {
// Load database module
final dbResult = await manager.loadModule('database', DatabaseServiceProvider(
manifest: _createDatabaseManifest(),
config: _createDatabaseConfig(testDatabase.connectionString),
));
expect(dbResult.success, isTrue);
// Load user module (depends on database)
final userResult = await manager.loadModule('users', UserServiceProvider(
manifest: _createUserManifest(),
config: _createUserConfig(),
));
expect(userResult.success, isTrue);
// Start all modules
await manager.start();
// Verify services are available
final userService = ServiceLocator.get<UserService>();
expect(userService, isNotNull);
// Test service functionality
final user = await userService.createUser(CreateUserRequest(
email: 'test@example.com',
username: 'testuser',
));
expect(user.id, isNotNull);
});
test('should handle module dependency resolution', () async {
// Load modules in wrong order (user before database)
final userResult = await manager.loadModule('users', UserServiceProvider(
manifest: _createUserManifest(),
config: _createUserConfig(),
));
expect(userResult.success, isTrue);
final dbResult = await manager.loadModule('database', DatabaseServiceProvider(
manifest: _createDatabaseManifest(),
config: _createDatabaseConfig(testDatabase.connectionString),
));
expect(dbResult.success, isTrue);
// Should still start successfully due to proper dependency resolution
await manager.start();
final userService = ServiceLocator.get<UserService>();
expect(userService, isNotNull);
});
});
}
Performance Considerations
Lazy Loading
class LazyLoadingServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register expensive service as lazy
registerLazy<ExpensiveService>(() async {
final service = ExpensiveService();
await service.initialize();
return service;
});
// Register proxy that loads on demand
registerSingleton<ExpensiveServiceProxy>(() => ExpensiveServiceProxy(
loader: () => resolve<ExpensiveService>(),
));
}
}
class ExpensiveServiceProxy implements ExpensiveService {
final Future<ExpensiveService> Function() loader;
ExpensiveService? _instance;
ExpensiveServiceProxy({required this.loader});
@override
Future<String> performExpensiveOperation() async {
_instance ??= await loader();
return await _instance!.performExpensiveOperation();
}
}
Service Caching
class CachingServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register cache
registerSingleton<Cache>(() => MemoryCache(
maxSize: getConfigInt('cache_max_size', defaultValue: 1000),
ttl: getConfigDuration('cache_ttl'),
));
// Register cached service
registerSingleton<UserService>(() => CachedUserService(
original: UserService(),
cache: resolve<Cache>(),
));
}
}
Best Practices and Patterns
Service Provider Composition
class CompositeServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
final List<ModuleServiceProvider> providers;
CompositeServiceProvider({
required super.manifest,
required super.config,
required this.providers,
});
@override
Future<void> register() async {
for (final provider in providers) {
await provider.register();
}
}
@override
Future<void> initialize() async {
for (final provider in providers) {
await provider.initialize();
}
}
@override
Future<void> terminate() async {
// Terminate in reverse order
for (final provider in providers.reversed) {
await provider.terminate();
}
}
}
Health Check Integration
class HealthCheckServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
registerSingleton<HealthCheckService>(() => HealthCheckService());
}
@override
Future<void> initialize() async {
final healthCheck = resolve<HealthCheckService>();
// Register health checks for all services
healthCheck.register('database', () async {
final db = resolve<DatabaseService>();
return await db.isHealthy();
});
healthCheck.register('cache', () async {
final cache = resolve<CacheService>();
return await cache.ping();
});
// Start health check endpoint
await healthCheck.start();
}
}
Conclusion
The service provider system in Hypermodern provides a robust foundation for building modular, maintainable applications. By following the structured lifecycle, leveraging dependency injection, and implementing proper error handling, you can create services that are easy to test, configure, and maintain.
The system's flexibility allows for various architectural patterns while maintaining consistency and reliability across your application's modules.