Architecture Deep Dives
Understanding API/Impl Split
What you'll learn
- When to use the API/Impl split pattern
- How the split enforces clean architecture boundaries
- The relationship between API, Impl, and Noop packages
- Real-world examples from the Archipelago template
The Problem
In a modular monorepo, features inevitably depend on each other. Without boundaries, you end up with:
- Circular dependencies between feature packages
- Tight coupling to specific implementations
- Inability to swap vendors without touching consumers
- Test setups that require spinning up real services
The Solution: API/Impl Split
The API/Impl split separates what a feature does from how it does it:
feature_x/
├── feature_x_api/ # Contracts: interfaces, models, exceptions
└── feature_x_impl/ # Implementation: datasources, repos, DIOther features depend only on feature_x_api. They never import feature_x_impl directly.
Here is that rule on real code. The Flagr feature-flag provider depends on the HttpClient contract in network_sdk_api. Switching from Dio to package:http changes the app's wiring only:
dependencies: network_sdk_api: path: ../../infrastructure/network_sdk_api # … network_sdk_dio: path: ../../infrastructure/network_sdk_dioimport 'package:network_sdk_dio/network_sdk_dio.dart';// … externalPackageModulesBefore: [ ExternalModule(NetworkSdkDioPackageModule),class DioHttpClientAdapter(this._dio) implements api.HttpClient { @override Future<api.HttpResponse> post(String path, {Object? body, /* … */}) => _send('POST', path, body: body, /* … */);}When to Split
Not every feature needs a split. Use this decision tree:
Split when:
- Other features depend on this feature's contracts
- You want to swap the underlying vendor (e.g., Firebase to Supabase)
- The feature has a noop variant for build-time stripping
Don't split when:
- The feature is self-contained (no external consumers)
- It's a simple utility package
- It's a debug-only tool (use standalone noop pattern instead)
API Package Anatomy
The API package contains only contracts:
// feature_x_api/lib/src/repositories/feature_x_repository.dart
abstract class FeatureXRepository {
Future<List<Item>> getItems();
Future<void> createItem(CreateItemRequest request);
}// feature_x_api/lib/src/models/item.dart
class Item {
final String id;
final String name;
const Item({required this.id, required this.name});
}No implementation details. No third-party dependencies (except shared models).
Impl Package Anatomy
The impl package provides the concrete implementation:
// feature_x_impl/lib/src/repositories/feature_x_repository_impl.dart
class FeatureXRepositoryImpl implements FeatureXRepository {
final FeatureXRemoteDataSource _remote;
const FeatureXRepositoryImpl(this._remote);
@override
Future<List<Item>> getItems() => _remote.fetchItems();
}The impl package depends on the API package and any vendor SDKs it needs.
Connecting with DI
The global GetIt wires everything together at the app level. Consumers request contracts from GetIt, and the DI configuration decides which implementation to provide.
Learn more about this in Dual-GetIt DI Pattern.
Next Steps
- Dual-GetIt DI Pattern: How global and local DI containers work together
- Feature Manifests and SDKs: How features register routes and stay lazy