Clean Architecture: Layers & Principles
Clean Architecture: Layers & Principles
Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is a software design philosophy that separates an application into concentric layers of responsibility. In Flutter, this means structuring your codebase so that business logic is completely independent of the UI framework, databases, and external services. The result is code that is testable, maintainable, and adaptable to change.
The Three Concentric Layers
Clean Architecture in Flutter is typically organised into three layers arranged from innermost (most stable) to outermost (most volatile):
1. Domain Layer (Innermost)
The Domain layer is the heart of the application. It contains pure Dart code — no Flutter imports, no third-party packages, no network or database dependencies. It defines:
- Entities — plain Dart classes that model your core business objects (e.g.,
User,Product,Order) - Repository interfaces (abstract classes) — contracts that describe what data operations are possible, without specifying how they are performed
- Use cases (interactors) — single-responsibility classes that execute one business rule (e.g.,
GetUserByIdUseCase,PlaceOrderUseCase)
Because the Domain layer has no external dependencies, it can be unit-tested with plain dart test — no mocking of Flutter internals required.
2. Data Layer (Middle)
The Data layer implements the repository interfaces defined in the Domain layer. It is responsible for all data operations and knows about external systems such as REST APIs, local SQLite databases, Firebase, and device storage. It contains:
- Repository implementations — concrete classes that fulfil the domain contracts
- Data sources — remote (HTTP clients) and local (database helpers, shared preferences)
- Data models (DTOs) — classes with
fromJson/toJsonthat map raw API/database responses to domain entities
3. Presentation Layer (Outermost)
The Presentation layer is where Flutter lives. Widgets, pages, and state management (Bloc, Riverpod, Provider, etc.) belong here. It calls use cases from the Domain layer and renders their results. It contains:
- Pages / Screens — Flutter
Widgetsubclasses that build the UI - State management classes — Cubits, ViewModels, Notifiers, Providers
- Shared UI widgets — reusable components specific to this app
The Dependency Rule
The single most important rule of Clean Architecture is the Dependency Rule:
Source code dependencies must point inward only. No inner layer may know anything about an outer layer.
Concretely:
- The Domain layer imports nothing from Data or Presentation
- The Data layer imports Domain (to implement interfaces and return entities), but never Presentation
- The Presentation layer imports Domain (to call use cases), and may import Data only through dependency injection — never directly calling a repository implementation by name
UserModel (a Data-layer DTO) directly into a Cubit or widget is a dependency rule violation. The Presentation layer should only ever see User (the Domain entity). The mapping happens inside the repository implementation in the Data layer.Recommended Flutter Folder Structure
A clean Flutter project organises code by feature, with each feature containing its own three sub-folders:
lib/
features/
auth/
domain/
entities/
user.dart // Pure Dart entity
repositories/
auth_repository.dart // Abstract interface
usecases/
login_usecase.dart
logout_usecase.dart
data/
models/
user_model.dart // DTO with fromJson/toJson
datasources/
auth_remote_datasource.dart
auth_local_datasource.dart
repositories/
auth_repository_impl.dart
presentation/
pages/
login_page.dart
widgets/
login_form.dart
bloc/
auth_bloc.dart
auth_event.dart
auth_state.dart
core/
error/
failures.dart
exceptions.dart
usecases/
usecase.dart // Base UseCase interface
di/
injection_container.dart // Dependency injection setup
Domain Layer Code Example
Here is how the Domain layer looks in pure Dart — no Flutter or third-party imports:
// lib/features/auth/domain/entities/user.dart
class User {
final String id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
}
// lib/features/auth/domain/repositories/auth_repository.dart
// Abstract contract — Data layer must implement this
abstract class AuthRepository {
Future<User> login({required String email, required String password});
Future<void> logout();
Future<User?> getCurrentUser();
}
// lib/features/auth/domain/usecases/login_usecase.dart
class LoginUseCase {
final AuthRepository repository;
const LoginUseCase(this.repository);
// Single public method — encapsulates one business action
Future<User> call({required String email, required String password}) {
return repository.login(email: email, password: password);
}
}
Data Layer Code Example
The Data layer implements the contract and handles JSON mapping. Notice it imports the Domain entity but never imports the Presentation layer:
// lib/features/auth/data/models/user_model.dart
import 'package:myapp/features/auth/domain/entities/user.dart';
class UserModel extends User {
const UserModel({
required super.id,
required super.name,
required super.email,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
// lib/features/auth/data/repositories/auth_repository_impl.dart
import 'package:myapp/features/auth/domain/entities/user.dart';
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';
import 'package:myapp/features/auth/data/datasources/auth_remote_datasource.dart';
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
const AuthRepositoryImpl(this.remoteDataSource);
@override
Future<User> login({required String email, required String password}) async {
// Returns a UserModel (Data), but the caller only sees User (Domain)
return remoteDataSource.login(email: email, password: password);
}
@override
Future<void> logout() => remoteDataSource.logout();
@override
Future<User?> getCurrentUser() => remoteDataSource.getCurrentUser();
}
Summary
Clean Architecture divides a Flutter app into three layers: Domain (pure business logic and entities), Data (repositories and data sources), and Presentation (Flutter widgets and state management). The Dependency Rule requires that every dependency arrow points inward — outer layers depend on inner layers, never the reverse. Enforcing this boundary keeps the Domain and its use cases fully testable in isolation and makes the entire codebase resilient to change.
import 'dart:async'; or other Dart-core imports, it probably does not belong in the Domain layer.