Datasource Module Pattern
Datasource Module Pattern
Section titled âDatasource Module PatternâThe Datasource Module pattern provides a clean way to organize and register GraphQL request strategies and cache handlers, avoiding circular dependencies and maintaining separation of concerns.
Overview
Section titled âOverviewâThe Datasource Module pattern solves the problem of strategy registration by:
- Centralizing Strategy Registration: All strategies for a feature are registered in one place
- Avoiding Circular Dependencies: Module creates datasource with strategies, not the other way around
- Enabling Dependency Injection: Works seamlessly with Riverpod providers
- Supporting Testing: Easy to mock and test individual strategies
Core Components
Section titled âCore Componentsâ1. Datasource Module Interface
Section titled â1. Datasource Module Interfaceâabstract class DatasourceModule<T> { T create(GqlClient client, RequestContext requestContext);}2. Request Strategy Keys
Section titled â2. Request Strategy KeysâEnum-based keys for type-safe strategy registration:
enum HotelBookingsRequestStrategyKeys { hotelBookingsList, hotelBookingsCount, hotelBookingsRequestedCount, hotelBookingsCompletedCount, hotelBookingsCanceledCount, hotelBookingsAllCount, hotelBookingsUpsert, hotelBookingsDelete,}3. Module Implementation
Section titled â3. Module Implementationâclass HotelBookingsDatasourceModule implements DatasourceModule<HotelBookingsDatasource> {
@override HotelBookingsDatasource create( GqlClient client, RequestContext requestContext, ) { final datasource = HotelBookingsDatasource( client: client, requestContext: requestContext, );
// Register all request strategies _registerRequestStrategies(requestContext);
// Register all cache handlers _registerCacheHandlers(requestContext);
return datasource; }
void _registerRequestStrategies(RequestContext requestContext) { // List strategies requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsList.name, HotelBookingsListRequestStrategy(), );
// Count strategies with inheritance requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsCount.name, HotelBookingsCountRequestStrategy(), );
requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsRequestedCount.name, HotelBookingsRequestedCountRequestStrategy(), );
requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsCompletedCount.name, HotelBookingsCompletedCountRequestStrategy(), );
// Mutation strategies requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, HotelBookingsUpsertRequestStrategy(), ); }
void _registerCacheHandlers(RequestContext requestContext) { // Upsert cache handler (complex example) requestContext.registerCacheHandler( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, HotelBookingsUpsertCacheHandlerStrategy(), );
// Delete cache handler requestContext.registerCacheHandler( HotelBookingsRequestStrategyKeys.hotelBookingsDelete.name, HotelBookingsDeleteCacheHandlerStrategy(), ); }}Strategy Registration Patterns
Section titled âStrategy Registration Patternsâ1. Basic Strategy Registration
Section titled â1. Basic Strategy Registrationâvoid _registerBasicStrategies(RequestContext requestContext) { requestContext.registerStrategy( 'strategyKey', ConcreteStrategy(), );}2. Inherited Strategy Registration
Section titled â2. Inherited Strategy RegistrationâFor strategies that share common behavior:
void _registerCountStrategies(RequestContext requestContext) { // Base count strategy requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsCount.name, HotelBookingsCountRequestStrategy(), );
// Status-specific count strategies (inherit from base) requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsRequestedCount.name, HotelBookingsRequestedCountRequestStrategy(), );
requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsCompletedCount.name, HotelBookingsCompletedCountRequestStrategy(), );}3. Conditional Strategy Registration
Section titled â3. Conditional Strategy RegistrationâRegister strategies based on configuration or feature flags:
void _registerConditionalStrategies(RequestContext requestContext) { // Always register basic strategies requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsList.name, HotelBookingsListRequestStrategy(), );
// Conditionally register advanced strategies if (FeatureFlags.advancedBookingEnabled) { requestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsAdvanced.name, HotelBookingsAdvancedRequestStrategy(), ); }}Cache Handler Registration
Section titled âCache Handler Registrationâ1. Simple Cache Handler
Section titled â1. Simple Cache Handlerâvoid _registerSimpleCacheHandlers(RequestContext requestContext) { requestContext.registerCacheHandler( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, HotelBookingsSimpleCacheHandlerStrategy(), );}2. Complex Cache Handler
Section titled â2. Complex Cache HandlerâFor handlers that manage multiple cache types:
void _registerComplexCacheHandlers(RequestContext requestContext) { // Complex handler that updates multiple caches requestContext.registerCacheHandler( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, HotelBookingsUpsertCacheHandlerStrategy(), // 1800+ lines );}Riverpod Integration
Section titled âRiverpod IntegrationâProvider Registration
Section titled âProvider Registrationâ@riverpodHotelBookingsDatasource hotelBookingsDatasource( HotelBookingsDatasourceRef ref,) { final client = ref.watch(gqlClientProvider); final requestContext = ref.watch(requestContextProvider);
return HotelBookingsDatasourceModule().create(client, requestContext);}Repository Integration
Section titled âRepository Integrationâ@riverpodIHotelBookingsRepository hotelBookingsRepository( HotelBookingsRepositoryRef ref,) { final datasource = ref.watch(hotelBookingsDatasourceProvider); return HotelBookingsRepository(datasource);}Testing the Module
Section titled âTesting the ModuleâUnit Testing Strategies
Section titled âUnit Testing Strategiesâvoid main() { group('HotelBookingsDatasourceModule', () { late MockGqlClient mockClient; late MockRequestContext mockRequestContext; late HotelBookingsDatasourceModule module;
setUp(() { mockClient = MockGqlClient(); mockRequestContext = MockRequestContext(); module = HotelBookingsDatasourceModule(); });
test('creates datasource with all strategies registered', () { // Act final datasource = module.create(mockClient, mockRequestContext);
// Assert expect(datasource, isA<HotelBookingsDatasource>());
// Verify all strategies were registered verify(() => mockRequestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsList.name, any(), )).called(1);
verify(() => mockRequestContext.registerStrategy( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, any(), )).called(1); });
test('registers cache handlers correctly', () { // Act module.create(mockClient, mockRequestContext);
// Assert verify(() => mockRequestContext.registerCacheHandler( HotelBookingsRequestStrategyKeys.hotelBookingsUpsert.name, any(), )).called(1); }); });}Integration Testing
Section titled âIntegration Testingâvoid main() { group('HotelBookingsDatasource Integration', () { late ProviderContainer container;
setUp(() { container = ProviderContainer( overrides: [ gqlClientProvider.overrideWithValue(mockClient), requestContextProvider.overrideWithValue(mockRequestContext), ], ); });
test('datasource executes strategies correctly', () async { // Arrange final datasource = container.read(hotelBookingsDatasourceProvider);
// Act final result = await datasource.queryList().first;
// Assert expect(result.data, isNotNull); }); });}Benefits
Section titled âBenefitsâ1. Separation of Concerns
Section titled â1. Separation of Concernsâ- Module handles registration logic
- Datasource focuses on business operations
- Strategies handle specific request/cache logic
2. Testability
Section titled â2. Testabilityâ- Easy to mock individual strategies
- Module can be tested independently
- Clear dependency injection points
3. Maintainability
Section titled â3. Maintainabilityâ- All strategies for a feature in one place
- Easy to add/remove strategies
- Clear registration patterns
4. Type Safety
Section titled â4. Type Safetyâ- Enum-based strategy keys prevent typos
- Compile-time verification of strategy registration
- Clear interfaces for all components
Common Patterns
Section titled âCommon Patternsâ1. Feature-Based Modules
Section titled â1. Feature-Based ModulesâOne module per feature (recommended):
HotelBookingsDatasourceModuleEventsDatasourceModuleUsersDatasourceModule2. Layer-Based Modules
Section titled â2. Layer-Based ModulesâOne module per operation type:
QueryDatasourceModuleMutationDatasourceModuleSubscriptionDatasourceModule3. Hybrid Modules
Section titled â3. Hybrid ModulesâCombination of feature and operation:
HotelBookingsQueryModuleHotelBookingsMutationModuleBest Practices
Section titled âBest Practicesâ- Use Enum Keys: Always use enum-based strategy keys for type safety
- Register All Strategies: Ensure all strategies are registered in the module
- Test Registration: Verify all strategies are properly registered
- Document Complex Handlers: Add comments for complex cache handlers
- Keep Modules Focused: One module per feature or logical grouping
- Use Consistent Naming: Follow naming conventions for strategies and handlers
The Datasource Module pattern provides a clean, testable, and maintainable way to organize GraphQL strategies while avoiding circular dependencies and supporting proper dependency injection.