Clean Architecture for AI Agents: 8 Principles for AI-Assisted Flutter Development
Uncle Bob's Clean Architecture was designed for humans. The principles assumed a developer would read code sequentially, build mental models across files, and understand implicit relationships through experience. AI agents don't work that way.
This isn't a criticism of Clean Architecture—it remains one of the most valuable frameworks for building maintainable software. But when Cursor, Copilot, or Claude are writing and refactoring your code, the traditional implementation needs to evolve. At Etere Studio, we've spent the last year adapting our clean architecture Flutter projects for AI-assisted development. Here's what we've learned.
Why Traditional Clean Architecture Struggles with AI
The core issue is context windows. AI agents can only see what you show them, and they can only hold so much information at once. Traditional Clean Architecture relies heavily on:
- Implicit layer relationships — The dependency rule is understood, not declared
- Scattered context — Related code lives in separate directories by layer
- Mental model assumptions — Developers "just know" that
UserRepositoryimplementsIUserRepository
An AI agent sees a use case that depends on IUserRepository and has no idea where the implementation lives. It might search, find three implementations, and pick the wrong one. Or worse, create a fourth.
The solution isn't abandoning Clean Architecture. It's making the implicit explicit.

The 8 Principles
These principles preserve Uncle Bob's core ideas—dependency inversion, separation of concerns, testability—while making your codebase navigable by AI agents.
Principle 1: Physical Boundaries Over Logical Ones
Traditional approach: organize by layer.
lib/
domain/
entities/
user.dart
order.dart
repositories/
user_repository.dart
order_repository.dart
data/
repositories/
user_repository_impl.dart
order_repository_impl.dart
AI-friendly approach: organize by feature with explicit layer markers.
lib/
features/
user/
domain/
user_entity.dart
user_repository.dart // interface
data/
user_repository_impl.dart
user_local_source.dart
user_remote_source.dart
presentation/
user_cubit.dart
user_screen.dart
user.dart // barrel export
order/
domain/
data/
presentation/
order.dart
When you tell an AI "modify the user repository," it can now find everything related to users in one directory tree. The layers still exist, but they're scoped to the feature.
Principle 2: Context Files as First-Class Architecture Artifacts
This is the biggest shift. Create explicit context files that AI agents can consume.

