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.
When to Split
Not every feature needs a split. Use this decision tree:
Split when:
- Other features depend on this feature's contracts
- You might 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 Dual-GetIt pattern wires everything together at the app level. Feature 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
- FeatureSDK Self-Registration — How features discover and register themselves