emkore

pub package License: BSD-3-Clause

A foundational Clean Architecture core library for Dart that provides abstract base classes, interfaces, and utilities for building scalable business applications.

🎯 What is emkore?

Emkore is a production-grade Clean Architecture foundation that eliminates boilerplate and provides battle-tested patterns for:

  • Use Cases & Interactors with built-in validation and authorization
  • Entity Management with system fields and type safety
  • Permission-Based Authorization with fine-grained access control
  • Interceptor Pipeline for cross-cutting concerns (logging, metrics, auditing)
  • Repository Patterns with generic interfaces
  • Validation Framework with fluent API and JSON schema generation

πŸ—οΈ Project Structure & Conventions

Following Clean Architecture principles, organize your project like this:

your_project/
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ entity/                    # Domain entities and DTOs
β”‚   β”‚   β”‚   └── user/
β”‚   β”‚   β”‚       β”œβ”€β”€ user.entity.dart   # Domain entity
β”‚   β”‚   β”‚       └── user.dto.dart      # Data transfer object
β”‚   β”‚   β”œβ”€β”€ usecase/                   # Use cases and interactors
β”‚   β”‚   β”‚   └── user/
β”‚   β”‚   β”‚       β”œβ”€β”€ interface/         # Abstract use case definitions
β”‚   β”‚   β”‚       β”‚   β”œβ”€β”€ create_user.usecase.dart
β”‚   β”‚   β”‚       β”‚   └── repository.dart
β”‚   β”‚   β”‚       └── create_user.interactor.dart  # Concrete implementations
β”‚   β”‚   └── repository/                # Data access implementations
β”‚   β”‚       └── user/
β”‚   β”‚           β”œβ”€β”€ memory/
β”‚   β”‚           β”‚   β”œβ”€β”€ user.repository.dart
β”‚   β”‚           β”‚   └── user.uow.dart  # Unit of Work factory
β”‚   β”‚           └── sql/               # Alternative implementations
β”‚   └── your_project.dart             # Main library exports
β”œβ”€β”€ test/                             # Unit and integration tests
β”œβ”€β”€ example/                          # Usage examples
└── doc/                              # Documentation

Naming Conventions

Follow these conventions for consistency:

Use Case and Resource Names

  • Use case names: snake_case for both action and resource
    • ('create', 'user') βœ…
    • ('update_status', 'order') βœ…
    • ('bulk_delete', 'document') βœ…
  • Resource names: Always singular
    • 'user' not 'users' βœ…
    • 'order' not 'orders' βœ…
    • 'document' not 'documents' βœ…

Class Names (PascalCase, Singular)

  • Use case interfaces: [Action][Resource]Usecase
    • CreateUserUsecase βœ…
    • UpdateStatusOrderUsecase βœ…
    • BulkDeleteDocumentUsecase βœ…
  • Interactor implementations: [Action][Resource]Interactor
    • CreateUserInteractor βœ…
    • UpdateStatusOrderInteractor βœ…
    • BulkDeleteDocumentInteractor βœ…
  • Entities: [Resource]Entity
    • UserEntity βœ…
    • OrderEntity βœ…
    • DocumentEntity βœ…
  • DTOs: [Resource]Dto
    • UserDto βœ…
    • OrderDto βœ…
    • DocumentDto βœ…
  • Repositories: [StorageType][Resource]Repository
    • SqlUserRepository βœ…
    • InMemoryOrderRepository βœ…
    • RestDocumentRepository βœ…

File and Folder Names

  • Match resource names (singular): snake_case for files, resource-based folders
    • entity/user/user.entity.dart βœ…
    • usecase/order/create_order.usecase.dart βœ…
    • repository/document/sql/document.repository.dart βœ…
    • repository/document/memory/document.repository.dart βœ…

πŸš€ Quick Start

Installation

Add emkore to your pubspec.yaml:

dependencies:
  emkore: ^0.3.2

Basic Usage

1. Define Your Entity

import 'package:emkore/emkore.dart' as emkore;

