Domain Layer Guide
The domain layer contains the core business logic and entities of your feature. It's the innermost layer that defines what your application does, independent of external concerns like UI or data storage.
Structure
domain/
├── entities/
│ └── feature_entity.dart
└── repositories/
└── feature_repository.dart1. Creating Entities
Entities represent your core business objects. They should contain business logic and be independent of external frameworks.
Entity Template
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'feature_entity.freezed.dart';
/// Represents a [FeatureName] in the application.
///
/// Contains the [FeatureName]'s properties and business logic.
/// Used throughout the [FeatureName] domain layer for operations.
@freezed
abstract class FeatureName with _$FeatureName {
/// Creates a [FeatureName] with the specified properties.
///
/// [id] - Unique identifier for the [FeatureName] (required).
/// [property1] - Description of property1 (required).
/// [property2] - Description of property2 (optional).
/// [createdAt] - Timestamp when created (required).
/// [updatedAt] - Timestamp when last modified (required).
const factory FeatureName({
required String id,
required String property1,
String? property2,
required DateTime createdAt,
required DateTime updatedAt,
}) = _FeatureName;
const FeatureName._();
/// Creates an empty [FeatureName] for initial state.
///
/// Useful as a default or initial state when no [FeatureName] is selected.
factory FeatureName.empty() => _FeatureName(
id: '',
property1: '',
property2: null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}Key Points for Entities
- Use Freezed: All entities should use
@freezedfor immutability and code generation - Documentation: Document the class and all public properties with
///comments - Required vs Optional: Mark properties as
requiredor optional based on business rules - Empty Factory: Provide an
empty()factory for initial states - Business Logic: Add computed properties and validation methods
- Private Constructor: Use
const FeatureName._();to enable extension methods
Real Example (Note Entity)
dart
/// Represents a user-created note in the application.
///
/// Contains the note's content, metadata, and timestamps.
/// Used throughout the notes domain layer for CRUD operations.
@freezed
abstract class Note with _$Note {
/// Creates a [Note] with the specified properties.
const factory Note({
required String id,
required String userId,
required String title,
required String content,
required DateTime createdAt,
required DateTime updatedAt,
}) = _Note;
const Note._();
/// Creates an empty note for initial state.
factory Note.empty() => _Note(
id: '',
userId: '',
title: '',
content: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}2. Creating Repository Interfaces
Repository interfaces define the contract for data operations without specifying implementation details.
Repository Template
dart
import 'package:flutter_boilerplate/core/error/failure.dart';
import 'package:flutter_boilerplate/features/[feature]/domain/entities/feature_entity.dart';
/// Abstract repository for [FeatureName] operations.
///
/// Defines the contract for [FeatureName] persistence implementations,
/// including CRUD operations and real-time updates.
abstract class FeatureRepository {
/// Stream of [FeatureName]s, automatically updated on changes.
///
/// Returns a stream that emits a tuple containing:
/// - [List<FeatureName>] - The list of items sorted by updatedAt descending.
/// - [Failure?] - Error information if the operation failed,
/// or null on success.
Stream<(List<FeatureName>, Failure?)> listenForChanges();
/// Gets a single [FeatureName] by its ID.
///
/// [id] - The unique identifier of the item to retrieve.
/// Returns a tuple containing:
/// - [FeatureName?] - The item if found, or null if not found.
/// - [Failure?] - Error information if the operation failed,
/// or null on success.
Future<(FeatureName?, Failure?)> getById(String id);
/// Creates a new [FeatureName].
///
/// [property1] - Required property description.
/// [property2] - Optional property description.
/// Returns a tuple containing:
/// - [FeatureName?] - The created item, or null if creation failed.
/// - [Failure?] - Error information if the operation failed,
/// or null on success.
Future<(FeatureName?, Failure?)> create({
required String property1,
String? property2,
});
/// Updates an existing [FeatureName].
///
/// [id] - The unique identifier of the item to update.
/// [property1] - Updated property description.
/// [property2] - Updated optional property description.
/// Returns a tuple containing:
/// - [FeatureName?] - The updated item, or null if update failed.
/// - [Failure?] - Error information if the operation failed,
/// or null on success.
Future<(FeatureName?, Failure?)> update({
required String id,
required String property1,
String? property2,
});
/// Deletes a [FeatureName] by its ID.
///
/// [id] - The unique identifier of the item to delete.
/// Returns a tuple containing:
/// - [void] - No value on success.
/// - [Failure?] - Error information if the operation failed,
/// or null on success.
Future<(void, Failure?)> delete(String id);
}Key Points for Repository Interfaces
- Return Tuples: Use
(Data?, Failure?)pattern for error handling - Stream Operations: Provide streams for real-time updates when applicable
- Comprehensive CRUD: Include Create, Read, Update, Delete operations
- Documentation: Document all methods with parameter descriptions and return values
- Validation: Document validation requirements in method comments
- Async Operations: All operations should be
FutureorStreambased
3. Code Generation Setup
After creating entities, run code generation:
bash
# Generate freezed code
flutter packages pub run build_runner build
# Or watch for changes during development
flutter packages pub run build_runner watch4. Best Practices
Entity Design
- Keep entities focused on a single responsibility
- Include only properties that are essential to the business logic
- Use meaningful names that reflect the business domain
- Add validation methods as computed properties
Repository Design
- Define clear contracts for all data operations
- Use consistent error handling patterns
- Document expected behavior and edge cases
- Keep interfaces technology-agnostic
Error Handling
- Always return
Failureobjects for errors - Provide meaningful error messages
- Use the tuple pattern
(Data?, Failure?)consistently
Documentation
- Document all public APIs with
///comments - Explain business rules and constraints
- Provide examples for complex operations
- Document parameter requirements and return values
5. Testing
Domain layer should have comprehensive unit tests:
dart
// test/features/[feature]/domain/entities/feature_entity_test.dart
void main() {
group('FeatureName', () {
test('should create valid entity', () {
final entity = FeatureName(
id: '1',
property1: 'Test',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(entity.isValid, true);
});
test('should create empty entity', () {
final entity = FeatureName.empty();
expect(entity.id, isEmpty);
expect(entity.isValid, false);
});
});
}The domain layer is the foundation of your feature. Keep it clean, well-documented, and focused on business logic rather than implementation details.