Clean Architecture & SOLID
Why Architecture Matters
As a Dart/Flutter application grows, without deliberate architecture, it becomes a "Big Ball of Mud" — a spaghetti of UI code directly calling API endpoints, business logic tangled with state management, and tests that are impossible to write. Clean Architecture prevents this.
Clean Architecture by Robert C. Martin defines a separation of concerns that makes software testable, maintainable, and independent of frameworks, databases, and UI.
The Three Layers
┌──────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ Widgets, Screens, ViewModels/BLoCs │
│ (knows about domain, not data) │
├──────────────────────────────────────────┤
│ DOMAIN LAYER │
│ Entities, Use Cases, Repository Interfaces│
│ (pure Dart — no Flutter, no http) │
├──────────────────────────────────────────┤
│ DATA LAYER │
│ Repository Implementations, APIs, DB │
│ (knows about domain interfaces) │
└──────────────────────────────────────────┘
↑ Dependency direction: inward only
Domain Layer — The Core
dart// Entity — pure data, no framework dependencies class User { final String id; final String name; final String email; const User({required this.id, required this.name, required this.email}); } // Repository interface — defined in domain, implemented in data abstract interface class UserRepository { Future<User?> findById(String id); Future<void> save(User user); } // Use Case — single piece of business logic class GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future<User?> execute(String userId) async { if (userId.isEmpty) throw ArgumentError('userId cannot be empty'); return _repository.findById(userId); } }
SOLID Principles in Dart
dart// S — Single Responsibility Principle // Each class has ONE reason to change class UserValidator { bool isValid(User u) => u.email.contains('@'); } class UserRepository { Future<void> save(User u) async { /* persist */ } } class UserNotifier { void notify(User u) { /* send email */ } } // NOT: class UserService { validate + save + notify } // THREE responsibilities // O — Open/Closed Principle // Open for extension, closed for modification abstract class Discount { double apply(double price); } class PercentDiscount extends Discount { final double percent; PercentDiscount(this.percent); double apply(double price) => price * (1 - percent / 100); } class FixedDiscount extends Discount { final double amount; FixedDiscount(this.amount); double apply(double price) => price - amount; } // Add new discount types WITHOUT changing existing code // L — Liskov Substitution Principle // Subtypes must be substitutable for their base types void applyDiscount(Discount discount, double price) { final newPrice = discount.apply(price); // works with ANY Discount print(newPrice); } // I — Interface Segregation Principle // Many small interfaces > one large interface abstract interface class Readable { Future<String> read(String id); } abstract interface class Writable { Future<void> write(String id, String data); } // Classes implement only what they need // D — Dependency Inversion Principle // High-level modules depend on abstractions, not concretions class OrderService { final UserRepository _users; // abstraction, not RemoteUserRepository final PaymentGateway _payments; // abstraction OrderService(this._users, this._payments); }
Dependency Injection
dart// Without DI — tightly coupled, untestable class BadOrderService { final _repo = RemoteUserRepository(); // hardcoded } // With DI — flexible, testable class GoodOrderService { final UserRepository _repo; GoodOrderService(this._repo); // injected from outside } // In tests: final service = GoodOrderService(MockUserRepository()); // In production: final service = GoodOrderService(RemoteUserRepository(apiClient));
Folder Structure for Clean Architecture
lib/
├── core/ # Shared utilities, constants, errors
│ ├── errors/
│ └── utils/
├── features/
│ └── auth/
│ ├── domain/
│ │ ├── entities/user.dart
│ │ ├── repositories/user_repository.dart # interface
│ │ └── usecases/login_usecase.dart
│ ├── data/
│ │ ├── datasources/auth_remote_datasource.dart
│ │ └── repositories/user_repository_impl.dart
│ └── presentation/
│ ├── screens/login_screen.dart
│ └── bloc/auth_bloc.dart
Summary
Clean Architecture combined with SOLID principles produces Dart/Flutter code that can grow to hundreds of thousands of lines without collapsing under its own complexity. The domain layer is pure Dart with no dependencies — fully unit testable. The data layer can be swapped (REST API today, GraphQL tomorrow). The presentation layer can be rebuilt without touching business logic. This is what separates a maintainable production application from a prototype.