class UserEntity extends emkore.Entity<UserEntity> {
  final String name;
  final String email;
  final bool isActive;

  UserEntity({
    required super.id,
    required super.createdAt,
    required super.updatedAt,
    required super.ownerId,
    super.deletedAt,
    required this.name,
    required this.email,
    required this.isActive,
  });

  @override
  UserEntity copyWith({
    String? id,
    DateTime? createdAt,
    DateTime? updatedAt,
    DateTime? deletedAt,
    String? ownerId,
    String? name,
    String? email,
    bool? isActive,
  }) {
    return UserEntity(
      id: id ?? this.id,
      createdAt: createdAt ?? this.createdAt,
      updatedAt: updatedAt ?? this.updatedAt,
      deletedAt: deletedAt ?? this.deletedAt,
      ownerId: ownerId ?? this.ownerId,
      name: name ?? this.name,
      email: email ?? this.email,
      isActive: isActive ?? this.isActive,
    );
  }
}

2. Create Use Case Input/Output

class CreateUserInput {
  final String name;
  final String email;
  final emkore.ResourceScope owner;

  CreateUserInput({
    required this.name,
    required this.email,
    required this.owner,
  });

  factory CreateUserInput.fromJson(emkore.JSON json) {
    return CreateUserInput(
      name: json.getString('name'),
      email: json.getString('email'),
      owner: emkore.ResourceScope.values.firstWhere(
        (s) => s.name == json.getString('owner'),
        orElse: () => emkore.ResourceScope.business,
      ),
    );
  }

  emkore.JSON toJson() {
    return emkore.JSON.from({
      'name': name,
      'email': email,
      'owner': owner.name,
    });
  }

  static emkore.ValidationResult validate(CreateUserInput input) {
    return emkore.ValidatorBuilder()
        .string('name', input.name, maxLength: 100, required: true)
        .email('email', input.email, required: true)
        .enumField('owner', input.owner,
          options: emkore.ResourceScope.values, required: true)
        .build();
  }
}

// Return simple DTO/JSON instead of Entity
typedef CreateUserOutput = Map<String, dynamic>;

3. Define Use Case Interface

abstract base class CreateUserUsecase
    extends emkore.Usecase<CreateUserInput, CreateUserOutput> {

  @override
  emkore.UsecaseName get usecaseName => ('create', 'user');

  @override
  List<emkore.Permission> get requiredPermissions => [
    emkore.Permission(resource: 'user', action: 'create'),
  ];

  // performExecute is inherited from Usecase base class
}

4. Implement the Interactor

class CreateUserInteractor extends CreateUserUsecase {
  final emkore.UnitOfWorkFactory _uowFactory;

  CreateUserInteractor(this._uowFactory);

  @override
  Future<CreateUserOutput> performExecute({
    required emkore.Actor actor,
    required CreateUserInput input,
  }) async {
    final validation = CreateUserInput.validate(input);
    if (!validation.isValid) {
      throw emkore.ValidationException(validation.errors.first);
    }

    // Use UnitOfWork for transaction support and repository access
    return await _uowFactory.transaction(actor.businessId, (uow) async {
      // Create entity with proper ownership
      final user = UserEntity(
        id: emkore.Entity.generateId('USR1'),
        createdAt: DateTime.now(),
        updatedAt: DateTime.now(),
        ownerId: input.owner == emkore.ResourceScope.business
            ? actor.businessId
            : actor.id,
        name: input.name,
        email: input.email,
        isActive: true,
      );

      // Create via repository and return DTO
      final repository = getRepository(uow); // Your UoW implementation provides repositories
      final createdUser = await repository.create(user);

      // Return DTO/JSON instead of Entity
      return {
        'id': createdUser.id,
        'name': createdUser.name,
        'email': createdUser.email,
        'isActive': createdUser.isActive,
        'createdAt': createdUser.createdAt.toIso8601String(),
      };
    });
  }
}

5. Set Up Actor with Permissions

class MyActor extends emkore.Actor {
  @override
  final String id;

  @override
  final String businessId;

