Skip to content

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, DI

Other 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:

dart
// feature_x_api/lib/src/repositories/feature_x_repository.dart
abstract class FeatureXRepository {
  Future<List<Item>> getItems();
  Future<void> createItem(CreateItemRequest request);
}
dart
// 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:

dart
// 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

Built by Banua Coder