Skip to content

Dual-GetIt DI Pattern

What you'll learn

  • Why Archipelago uses two GetIt instances (global + local per feature)
  • How infrastructure SDKs register in the global scope
  • How features create isolated local scopes via shell pages
  • The lifecycle: mount → register → use → dispose → reset

The Problem

In a monorepo with many feature modules, registering everything in a single DI container causes:

  • Naming collisions between features
  • Leaked state when navigating between features
  • Tight coupling — features can accidentally depend on each other's internals

The Solution: Global + Local GetIt

Archipelago uses two GetIt scopes:

Global Scope (GetIt.instance)

Shared infrastructure available everywhere — registered once at app startup.

dart
// apps/template_app/lib/di/injector.dart
final getIt = GetIt.instance;

@InjectableInit(
  externalPackageModulesBefore: [
    ExternalModule(AppUtilitiesPackageModule),
    ExternalModule(NetworkSdkDioPackageModule),
    ExternalModule(MonitoringImplPackageModule),
    ExternalModule(RouterRegistryPackageModule),
    // Feature SDKs (eager — triggers self-registration)
    ExternalModule(AuthImplGlobalModule),
    ExternalModule(HomeImplGlobalModule),
  ],
)
Future<void> initializeInjector() => $initGetIt(getIt);

Infrastructure packages expose their module via @InjectableInit.microPackage():

dart
// infrastructure/network_sdk_dio/lib/src/di/injector.dart
@InjectableInit.microPackage()
void initMicroPackage(GetIt getIt) => $initMicroPackage(getIt);

Local Scope (GetIt.asNewInstance() per feature)

Each feature creates its own isolated GetIt instance for internal deps (repos, datasources, usecases):

dart
// features/auth/auth_impl/lib/src/di/auth_local_module.dart
final GetIt authGetIt = GetIt.asNewInstance();

void initAuthLocalModule() => initAuthLocalDI(authGetIt);
void resetAuthLocalModule() => authGetIt.reset();

Shell Page Lifecycle

The local DI scope is tied to the feature's shell page — a StatefulWidget that wraps all nested routes:

dart
// features/auth/auth_impl/lib/src/presentation/auth_shell_page.dart
@RoutePage()
class AuthShellPage extends StatefulWidget { ... }

class _AuthShellPageState extends State<AuthShellPage> {
  @override
  void initState() {
    super.initState();
    initAuthLocalModule();  // Register local deps
  }

  @override
  void dispose() {
    resetAuthLocalModule(); // Reset for clean re-entry
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => const AutoRouter();
}

When the user navigates to /auth, the shell mounts and registers local deps. When they navigate away, dispose() resets the container for a clean slate.

Accessing Dependencies

dart
// Global deps — available anywhere
final networkClient = getIt<NetworkClient>();
final authSDK = getIt<AuthSDK>();

// Local deps — only within the feature
final loginUseCase = authGetIt<LoginUseCase>();
final loginRepo = authGetIt<LoginRepository>();

When to Use Each Scope

ScopeRegisterExamples
GlobalApp startup (eager singletons)SDKs, network, monitoring, auth state
LocalShell page initState()Repos, datasources, usecases, BLoCs

Key Rules

  1. FeatureSDKs are global — registered as eager singletons so FeatureSDKRegistry is populated before pre-launch
  2. Internal deps are local — repos, datasources, usecases stay in the feature's own GetIt
  3. Local deps can access globalauthGetIt can resolve from getIt if needed via GetIt.instance
  4. Reset on disposeauthGetIt.reset() ensures no leaked state between feature visits

Next Steps

Built by Banua Coder