  @override
  final String token;

  MyActor({required this.id, required this.businessId, required this.token}) {
    permissions = [
      emkore.Permission(resource: 'user', action: 'create'),
      emkore.Permission(resource: 'user', action: 'read'),
      // Add more permissions as needed
    ];
  }
}

6. Execute the Interactor

void main() async {
  final uowFactory = MyUnitOfWorkFactory(); // Your UoW implementation
  final createUser = CreateUserInteractor(uowFactory);

  final actor = MyActor(
    id: 'user_123',
    businessId: 'company_abc',
    token: 'jwt_token_here'
  );

  final input = CreateUserInput(
    name: 'John Doe',
    email: 'john@example.com',
    owner: emkore.ResourceScope.business,
  );

  try {
    final userDto = await createUser.execute(actor: actor, input: input);
    print('Created user: ${userDto['name']} (${userDto['id']})');
  } catch (e) {
    print('Error: $e');
  }
}

πŸ—οΈ Architecture Overview

Emkore implements Clean Architecture principles with these core abstractions:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 UI Layer                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚              Use Cases                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ Interactors │←│     Interceptors        β”‚ β”‚
β”‚  β”‚             β”‚ β”‚ β€’ Authorization         β”‚ β”‚
β”‚  β”‚             β”‚ β”‚ β€’ Logging              β”‚ β”‚
β”‚  β”‚             β”‚ β”‚ β€’ Performance          β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ Audit                β”‚ β”‚
β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚              Domain Layer                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  Entities   β”‚ β”‚      Permissions        β”‚ β”‚
β”‚  β”‚             β”‚ β”‚                         β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚            Infrastructure                   β”‚
β”‚              Repositories                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

✨ Key Features

πŸ” Built-in Authorization

// Fine-grained permissions with constraints
final permission = emkore.Permission(
  resource: 'document',
  action: 'update',
  constraints: emkore.ResourceConstraints(
    scope: emkore.ResourceScope.business,
    businessId: 'company_123',
  ),
);

// Automatic authorization checking in use cases
abstract class UpdateDocumentUsecase extends emkore.Usecase<Input, Output> {
  @override
  List<emkore.Permission> get requiredPermissions => [permission];
}

πŸ“Š Interceptor Pipeline

// Add cross-cutting concerns to any interactor
final interactor = CreateUserInteractor(uowFactory);

// Add local interceptors (specific to this interactor instance)
interactor.use([
  emkore.LoggerInterceptor<CreateUserInput, CreateUserOutput>(),
  emkore.PerformanceInterceptor<CreateUserInput, CreateUserOutput>(),
]);

// Add global interceptors (apply to all use cases)
emkore.Usecase.useGlobal([
  emkore.AuthorizationInterceptor<dynamic, dynamic>(),
]);

βœ… Fluent Validation

final validation = emkore.ValidatorBuilder()
    .string('name', input.name, maxLength: 100, required: true)
    .email('email', input.email, required: true)
    .money('price', input.price, min: emkore.Money(0), required: true)
    .entityId('parentId', input.parentId, 'DOC1', required: false)
    .build();

if (!validation.isValid) {
  // Handle validation errors with field-specific messages
  print(validation.getFieldErrors('email')); // ["Invalid email format"]
}

πŸ”„ JSON Handling

Use emkore's JSON helper methods instead of direct casting for type safety:

class CreateUserInput {
  final String name;
  final String email;
  final int age;

  factory CreateUserInput.fromJson(emkore.JSON json) {
    return CreateUserInput(
      name: json.getString('name'),        // βœ… Type-safe access
      email: json.getString('email'),      // βœ… With validation
      age: json.getInt('age'),            // βœ… Automatic conversion
      // json['name'] as String,          // ❌ Avoid direct casting
    );
  }

  emkore.JSON toJson() {
    return emkore.JSON.from({
      'name': name,
      'email': email,
      'age': age,
    });
  }
}

