Event System

Build decoupled, maintainable applications with Khadem's powerful event-driven architecture. The event system enables components to communicate without tight coupling, using a publish-subscribe pattern with advanced features like priorities, groups, and subscriber management.

Pub-Sub PatternPriority-BasedAsync ExecutionEvent GroupsType-Safe

Introduction

The event system in Khadem provides a robust mechanism for implementing the Observer pattern in your applications. It allows different parts of your application to communicate without knowing about each other, promoting loose coupling and better separation of concerns.

Key Features

  • Priority-Based Execution: Control the order in which listeners execute using four priority levels (low, normal, high, critical)
  • Async Support: Execute listeners asynchronously with the queue parameter for non-blocking operations
  • Event Groups: Organize related events and emit them together for batch operations
  • Subscriber Pattern: Group related event handlers in classes for better organization and automatic cleanup
  • One-Time Listeners: Register listeners that automatically remove themselves after first execution
  • Error Isolation: Errors in one listener don't affect other listeners - each executes independently

Basic Usage

At its core, the event system uses a simple publish-subscribe pattern. You register listeners using on() and trigger them using emit(). All events are accessed through the global Khadem.eventBus instance.

dart
// Register an event listener
Khadem.eventBus.on('user.created', (user) async {
  print('New user created: ${user['name']}');
  await sendWelcomeEmail(user);
});

// Emit an event with payload
await Khadem.eventBus.emit('user.created', {
  'id': 123,
  'name': 'John Doe',
  'email': 'john@example.com'
});

💡 Event Naming Conventions

Use dot notation to namespace your events by domain or feature:

  • user.created, user.updated, user.deleted - User lifecycle events
  • order.placed, order.confirmed, order.shipped - Order workflow events
  • auth.login, auth.logout, auth.failed - Authentication events

This convention makes events easier to understand, organize, and filter.

Priority-Based Execution

When multiple listeners are registered for the same event, you can control their execution order using priorities. Listeners with higher priority execute first, ensuring critical operations complete before less important ones. This is particularly useful for security logging, validation, or cleanup operations that must happen in a specific order.

dart
// Register listeners with different priorities
Khadem.eventBus.on('order.processed', (order) async {
  // Critical: Security logging
  await logSecurityEvent('Order processed', order);
}, priority: EventPriority.critical);

Khadem.eventBus.on('order.processed', (order) async {
  // High: Business logic
  await updateInventory(order);
}, priority: EventPriority.high);

Khadem.eventBus.on('order.processed', (order) async {
  // Normal: Notifications
  await sendOrderConfirmation(order);
}, priority: EventPriority.normal);

Khadem.eventBus.on('order.processed', (order) async {
  // Low: Analytics
  await trackAnalytics('order_processed', order);
}, priority: EventPriority.low);

// Emit event - listeners execute in priority order
await Khadem.eventBus.emit('order.processed', orderData);

Understanding Priority Levels

🔴 EventPriority.critical (1000):

Reserved for security, audit logging, and critical monitoring. These listeners execute first and should never fail.

🟠 EventPriority.high (750):

Core business logic that must execute before side effects. Examples: inventory updates, payment processing, data validation.

🟡 EventPriority.normal (500) - Default:

Standard application logic like sending notifications, updating caches, or triggering related workflows.

🟢 EventPriority.low (250):

Non-critical operations like analytics tracking, cleanup tasks, or logging that won't affect user experience if delayed.

⚠️ Important Notes

  • • Priorities only control execution order, not whether a listener executes
  • • All listeners of the same priority execute in registration order (undefined which goes first)
  • • Don't rely on priorities for critical business logic dependencies - use explicit sequencing instead

One-Time Listeners

Sometimes you need a listener to execute only once and then automatically remove itself. This is perfect for initialization tasks, one-time setup operations, or responding to events that should only trigger an action the first time they occur. The once() method is a convenience wrapper that registers a listener with the once: true flag.

dart
// Register one-time listeners
Khadem.eventBus.once('app.initialized', () async {
  print('App initialized for the first time!');
  await performInitialSetup();
});

