Clean Architecture Overview
Building a skyscraper with a solid blueprint
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Clean Architecture is a software design philosophy that separates an application into distinct layers with clear responsibilities and a strict dependency rule: inner layers never depend on outer layers. In team_mvp_kit, the four layers are Domain (business logic and entities), Data (API calls, DTOs, and storage), Presentation (UI and BLoCs), and Infrastructure (DI, routing, and shared utilities). Code is organized feature-first, so each feature contains its own domain, data, and presentation folders.
Real-world relevance
Clean Architecture is the reason team_mvp_kit can scale from a starter kit to a full production app without rewriting. When the team decided to switch from one payment provider to another, they only changed the data layer. When adding offline support, they added a local data source and updated the repository without changing a single line in domain or presentation.
Key points
- Why Architecture Matters — Without architecture, small apps are fine. But as apps grow, code becomes tangled, changes break unrelated features, and testing becomes impossible. Architecture provides guardrails that keep complexity manageable.
- The Dependency Rule — The most important rule: dependencies point inward. Outer layers know about inner layers, but inner layers never know about outer layers. Domain knows nothing about Flutter, databases, or APIs.
- The Four Layers — team_mvp_kit uses four layers: Domain (entities, use cases, repository interfaces), Data (DTOs, data sources, repository implementations), Presentation (BLoCs, screens, widgets), and Infrastructure (DI setup, routing, shared utilities).
- Domain Layer — The domain layer is the heart of the application. It contains business entities, use case classes, and repository interfaces. It has zero dependencies on Flutter, packages, or external services.
- Data Layer — The data layer implements the repository interfaces defined in the domain layer. It handles API calls, database queries, caching, and data transformation between DTOs and entities.
- Presentation Layer — The presentation layer contains Flutter widgets, screens, and BLoCs. It depends on the domain layer (use cases and entities) but never directly touches data sources or APIs.
- Infrastructure Layer — The infrastructure layer provides cross-cutting concerns: dependency injection, router configuration, network client setup, and shared utilities that do not belong to a single feature.
- Feature-First Organization — team_mvp_kit organizes code by feature first, then by layer. Each feature folder contains its own domain, data, and presentation subfolders, making features self-contained.
- Benefits of Clean Architecture — Testability: mock any layer. Flexibility: swap databases without touching UI. Scalability: add features without breaking existing ones. Onboarding: new developers know where everything goes.
Code example
// Complete data flow through layers:
//
// 1. DOMAIN: Entity (pure Dart)
class Product {
final String id;
final String name;
final double price;
final String imageUrl;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
}
// 2. DOMAIN: Repository interface
abstract class ProductRepository {
Future<List<Product>> getAll();
Future<Product> getById(String id);
}
// 3. DOMAIN: Use case
class GetProductsUseCase {
final ProductRepository _repository;
const GetProductsUseCase(this._repository);
Future<List<Product>> call() {
return _repository.getAll();
}
}
// 4. DATA: DTO (data transfer object)
class ProductDto {
final String id;
final String name;
final double price;
final String imageUrl;
const ProductDto({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
});
factory ProductDto.fromJson(Map<String, dynamic> json) {
return ProductDto(
id: json['id'] as String,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
imageUrl: json['image_url'] as String,
);
}
Product toEntity() {
return Product(
id: id,
name: name,
price: price,
imageUrl: imageUrl,
);
}
}
// 5. DATA: Repository implementation
class ProductRepositoryImpl implements ProductRepository {
final ProductRemoteDataSource _remote;
const ProductRepositoryImpl(this._remote);
@override
Future<List<Product>> getAll() async {
final dtos = await _remote.fetchProducts();
return dtos.map((dto) => dto.toEntity()).toList();
}
@override
Future<Product> getById(String id) async {
final dto = await _remote.fetchProductById(id);
return dto.toEntity();
}
}
// 6. PRESENTATION: BLoC uses use case
class ProductListBloc
extends Bloc<ProductEvent, ProductState> {
final GetProductsUseCase _getProducts;
ProductListBloc(this._getProducts)
: super(const ProductState()) {
on<LoadProducts>(_onLoad);
}
Future<void> _onLoad(
LoadProducts event,
Emitter<ProductState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final products = await _getProducts();
emit(state.copyWith(
products: products,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
failure: ServerFailure(e.toString()),
));
}
}
}Line-by-line walkthrough
- 1. The Product entity is a pure Dart class with no framework dependencies.
- 2. ProductRepository is an abstract class defining the contract that the data layer must fulfill.
- 3. GetProductsUseCase wraps a single business operation and depends only on the repository interface.
- 4. ProductDto mirrors the entity but adds fromJson for deserialization and toEntity for conversion.
- 5. The fromJson factory handles JSON field types, including snake_case to camelCase conversion.
- 6. toEntity converts the DTO to a domain entity, decoupling API format from business format.
- 7. ProductRepositoryImpl implements the interface by delegating to the remote data source.
- 8. The getAll method fetches DTOs, then maps each to an entity using toEntity.
- 9. ProductListBloc depends on the use case, not the repository or data source directly.
- 10. The _onLoad handler follows the loading/success/failure pattern from Lesson 35.
- 11. The flow: Screen event -> BLoC -> UseCase -> Repository interface -> Implementation -> DataSource.
Spot the bug
// domain/usecases/get_products_usecase.dart
import 'package:dio/dio.dart';
import '../repositories/product_repository.dart';
class GetProductsUseCase {
final ProductRepository _repository;
GetProductsUseCase(this._repository);
Future<List<Product>> call() async {
try {
return await _repository.getProducts();
} on DioException catch (e) {
throw ServerFailure(e.message ?? 'Error');
}
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Flutter App Architecture Guide (flutter.dev)
- Clean Architecture by Uncle Bob (cleancoder.com)
- Very Good Architecture (VGV) (verygood.ventures)