Skip to content

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.dart

1. 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

  1. Use Freezed: All entities should use @freezed for immutability and code generation
  2. Documentation: Document the class and all public properties with /// comments
  3. Required vs Optional: Mark properties as required or optional based on business rules
  4. Empty Factory: Provide an empty() factory for initial states
  5. Business Logic: Add computed properties and validation methods
  6. 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

  1. Return Tuples: Use (Data?, Failure?) pattern for error handling
  2. Stream Operations: Provide streams for real-time updates when applicable
  3. Comprehensive CRUD: Include Create, Read, Update, Delete operations
  4. Documentation: Document all methods with parameter descriptions and return values
  5. Validation: Document validation requirements in method comments
  6. Async Operations: All operations should be Future or Stream based

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 watch

4. 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 Failure objects 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.