Available JSON helper methods:

  • getString(), getOptionalString()
  • getInt(), getOptionalInt()
  • getBool(), getOptionalBool()
  • getDouble(), getOptionalDouble()
  • getList<T>(), getSet<T>()
  • getJSONList() for nested objects

πŸ’° Type-Safe Value Objects

// Money prevents precision errors and enforces type safety
final price = emkore.Money(1299); // $12.99 in cents
final tax = emkore.Money(104);    // $1.04
final total = price + tax;        // $14.03

// Only Money can be added to Money - compile-time safety!
// final invalid = price + 5; // ❌ Compile error

πŸ“š Advanced Usage

The UnitOfWork pattern provides transaction support and repository access:

// Define your UoW factory
class MyUnitOfWorkFactory implements emkore.UnitOfWorkFactory {
  @override
  Future<T> transaction<T>(
    String tenantId,
    Future<T> Function(emkore.UnitOfWork) body,
  ) async {
    // Create transaction context for tenant
    final uow = MyUnitOfWork(tenantId);

    try {
      final result = await body(uow);
      await uow.commit();
      return result;
    } catch (e) {
      await uow.rollback();
      rethrow;
    }
  }
}

// Your UoW implementation provides repositories
class MyUnitOfWork implements emkore.UnitOfWork {
  @override
  final String tenantId;

  MyUnitOfWork(this.tenantId);

  UserRepository get userRepository => UserRepository(tenantId);
  OrderRepository get orderRepository => OrderRepository(tenantId);

  @override
  Future<void> commit() async {
    // Commit transaction
  }

  @override
  Future<void> rollback() async {
    // Rollback transaction
  }
}

Repository Pattern

Repositories work with DTOs for consistent data transfer:

abstract interface class UserRepository {
  Future<UserDto> create(UserDto dto);
  Future<UserDto> findById(String id);
  Future<List<UserDto>> findByBusinessId(String businessId);
  Future<UserDto> update(UserDto dto);
  Future<void> delete(String id);
}

// Use case output can be DTO but should convert to plain JSON at API boundaries
typedef CreateUserOutput = UserDto; // DTO for internal use

// At API/UI boundaries, convert DTOs to plain JSON:
final userDto = await createUser.execute(actor: actor, input: input);
final plainJson = userDto.toJson(); // Convert to Map<String, dynamic> for external use

Custom Interceptors

class MetricsInterceptor implements emkore.Interceptor<Input, Output> {
  @override
  String get name => 'metrics';

  @override
  FutureOr<Input> beforeExecute(Input input, emkore.InterceptorContext context) {
    // Record metrics before execution
    startTimer(context.usecaseName);
    return input;
  }

  @override
  FutureOr<Output> afterExecute(Output result, emkore.InterceptorContext context) {
    // Record success metrics
    recordSuccess(context.usecaseName);
    return result;
  }

  @override
  FutureOr<void> onError(Object error, emkore.InterceptorContext context) {
    // Record error metrics
    recordError(context.usecaseName, error);
  }
}

πŸ”§ Configuration

Multi-tenancy Support

// Each business gets isolated data
final userRepo = repositoryFactory.createUserRepository(actor.businessId);

// Built-in tenant isolation in all operations
final users = await userRepo.findByBusinessId(actor.businessId);

Permission Hierarchies

// ResourceScope hierarchy: owned < team < business < all
emkore.ResourceScope.owned     // User's own resources
emkore.ResourceScope.team      // Team resources
emkore.ResourceScope.business  // Business-wide resources
emkore.ResourceScope.all       // System-wide resources

πŸ“– Documentation

🀝 Contributing

This is a foundational library focused on stability and production use. Contributions should:

  • Maintain backward compatibility
  • Include comprehensive tests
  • Follow Clean Architecture principles
  • Provide clear documentation

πŸ“„ License

BSD-3-Clause License. See LICENSE for details.

🏒 About :emko

Emkore is developed by :emko, focused on building foundational tools for scalable software architecture.


Ready to build better software? Add emkore to your project and experience Clean Architecture done right. ✨

Libraries

emkore
:emko core | A Clean Architecture-inspired Dart library by :emko