Raiser

Raiser Logo

A type-safe, async-first domain event library for Dart. Raiser provides a clean event bus implementation following clean architecture principles, perfect for decoupling components in your application.

Features

  • Type-safe event handling — Generic handlers ensure compile-time type checking
  • Async-first design — All handlers are asynchronous by default
  • Priority-based ordering — Control handler execution order with priorities
  • Middleware support — Wrap handler execution with cross-cutting concerns
  • Flexible error strategies — Choose how errors are handled during event propagation
  • Subscription management — Cancel handlers when no longer needed
  • Domain event metadata — Built-in support for event IDs, timestamps, and aggregate IDs
  • DDD-friendly — Designed with Domain-Driven Design patterns in mind

Installation

dependencies:
  raiser: ^3.0.0

Quick Start

EventBus works with ANY type - you don't need to use RaiserEvent at all:

import 'package:raiser/raiser.dart';

// Define an event - just a simple class!
final class UserCreated {
  UserCreated({
    required this.userId,
    required this.email,
  });

  final String userId;
  final String email;
}

void main() async {
  final bus = EventBus();

  // Subscribe to events
  bus.on<UserCreated>((event) async {
    print('Welcome ${event.email}!');
  });

  // Publish events
  await bus.publish(UserCreated(userId: '123', email: 'alice@example.com'));
}

RaiserEvent (Optional)

You don't have to use RaiserEvent! The EventBus is fully generic and works with any type.

However, if you want standardized domain event metadata, you can optionally implement RaiserEvent, which is an interface extending ZooperDomainEvent from zooper_flutter_core. This provides:

Property Type Description
id EventId Unique identifier (ULID-based)
occurredOn DateTime When the event occurred
metadata Map<String, Object?> Additional event context

RaiserEvent is intentionally an interface (not a base class) to avoid forcing single inheritance. If you choose to use it, implement it explicitly:

import 'package:zooper_flutter_core/zooper_flutter_core.dart';

final class OrderPlaced implements RaiserEvent {
  OrderPlaced({
    required this.orderId,
    required this.amount,
    EventId? eventId,
    DateTime? occurredOn,
    Map<String, Object?> metadata = const {},
  }) : id = eventId ?? EventId.fromUlid(),
       occurredOn = occurredOn ?? DateTime.now(),
       metadata = Map<String, Object?>.unmodifiable(metadata);

  final String orderId;
  final double amount;

  @override
  final EventId id;

  @override
  final DateTime occurredOn;

  @override
  final Map<String, Object?> metadata;
}

Aggregate IDs

Store aggregate identifiers in the metadata map:

final event = OrderPlaced(
  orderId: 'order-123',
  amount: 99.99,
  metadata: {'aggregateId': 'user-456'},
);

// Access via metadata
final aggregateId = event.metadata['aggregateId'] as String?;

Event Handlers

Function Handlers

Quick inline handlers:

bus.on<UserCreated>((event) async {
  await sendWelcomeEmail(event.email);
});

Class-Based Handlers

Better for complex logic and testing:

class WelcomeEmailHandler implements EventHandler<UserCreated> {
  @override
  Future<void> handle(UserCreated event) async {
    await sendWelcomeEmail(event.email);
  }
}

bus.register<UserCreated>(WelcomeEmailHandler());

Handler Priority

Higher values execute first:

bus.on<OrderPlaced>((e) async => print('Second'), priority: 0);
bus.on<OrderPlaced>((e) async => print('First'), priority: 10);
bus.on<OrderPlaced>((e) async => print('Last'), priority: -5);

Middleware

Wrap handler execution with cross-cutting concerns like logging, timing, or validation:

// Add middleware that wraps all handler execution
bus.addMiddleware((Object event, Future<void> Function() next) async {
  print('Before: ${event.runtimeType}');
  await next();
  print('After: ${event.runtimeType}');
}, priority: 100);

// Middleware with higher priority wraps those with lower priority
bus.addMiddleware((Object event, Future<void> Function() next) async {
  final stopwatch = Stopwatch()..start();
  await next();
  print('Took ${stopwatch.elapsedMilliseconds}ms');
}, priority: 50);

Subscriptions

Both on(), register(), and addMiddleware() return a Subscription:

final subscription = bus.on<UserCreated>((event) async {
  // Handle event
});

// Stop receiving events
subscription.cancel();

// Check status
print(subscription.isCancelled); // true

Error Handling

Configure error behavior with ErrorStrategy:

Strategy Behavior
stop Halt on first error, rethrow immediately (default)
continueOnError Run all handlers, throw AggregateException
swallow Run all handlers, errors only go to callback
// Stop on first error (default)
final bus = EventBus(errorStrategy: ErrorStrategy.stop);

// Collect all errors
final bus = EventBus(errorStrategy: ErrorStrategy.continueOnError);

// Silent failures with logging
final bus = EventBus(
  errorStrategy: ErrorStrategy.swallow,
  onError: (error, stackTrace) => logger.error('Failed: $error'),
);

AggregateException

When using continueOnError:

try {
  await bus.publish(event);
} on AggregateException catch (e) {
  print('${e.errors.length} handlers failed');
}

Code Generation

For automatic handler discovery and registration, use the companion packages:

dependencies:
  raiser: ^3.0.0
  raiser_annotation: ^3.0.0

dev_dependencies:
  build_runner: ^2.4.0
  raiser_generator: ^3.0.0

See raiser_generator for details.

License

MIT License - see LICENSE for details.

Libraries

raiser
Raiser - A type-safe domain event library for Dart.