Khadem.eventBus.once('database.migrated', (version) async {
  print('Database migrated to version: $version');
  await updateMigrationStatus(version);
});

// Or use the once() convenience method
Khadem.eventBus.once('cache.warmed', () async {
  print('Cache warmed up successfully');
});

// These listeners will be automatically removed after first execution
await Khadem.eventBus.emit('app.initialized');
await Khadem.eventBus.emit('database.migrated', '1.2.0');

Common Use Cases

  • Application Initialization: Run setup code when the app first starts
  • Database Migrations: Execute migration logic only once per version
  • Cache Warming: Warm up caches on first request only
  • Feature Introductions: Show a welcome message or tutorial on first use
  • Resource Cleanup: Clean up resources the first time they're no longer needed

💡 Pro Tip: One-time listeners work with priorities too! You can create a critical one-time listener that executes before other listeners but only once: once('event', handler, priority: EventPriority.critical)

Event Groups

Event groups allow you to organize related events under a common name and trigger them all at once. This is useful when you have multiple events that represent different aspects of the same operation or lifecycle. Instead of emitting each event individually, you can emit the entire group with a single call to emitGroup().

Groups are particularly powerful for domain-driven design, where you can group all events related to a specific aggregate or entity (like all user-related events, or all order workflow events).

dart
// Create event groups for related operations
Khadem.eventBus.addToGroup('user.lifecycle', 'user.created');
Khadem.eventBus.addToGroup('user.lifecycle', 'user.updated');
Khadem.eventBus.addToGroup('user.lifecycle', 'user.deleted');

Khadem.eventBus.addToGroup('order.workflow', 'order.placed');
Khadem.eventBus.addToGroup('order.workflow', 'order.confirmed');
Khadem.eventBus.addToGroup('order.workflow', 'order.shipped');

// Register listeners for group events
Khadem.eventBus.on('user.created', (user) async => await logUserActivity(user, 'created'));
Khadem.eventBus.on('user.updated', (user) async => await logUserActivity(user, 'updated'));
Khadem.eventBus.on('user.deleted', (user) async => await logUserActivity(user, 'deleted'));

// Emit entire group at once
await Khadem.eventBus.emitGroup('user.lifecycle', userData);

// Remove events from groups
Khadem.eventBus.removeFromGroup('user.lifecycle', 'user.updated');

When to Use Groups

  • Lifecycle Events: Group all events for an entity's lifecycle (created, updated, deleted)
  • Workflow Stages: Group events representing different stages of a process
  • Feature Toggles: Enable/disable entire feature sets by managing groups
  • Batch Notifications: Send multiple related notifications at once
  • Audit Logging: Log all events in a category together

📝 Note: Events can belong to multiple groups. The same event can be in both 'user.lifecycle' and 'audit.all' groups, giving you flexible organization options.

Asynchronous Execution

By default, event listeners execute synchronously in priority order - each listener completes before the next one starts. While listeners can be async functions, they still execute sequentially. This ensures predictable behavior but can be slow when listeners perform I/O operations.

Setting queue: true changes this behavior. All listeners execute concurrently in separate futures, and the emit call waits for all of them to complete using Future.wait(). This dramatically improves performance when listeners perform independent operations like sending emails, calling APIs, or updating multiple databases.

dart
// Synchronous execution (default)
Khadem.eventBus.on('file.uploaded', (file) async {
  // This blocks other listeners
  await processFile(file);
  await generateThumbnail(file);
});

// Asynchronous execution (recommended for I/O)
await Khadem.eventBus.emit('file.uploaded', fileData, queue: true);

// Multiple async operations
Khadem.eventBus.on('user.registered', (user) async {
  // These run in parallel when queue: true
  final emailTask = sendWelcomeEmail(user);
  final profileTask = createUserProfile(user);
  final analyticsTask = trackRegistration(user);

  await Future.wait([emailTask, profileTask, analyticsTask]);
});