ARCHITECTURE.md at the root:
## Pattern: Clean Architecture (Feature-First)
### Dependency Rule
Dependencies flow inward: presentation → domain ← data
Domain layer has zero external dependencies.
### Layer Responsibilities
- **domain/**: Entities, repository interfaces, use cases
- **data/**: Repository implementations, data sources, DTOs
- **presentation/**: Cubits/Blocs, widgets, screens
### Conventions
- Repository interfaces: `{name}_repository.dart`
- Repository implementations: `{name}_repository_impl.dart`
- Entities: `{name}_entity.dart`
- DTOs: `{name}_dto.dart`
### State Management
Using flutter_bloc with Cubits for simple state, Blocs for complex flows.
CONTEXT.md per feature:
# User Feature
## Purpose
Handles user authentication, profile management, and preferences.
## Key Files
- `domain/user_entity.dart` - Core user model
- `domain/user_repository.dart` - Interface for user operations
- `data/user_repository_impl.dart` - Implementation using Firebase
- `presentation/user_cubit.dart` - State management for user screens
## Dependencies
- Depends on: core/network, core/storage
- Used by: features/order, features/settings
## Testing
Run: `flutter test test/features/user/`
These files cost nothing at runtime but save hours of AI confusion.
Principle 3: Constructor Injection, Always
Service locators like GetIt are convenient for humans. They're invisible to AI.
Problematic:
class UserCubit extends Cubit<UserState> {
UserCubit() : super(UserInitial()) {
_repository = GetIt.I<UserRepository>();
}
late final UserRepository _repository;
}
An AI sees UserCubit() and has no idea it depends on UserRepository. It might generate code that instantiates the cubit without registering the dependency.
AI-friendly:
class UserCubit extends Cubit<UserState> {
UserCubit({required this.repository}) : super(UserInitial());
final UserRepository repository;
}
Now the dependency is visible in the constructor signature. Any AI can see exactly what this class needs.
You can still use GetIt for wiring at the composition root—just make dependencies explicit at the class level.
Principle 4: Tests as Executable Specifications
Tests aren't just for catching bugs. They're the most reliable documentation for AI agents.
void main() {
group('UserRepository', () {
group('getUser', () {
test('returns user when API call succeeds', () async {
// Arrange
final mockApi = MockUserApi();
when(() => mockApi.fetchUser('123'))
.thenAnswer((_) async => UserDto(id: '123', name: 'John'));
final repository = UserRepositoryImpl(api: mockApi);
// Act
final result = await repository.getUser('123');
// Assert
expect(result, isA<User>());
expect(result.name, equals('John'));
});
test('throws UserNotFoundException when user does not exist', () async {
// Arrange
final mockApi = MockUserApi();
when(() => mockApi.fetchUser('999'))
.thenThrow(ApiException(statusCode: 404));
final repository = UserRepositoryImpl(api: mockApi);
// Act & Assert
expect(
() => repository.getUser('999'),
throwsA(isA<UserNotFoundException>()),
);
});
});
});
}
When you ask an AI to "add caching to the user repository," well-structured tests tell it:
- What the current behavior is
- What patterns to follow
- What edge cases to handle
Tests enable safe AI refactoring. Without them, you're gambling.
Principle 5: Explicit Error Types Over Generic Exceptions
Generic exceptions hide behavior from AI agents.
Problematic:
Future<User> getUser(String id) async {
try {
return await api.fetchUser(id);
} catch (e) {
throw Exception('Failed to get user');
}
}
AI-friendly:
sealed class UserError {
const UserError();
}
class UserNotFoundError extends UserError {
const UserNotFoundError(this.userId);
final String userId;
}
class UserNetworkError extends UserError {
const UserNetworkError(this.message);
final String message;
}
Future<Either<UserError, User>> getUser(String id) async {
try {
final dto = await api.fetchUser(id);
return Right(dto.toEntity());
} on ApiException catch (e) {
if (e.statusCode == 404) {
return Left(UserNotFoundError(id));
}
return Left(UserNetworkError(e.message));
}
}
Sealed classes with exhaustive pattern matching make error handling visible and verifiable. AI agents can see all possible outcomes and generate appropriate handling code.
Principle 6: Co-locate Interface and Implementation
Traditional Clean Architecture puts interfaces in domain/ and implementations in data/. This separation makes sense conceptually but creates navigation problems.
Alternative structure:
features/user/
domain/
user_repository.dart // interface
user_repository_impl.dart // implementation
user_entity.dart
Wait—implementation in domain? Not exactly. The file lives in domain/ but the class still depends only on domain concepts. What we're co-locating is the contract and its primary implementation.
For cases with multiple implementations (like a FakeUserRepository for testing), keep the fake in test/ or a dedicated fakes/ directory.
Principle 7: Barrel Exports with Visibility Control
Barrel files (user.dart that exports everything from a feature) help AI agents understand public APIs.
// features/user/user.dart
// Public API - what other features can use
export 'domain/user_entity.dart';
export 'domain/user_repository.dart';
export 'presentation/user_cubit.dart';
export 'presentation/user_screen.dart';
// Internal - don't export
// data/user_repository_impl.dart
// data/user_dto.dart
// data/user_local_source.dart
When an AI sees import 'package:app/features/user/user.dart', it knows exactly what's available without crawling the directory.
Principle 8: Document the "Why" in Code Comments
AI agents are excellent at understanding what code does. They struggle with why certain decisions were made.
/// Repository for user operations.
///
/// Uses Firebase Auth for authentication and Firestore for profile data.
/// Profile data is cached locally for 24 hours to reduce Firestore reads
/// (billing optimization - we hit 50k reads/day in production).
///
/// See: https://firebase.google.com/pricing for read limits
abstract class UserRepository {
/// Fetches user by ID.
///
/// Returns cached data if available and less than 24 hours old.
/// Throws [UserNotFoundException] if user doesn't exist in Firebase.
/// Throws [UserNetworkError] for connectivity issues.
Future<User> getUser(String id);
}
The "billing optimization" comment prevents an AI from removing the cache in a refactor because it "simplifies the code."
Comparison: Traditional vs AI-Friendly Clean Architecture
| Aspect | Traditional | AI-Friendly |
|---|---|---|
| Organization | By layer | By feature, then layer |
| Dependencies | Service locator | Constructor injection |
| Context | Implicit (developer knowledge) | Explicit (CONTEXT.md files) |
| Interfaces | Separate from implementation | Co-located with primary impl |
| Errors | Generic exceptions | Sealed class hierarchies |
| Public API | Implicit | Barrel exports |
| Rationale | In developer's head | In code comments |
Practical Migration Path
You don't need to rewrite your codebase. Start with:
- Add ARCHITECTURE.md — 30 minutes, immediate benefit
- Add CONTEXT.md to your most complex feature — Test the impact
- Convert service locator to constructor injection — One class at a time
- Reorganize one feature — See if the structure helps your AI workflow
At Etere Studio, we migrated a 50k LOC Flutter app over 6 weeks. The AI productivity gains were noticeable within the first week, just from adding context files.
When This Doesn't Apply
These principles optimize for AI-assisted development. If you're:
- Working solo without AI tools
- On a small project (under 10k LOC)
- Using a framework with strong conventions (like Rails)
…traditional Clean Architecture might serve you better. The overhead of context files and explicit everything isn't free.
But if you're building a production Flutter app with a team, and you're using AI coding assistants daily? These adaptations pay for themselves quickly.
The goal isn't to replace Clean Architecture—it's to make it legible to a new kind of collaborator. AI agents are increasingly part of how we build software. Our architecture should acknowledge that.
Working on a Flutter project and want to discuss architecture approaches? Let's talk.