// Emit with async processing
await Khadem.eventBus.emit('user.registered', userData, queue: true);

Synchronous vs Asynchronous

Synchronous (queue: false - default):
  • Listeners execute in priority order, one after another
  • Predictable execution sequence
  • Good for operations that depend on each other
  • Slower when listeners perform I/O operations
Asynchronous (queue: true):
  • All listeners start concurrently in separate futures
  • Much faster for I/O-bound operations
  • Perfect when listeners are independent
  • Priority order still determines start time, but execution overlaps
  • The emit call waits for ALL listeners to complete

⚡ Performance Tip: If you have 3 listeners that each take 1 second, synchronous execution takes 3 seconds, but asynchronous execution takes only ~1 second (assuming they're independent operations).

⚠️ When NOT to Use queue: true

  • • When listeners must execute in a specific order and depend on each other
  • • When later listeners need results from earlier ones
  • • For CPU-bound operations (won't gain performance benefits)
  • • When you need to guarantee that listener A completes before listener B starts

Event Subscribers

When you have multiple related event handlers, scattering them across your codebase with individual on() calls can become messy and hard to maintain. Event subscribers solve this by letting you group all related handlers in a single class that implements the EventSubscriberInterface.

Subscribers are particularly powerful because they enable automatic cleanup. When you call offSubscriber(), all listeners registered by that subscriber are removed at once. This prevents memory leaks and makes component lifecycle management much simpler.

dart
// Create an event subscriber class
class UserEventSubscriber implements EventSubscriberInterface {
  @override
  List<EventMethod> getEventHandlers() => [
    EventMethod(
      eventName: 'user.created',
      handler: _onUserCreated,
      priority: EventPriority.high,
    ),
    EventMethod(
      eventName: 'user.updated',
      handler: _onUserUpdated,
      priority: EventPriority.normal,
    ),
    EventMethod(
      eventName: 'user.deleted',
      handler: _onUserDeleted,
      priority: EventPriority.normal,
      once: true, // Only handle first deletion
    ),
  ];

  Future<void> _onUserCreated(dynamic user) async {
    await sendWelcomeEmail(user);
    await createUserProfile(user);
  }

  Future<void> _onUserUpdated(dynamic user) async {
    await updateUserCache(user);
    await logUserChange(user);
  }

  Future<void> _onUserDeleted(dynamic user) async {
    await cleanupUserData(user);
    await logUserDeletion(user);
  }
}

// Register the subscriber
final subscriber = UserEventSubscriber();
registerSubscribers([subscriber]);

// All cleanup happens automatically when subscriber is destroyed
Khadem.eventBus.offSubscriber(subscriber);

Why Use Subscribers?

🎯 Better Organization:

All event handlers for a feature or domain are in one place, making code easier to find and maintain.

🧹 Automatic Cleanup:

One call to offSubscriber(subscriber) removes all listeners registered by that subscriber, preventing memory leaks.

🧪 Easier Testing:

You can test the entire subscriber class in isolation, and each handler method can be tested individually.

📦 Encapsulation:

Private handler methods keep implementation details hidden while the public interface remains clean.

⚙️ Flexible Configuration:

Each handler in the subscriber can have its own priority and once flag, giving you fine-grained control.

💡 Subscriber Best Practices

  • • Create one subscriber per domain or feature (e.g., UserEventSubscriber, OrderEventSubscriber)
  • • Keep handlers focused - each should do one thing well
  • • Use dependency injection to pass services into the subscriber constructor
  • • Always clean up subscribers in your application's shutdown logic
  • • Consider using the subscriber parameter even with manual on() calls for consistency

Listener Management

Properly managing listeners is crucial to prevent memory leaks and ensure your application stays performant. The event system provides several methods to remove listeners at different granularities - from removing a specific listener function to clearing the entire event system.

dart
// Remove specific listener
void removeListener() {
  Khadem.eventBus.off('user.created', myHandler);
}

// Remove all listeners for an event
void clearEventListeners() {
  Khadem.eventBus.offEvent('user.created');
}

// Remove all listeners for a subscriber
void cleanupSubscriber() {
  Khadem.eventBus.offSubscriber(this);
}

// Clear everything (use with caution)
void resetEventSystem() {
  Khadem.eventBus.clear();
}

// Check if event has listeners
if (Khadem.eventBus.hasListeners('user.created')) {
  print('Event has active listeners');
}

// Get listener count
final count = Khadem.eventBus.listenerCount('user.created');
print('Listeners for user.created: $count');

Available Management Methods

off(event, listener)

Removes a specific listener function from an event. Requires the exact function reference, so avoid anonymous functions if you need to remove them later.

offEvent(event)

Removes ALL listeners for a specific event, and removes that event from all groups and subscriber tracking. Use when you want to completely disable an event.

offSubscriber(subscriber)

Removes all listeners registered by a specific subscriber object. This is the recommended cleanup method when using the subscriber pattern.

clear()

Nuclear option: removes ALL listeners, groups, and subscriber tracking. Use with extreme caution - typically only in tests or complete application resets.

hasListeners(event)

Check if an event has any registered listeners before emitting (useful for performance optimization).

listenerCount(event)

Get the number of listeners for an event (useful for debugging and monitoring).

🔒 Memory Leak Prevention

Common mistake: Registering event listeners in long-lived objects (like services or singletons) without ever removing them.

Solution strategies:

  • Use the subscriber parameter when calling on() to track listener ownership
  • Always call offSubscriber(this) in dispose/cleanup methods
  • Store function references if you need to remove them later (avoid anonymous functions)
  • Use one-time listeners with once() when appropriate to avoid manual cleanup
  • Periodically audit listener counts in production using listenerCount()

Error Handling

A critical design principle of the event system is error isolation: if one listener throws an error, it should not prevent other listeners from executing. The event system catches errors in each listener and logs them (when queue: true), but you should still implement proper error handling in your listeners.

When queue: false (the default), errors are silently caught to prevent disrupting subsequent listeners. When queue: true, errors are caught and logged via Khadem.logger.error(). Always wrap your listener code in try-catch blocks for explicit error handling.

dart
// Proper error handling in listeners
Khadem.eventBus.on('user.created', (user) async {
  try {
    await sendWelcomeEmail(user);
    await createUserProfile(user);
    await logUserCreation(user);
  } catch (e, stackTrace) {
    // Log error but don't rethrow
    Khadem.logger.error('Failed to process user creation', error: e, stackTrace: stackTrace);

    // Optionally emit error event
    await Khadem.eventBus.emit('user.creation.failed', {
      'user': user,
      'error': e.toString(),
      'timestamp': DateTime.now(),
    });
  }
});

// Error event handler
Khadem.eventBus.on('user.creation.failed', (errorData) async {
  // Handle failed user creation
  await notifyAdmin(errorData);
  await cleanupPartialData(errorData['user']);
});

// Async error handling
Khadem.eventBus.on('file.processed', (file) async {
  try {
    final result = await processFileAsync(file);
    await Khadem.eventBus.emit('file.processing.success', result);
  } on FileProcessingException catch (e) {
    await Khadem.eventBus.emit('file.processing.error', {
      'file': file,
      'error': e.message,
    });
  } catch (e) {
    await Khadem.eventBus.emit('file.processing.unexpected_error', {
      'file': file,
      'error': e.toString(),
    });
  }
});

Error Handling Strategy

1. Always Catch Errors in Listeners:

Don't rely on the event system's error catching. Wrap your logic in try-catch for explicit control over error handling.

2. Log Errors, Don't Rethrow:

Rethrowing errors can break other listeners. Log the error and handle it gracefully instead.

3. Use Error Events:

Emit error-specific events (like 'user.creation.failed') to centralize error handling and enable error recovery mechanisms.

4. Specific Exception Types:

Catch specific exception types with on ExceptionType catch (e) for targeted error handling.

5. Include Context:

When logging errors, include the event name, payload data, and any relevant context to aid debugging.

⚠️ Error Event Pattern

Creating error events for failed operations enables powerful error recovery patterns:

  • Centralized Error Logging: One listener handles all error events for comprehensive logging
  • Retry Mechanisms: Error listeners can implement retry logic for transient failures
  • User Notifications: Automatically notify users or admins when critical operations fail
  • Cleanup Operations: Remove partial data or rollback changes when operations fail partway through
  • Circuit Breakers: Track failure rates and temporarily disable problematic operations

Best Practices

Following these best practices will help you build maintainable, performant event-driven applications. These guidelines are based on common patterns and pitfalls discovered through production use.

✅ Recommended Practices

• Use Namespaced Event Names

Follow dot notation: domain.action (e.g., user.created, order.shipped)

• Always Handle Errors

Wrap listener logic in try-catch blocks and log errors appropriately

• Set Appropriate Priorities

Use critical for security/audit, high for business logic, normal for notifications, low for analytics

• Clean Up Listeners

Remove listeners in dispose methods using offSubscriber() or off()

• Use Subscribers for Organization

Group related handlers in subscriber classes for better maintainability

• Document Event Contracts

Document expected payload structure for each event type

• Use queue: true for I/O

Enable async execution for database, API, or file operations

• Test Event Flows

Write tests that verify listeners execute and emit calls trigger expected behavior

❌ Common Pitfalls

• Heavy Sync Operations

Don't perform expensive computations in synchronous listeners - use queue: true

• Assuming Execution Order

Don't rely on order except for priorities; listeners of same priority may run in any order

• Forgetting Cleanup

Always remove listeners to prevent memory leaks, especially in services and widgets

• Using Events for Direct Calls

Don't use events when a simple method call would work - events add overhead

• Circular Event Emissions

Avoid emitting events within listeners that could trigger the same listener again

• Ignoring Errors

Never let errors fail silently - always log them with context

• Anonymous Functions

Avoid anonymous functions if you need to remove listeners later - store references

• Blocking Operations

Don't use synchronous blocking code - always use async/await for I/O

💡 When to Use Events vs Direct Calls

Use Events When:

  • Multiple components need to react to the same action
  • You want to decouple components (sender doesn't know about receivers)
  • The number of reactions may change over time
  • You need to enable/disable reactions dynamically

Use Direct Method Calls When:

  • You need a return value from the operation
  • The operation must complete before continuing
  • There's only one receiver and it won't change
  • The caller needs to handle errors from the receiver

Advanced Patterns

These advanced patterns demonstrate how to leverage the event system for sophisticated architectural approaches. Each pattern solves specific problems and can be combined for maximum effectiveness.

🏛️ Domain Events Pattern

Organize events by business domain using constant classes. This provides type safety, discoverability through IDE autocomplete, and prevents typos in event names. Perfect for domain-driven design where events represent significant business occurrences.

dart
// Domain-driven event organization
class UserDomainEvents {
  static const String created = 'user.domain.created';
  static const String updated = 'user.domain.updated';
  static const String deleted = 'user.domain.deleted';
  static const String passwordChanged = 'user.domain.password_changed';
  static const String emailVerified = 'user.domain.email_verified';
}

class OrderDomainEvents {
  static const String placed = 'order.domain.placed';
  static const String confirmed = 'order.domain.confirmed';
  static const String shipped = 'order.domain.shipped';
  static const String delivered = 'order.domain.delivered';
  static const String cancelled = 'order.domain.cancelled';
}

// Usage with domain events
Khadem.eventBus.on(UserDomainEvents.created, (user) async {
  await sendWelcomeEmail(user);
  await createUserProfile(user);
});

Khadem.eventBus.on(OrderDomainEvents.placed, (order) async {
  await reserveInventory(order);
  await processPayment(order);
});

// Group domain events
Khadem.eventBus.addToGroup('user.domain', UserDomainEvents.created);
Khadem.eventBus.addToGroup('user.domain', UserDomainEvents.updated);
Khadem.eventBus.addToGroup('user.domain', UserDomainEvents.deleted);

Khadem.eventBus.addToGroup('order.lifecycle', OrderDomainEvents.placed);
Khadem.eventBus.addToGroup('order.lifecycle', OrderDomainEvents.confirmed);
Khadem.eventBus.addToGroup('order.lifecycle', OrderDomainEvents.shipped);
Benefits: Refactoring support, IDE autocomplete, documentation, preventing typos, clear domain boundaries

👁️ Observer Pattern

Implement the classic Observer pattern where services register themselves as observers of domain entities. Services react to entity changes without the entity knowing about the services. This achieves true decoupling - you can add new observers without modifying existing code.

dart
// Observer pattern with events
class UserService {
  Future<void> createUser(Map<String, dynamic> userData) async {
    final user = await _saveUserToDatabase(userData);
    await Khadem.eventBus.emit('user.created', user);
  }

  Future<void> updateUser(int userId, Map<String, dynamic> updates) async {
    final user = await _updateUserInDatabase(userId, updates);
    await Khadem.eventBus.emit('user.updated', user);
  }
}

class EmailService {
  EmailService() {
    // Subscribe to user events
    Khadem.eventBus.on('user.created', _sendWelcomeEmail);
    Khadem.eventBus.on('user.updated', _sendUpdateNotification);
  }

  Future<void> _sendWelcomeEmail(dynamic user) async {
    await sendEmail(user['email'], 'Welcome!', 'Welcome to our platform');
  }

  Future<void> _sendUpdateNotification(dynamic user) async {
    await sendEmail(user['email'], 'Profile Updated', 'Your profile has been updated');
  }
}

class AnalyticsService {
  AnalyticsService() {
    Khadem.eventBus.on('user.created', _trackRegistration);
    Khadem.eventBus.on('user.updated', _trackProfileUpdate);
  }

  Future<void> _trackRegistration(dynamic user) async {
    await trackEvent('user_registration', {'user_id': user['id']});
  }

  Future<void> _trackProfileUpdate(dynamic user) async {
    await trackEvent('profile_update', {'user_id': user['id']});
  }
}

// Usage
final userService = UserService();
final emailService = EmailService(); // Automatically subscribes
final analyticsService = AnalyticsService(); // Automatically subscribes

await userService.createUser(userData); // Triggers both services
Use Case: Perfect when multiple services need to react to the same domain events (emails, analytics, notifications, etc.)

🔧 Event Middleware Pattern

Add cross-cutting concerns like logging, error handling, performance monitoring, or security checks that apply to all or many events. Middleware wraps event emission to add behavior without modifying individual listeners. You can chain multiple middlewares to create a processing pipeline.

dart
// Event middleware for cross-cutting concerns
class EventMiddleware {
  static Future<void> loggingMiddleware(String event, dynamic payload) async {
    Khadem.logger.info('Event emitted: $event', extra: {'payload': payload});
    await Khadem.eventBus.emit(event, payload);
    Khadem.logger.info('Event completed: $event');
  }

  static Future<void> errorHandlingMiddleware(String event, dynamic payload) async {
    try {
      await Khadem.eventBus.emit(event, payload);
    } catch (e, stackTrace) {
      Khadem.logger.error('Event failed: $event', error: e, stackTrace: stackTrace);
      await Khadem.eventBus.emit('event.error', {
        'event': event,
        'payload': payload,
        'error': e.toString(),
        'timestamp': DateTime.now(),
      });
    }
  }

  static Future<void> performanceMiddleware(String event, dynamic payload) async {
    final start = DateTime.now();
    await Khadem.eventBus.emit(event, payload);
    final duration = DateTime.now().difference(start);

    if (duration > Duration(milliseconds: 100)) {
      Khadem.logger.warn('Slow event: $event took ${duration.inMilliseconds}ms');
    }
  }
}

// Usage with middleware
Future<void> emitWithMiddleware(String event, dynamic payload) async {
  await EventMiddleware.loggingMiddleware(event, payload);
  await EventMiddleware.errorHandlingMiddleware(event, payload);
  await EventMiddleware.performanceMiddleware(event, payload);
}

// Or create a middleware pipeline
class EventPipeline {
  final List<Future<void> Function(String, dynamic)> _middlewares = [];

  void addMiddleware(Future<void> Function(String, dynamic) middleware) {
    _middlewares.add(middleware);
  }

  Future<void> emit(String event, dynamic payload) async {
    for (final middleware in _middlewares) {
      await middleware(event, payload);
    }
  }
}
Common Uses: Logging, performance tracking, error recovery, authentication checks, rate limiting, audit trails

📜 Event Sourcing Pattern

Store every state change as an immutable event instead of storing just current state. This creates a complete audit trail, enables time travel debugging, and allows you to reconstruct any past state by replaying events. The EventStore saves all events, and you can rebuild aggregate state by replaying their events in order.

dart
// Event sourcing pattern
class EventStore {
  final List<Map<String, dynamic>> _events = [];

  Future<void> saveEvent(String eventType, dynamic payload) async {
    final event = {
      'id': Uuid().v4(),
      'type': eventType,
      'payload': payload,
      'timestamp': DateTime.now(),
      'version': _events.length + 1,
    };

    _events.add(event);
    await Khadem.eventBus.emit('event.stored', event);
  }

  List<Map<String, dynamic>> getEventsForAggregate(String aggregateId) {
    return _events.where((event) =>
      event['payload']['aggregateId'] == aggregateId
    ).toList();
  }

  Future<void> replayEvents() async {
    for (final event in _events) {
      await Khadem.eventBus.emit(event['type'], event['payload']);
    }
  }
}

// Event-sourced aggregate
class UserAggregate {
  final String id;
  String name;
  String email;
  bool emailVerified = false;

  UserAggregate(this.id, this.name, this.email);

  Future<void> updateName(String newName) async {
    await Khadem.eventBus.emit('user.name_updated', {
      'aggregateId': id,
      'oldName': name,
      'newName': newName,
      'timestamp': DateTime.now(),
    });
    name = newName;
  }

  Future<void> verifyEmail() async {
    await Khadem.eventBus.emit('user.email_verified', {
      'aggregateId': id,
      'email': email,
      'timestamp': DateTime.now(),
    });
    emailVerified = true;
  }
}

// Event handlers for state reconstruction
class UserEventHandlers {
  final Map<String, UserAggregate> _users = {};

  UserEventHandlers() {
    Khadem.eventBus.on('user.name_updated', _onNameUpdated);
    Khadem.eventBus.on('user.email_verified', _onEmailVerified);
  }

  Future<void> _onNameUpdated(dynamic event) async {
    final user = _users[event['aggregateId']];
    if (user != null) {
      user.name = event['newName'];
    }
  }

  Future<void> _onEmailVerified(dynamic event) async {
    final user = _users[event['aggregateId']];
    if (user != null) {
      user.emailVerified = true;
    }
  }

  UserAggregate? getUser(String id) => _users[id];
}
Benefits: Complete audit trail, time travel debugging, state reconstruction, event replay for testing, compliance support

🎯 Choosing the Right Pattern

Start with Domain Events if you're building a complex application with clear domain boundaries.

Use Observer Pattern when you have multiple services that need to react to the same domain events.

Add Middleware when you find yourself duplicating cross-cutting logic across listeners.

Adopt Event Sourcing when you need comprehensive audit trails, time travel, or complex state reconstruction.

💡 Pro Tip: These patterns work great together! Combine domain events + observers + middleware for a robust architecture.

Debugging and Inspection

The event system provides introspection methods to help you debug event flows, identify memory leaks, and understand the current state of your event system. These are particularly useful during development and troubleshooting.

dart
// Inspect registered listeners
final listeners = Khadem.eventBus.listeners;
for (final entry in listeners.entries) {
  print('Event: ${entry.key}');
  print('Listeners: ${entry.value.length}');
}

// Inspect event groups
final groups = Khadem.eventBus.eventGroups;
for (final entry in groups.entries) {
  print('Group: ${entry.key}');
  print('Events: ${entry.value.join(', ')}');
}

// Inspect subscriber events
final subscriberEvents = Khadem.eventBus.subscriberEvents;
for (final entry in subscriberEvents.entries) {
  print('Subscriber: ${entry.key}');
  print('Events: ${entry.value.join(', ')}');
}

// Debug event system state
void debugEventSystem() {
  print('=== Event System Debug ===');
  print('Total events: ${listeners.length}');
  print('Total groups: ${groups.length}');
  print('Total subscribers: ${subscriberEvents.length}');

  for (final entry in listeners.entries) {
    print('${entry.key}: ${entry.value.length} listeners');
  }
}

Available Inspection Methods

listeners - Get all registered event listeners

Returns Map<String, List<EventRegistration>> showing all events and their listeners

eventGroups - Get all event groups

Returns Map<String, Set<String>> showing groups and their member events

subscriberEvents - Get subscriber event mappings

Returns Map<Object, Set<String>> showing which subscribers are listening to which events

hasListeners(event) - Check if event has listeners

Returns bool, useful for avoiding unnecessary work when emitting

listenerCount(event) - Count listeners for an event

Returns int, useful for detecting memory leaks (ever-increasing counts)

🐛 Debugging Tips

  • Finding Memory Leaks: Call listenerCount() for your events periodically. If counts keep growing, you have a leak - listeners aren't being removed properly.
  • Event Not Firing: Use hasListeners(event) to verify listeners are registered. Check for typos in event names (domain events pattern helps prevent this).
  • Unexpected Execution Order: Check listener priorities. Remember: listeners with the same priority execute in registration order (which is undefined across multiple files).
  • Performance Issues: Use listeners to see how many handlers each event has. Too many listeners on frequently-emitted events can cause performance problems.

Performance Optimization

While the event system is designed to be performant, understanding these optimization strategies will help you build faster applications, especially when dealing with high-frequency events or many listeners.

⚡ Execution Performance

Use queue: true for I/O Operations

Database queries, API calls, and file operations should run concurrently, not sequentially.

Limit Listeners Per Event

More than 10-15 listeners on a single event often indicates architectural problems.

Check hasListeners() Before Emitting

Skip emission entirely if no listeners exist: if (Khadem.eventBus.hasListeners('event')) { ... }

Use Event Groups Wisely

emitGroup() is great for batch operations but slower than individual emit() calls for few events.

Optimize Critical Path Listeners

Listeners with EventPriority.critical should be extremely fast since they block all other listeners.

💾 Memory Management

Always Clean Up Listeners

Call offSubscriber() in dispose/cleanup methods. Forgotten listeners cause memory leaks.

Use Subscribers for Automatic Cleanup

Subscriber pattern makes cleanup easier: one call removes all related listeners.

Avoid Large Payloads

Pass IDs or references instead of entire objects, especially for frequently-emitted events.

Monitor Listener Counts

Use listenerCount() in monitoring tools to detect leaks early in development.

Be Careful with Circular References

Event payloads with circular object references can prevent garbage collection.

🚨 Performance Anti-Patterns

  • Emitting in Loops: Don't emit events inside loops. Collect data and emit once after the loop.
  • Synchronous Heavy Computation: Never perform CPU-intensive work in sync listeners - use isolates or queue: true.
  • Nested Event Emissions: Emitting events within listeners that trigger more events creates cascades and poor performance.
  • Over-Using Events: Not everything should be an event. Direct method calls are faster for simple operations.
  • Ignoring Listener Counts: Hundreds of listeners on a single event indicates poor design - refactor into groups or use different events.

📊 Performance Metrics to Monitor

  • Total number of registered events (from listeners.length)
  • Average listeners per event (should be < 10 for most events)
  • Number of event groups (should be manageable, < 50 typically)
  • Subscriber count (from subscriberEvents.length)
  • Memory usage trends (watch for gradual increases indicating leaks)
  • Event emission frequency (identify hot paths for optimization)

On this page