Inter-Module Communication Patterns
Complete guide to EventBus and ActionRegistry patterns for module communication
Inter-Module Communication Patterns
This document provides the complete reference for how modules communicate with each other in GoGrab Space.
Two Communication Mechanisms
1. EventBus (Reactive Events)
Use for: Broadcasting state changes, notifications, lifecycle events
2. ActionRegistry (Command Pattern)
Use for: Invoking operations, requesting data, triggering workflows
EventBus Pattern
Emitting Events
import { inject } from '@angular/core';
import { EventBusService } from '@libs-core/event-system';
export class UserManagementModule {
private readonly eventBus = inject(EventBusService);
async createUser(userData: CreateUserInput) {
const user = await this.userService.create(userData);
// Emit event for other modules to react
this.eventBus.emit('user:created', {
userId: user.id,
email: user.email,
tenantId: context.tenantId,
timestamp: Date.now()
});
return user;
}
}
Listening to Events
import { inject, OnInit } from '@angular/core';
import { EventBusService } from '@libs-core/event-system';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class AnalyticsModule implements OnInit {
private readonly eventBus = inject(EventBusService);
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
// Listen for user creation events
this.eventBus.on('user:created').pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(event => {
this.trackUserCreation({
userId: event.userId,
source: 'user-management',
timestamp: event.timestamp
});
});
}
}
Standard Event Names
| Event | Payload | Description |
|---|---|---|
user:created |
{ userId, email, tenantId } |
New user created |
user:updated |
{ userId, changes, tenantId } |
User data updated |
user:deleted |
{ userId, tenantId } |
User deleted |
module:loaded |
{ moduleId, version } |
Module initialized |
module:error |
{ moduleId, error } |
Module error occurred |
data:changed |
{ collection, documentId } |
Data mutation |
ActionRegistry Pattern
Defining Public Actions
import { Injectable, inject } from '@angular/core';
import { Observable, of, map, catchError } from 'rxjs';
import {
PublicAction,
ExecutableAction,
ActionContext,
ActionParams,
ActionParameterBuilder,
ParameterBuilders,
} from '@libs-core/action-registry';
import { EventBusService } from '@libs-core/event-system';
import { LoggerService } from '@libs-core/log-system';
export interface CreateUserInput extends ActionParams {
email: string;
firstName: string;
lastName: string;
roleId: string;
tenantId: string;
}
export interface CreateUserOutput {
success: boolean;
userId?: string;
error?: string;
}
@PublicAction({
id: 'users:create-user',
name: 'Create User',
description: 'Create a new user with role assignment',
permissions: ['users:create'],
tags: ['users', 'crud', 'creation'],
parameters: [
ParameterBuilders.email('email')
.label('Email Address')
.helpText('User email address for login')
.build(),
ActionParameterBuilder.string('firstName')
.description('User first name')
.required('First name is required')
.minLength(2, 'First name must be at least 2 characters')
.maxLength(50, 'First name must not exceed 50 characters')
.label('First Name')
.placeholder('John')
.build(),
ActionParameterBuilder.string('lastName')
.description('User last name')
.required('Last name is required')
.label('Last Name')
.build(),
ActionParameterBuilder.string('roleId')
.description('Role ID to assign')
.required('Role is required')
.label('Role')
.build(),
],
})
@Injectable({ providedIn: 'root' })
export class CreateUserAction implements ExecutableAction<CreateUserInput, CreateUserOutput> {
private readonly userService = inject(UserService);
private readonly eventBus = inject(EventBusService);
private readonly logger = inject(LoggerService).child({ source: 'CreateUserAction' });
execute(params: CreateUserInput, context: ActionContext): Observable<CreateUserOutput> {
this.logger.info('Executing CreateUser action', { email: params.email });
return this.userService.createUser({
email: params.email,
firstName: params.firstName,
lastName: params.lastName,
roleId: params.roleId,
}).pipe(
map(user => {
// Emit event for other modules
this.eventBus.emit('user:created', {
userId: user.id,
tenantId: params.tenantId,
timestamp: Date.now()
});
return { success: true, userId: user.id };
}),
catchError(error => {
this.logger.error('Failed to create user', { error });
return of({ success: false, error: error.message });
})
);
}
}
Invoking Actions from Other Modules
import { inject } from '@angular/core';
import { ActionRegistryService } from '@libs-core/action-registry';
export class OnboardingModule {
private readonly actionRegistry = inject(ActionRegistryService);
async onboardNewUser(email: string, name: string) {
// Invoke user-management module's action
const result = await firstValueFrom(
this.actionRegistry.execute('users:create-user', {
email,
firstName: name.split(' ')[0],
lastName: name.split(' ')[1] || '',
roleId: 'default-user-role',
tenantId: this.contextService.currentTenantId()
})
);
if (result.success) {
console.log('User created:', result.userId);
// Continue onboarding...
} else {
console.error('Failed to create user:', result.error);
}
}
}
Registering Actions in Module Manifest
export class UserManagementModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = {
id: 'user-management',
version: '1.0.0',
name: 'User Management',
// Public actions available to other modules
publicActions: [
CreateUserAction,
UpdateUserAction,
DeleteUserAction,
GetUserAction,
ListUsersAction,
],
};
}
Widget Flow Execution
Widgets can trigger module actions via the flow executor:
import { Component, inject } from '@angular/core';
import { WIDGET_FLOW_EXECUTOR_SERVICE } from '@libs-foundational/core-angular';
import type { IWidgetFlowExecutorService } from '@libs-foundational/core-models';
@Component({
selector: 'lib-button-widget',
template: `
<ui-button
[text]="buttonText()"
(click)="handleClick($event)"
/>
`
})
export class ButtonWidgetComponent {
// Optional injection - won't crash if Singularity not loaded
private readonly flowExecutor = inject<IWidgetFlowExecutorService>(
WIDGET_FLOW_EXECUTOR_SERVICE,
{ optional: true }
);
async handleClick(event: MouseEvent) {
const config = this.config();
if (config.action === 'flow' && config.flowId && this.flowExecutor) {
// Execute Singularity flow
await this.flowExecutor.executeFlow(config.flowId, {
widgetId: this.widget().id,
event: 'click',
mouseX: event.clientX,
mouseY: event.clientY
});
} else if (config.action === 'module-action' && config.actionId) {
// Execute module action directly
const actionRegistry = inject(ActionRegistryService);
actionRegistry.execute(config.actionId, config.actionParams).subscribe();
}
}
}
Best Practices
1. Event Naming Convention
- Use
module:event-nameformat - Examples:
user:created,order:completed,payment:failed - Keep event names consistent across modules
2. Action Naming Convention
- Use
module:action-nameformat - Examples:
users:create-user,orders:cancel-order - Action IDs should be kebab-case
3. Type Safety
- Always define input/output interfaces
- Use
ActionParamsbase for inputs - Include JSDoc comments
4. Error Handling
- Actions should return
{ success: boolean, error?: string } - Never throw exceptions from actions
- Log errors via LoggerService
5. Permissions
- Always declare required permissions in
@PublicAction - Check permissions in action execution context
- Fail gracefully if permissions missing
6. Memory Management
- Use
takeUntilDestroyed()for event subscriptions - Clean up subscriptions in
ngOnDestroy - Avoid memory leaks
Complete Example: Two Modules Communicating
Module A: User Management (Provider)
// actions/create-user.action.ts
@PublicAction({
id: 'users:create-user',
name: 'Create User',
permissions: ['users:create'],
parameters: [/* ... */],
})
@Injectable({ providedIn: 'root' })
export class CreateUserAction implements ExecutableAction {
private readonly eventBus = inject(EventBusService);
execute(params, context): Observable<CreateUserOutput> {
return this.userService.create(params).pipe(
tap(user => {
// Emit event
this.eventBus.emit('user:created', {
userId: user.id,
email: user.email
});
}),
map(user => ({ success: true, userId: user.id }))
);
}
}
// user-management.module.ts
export class UserManagementModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = {
id: 'user-management',
publicActions: [CreateUserAction],
};
}
Module B: Analytics (Consumer)
// analytics.module.ts
export class AnalyticsModule implements BrowserModuleContract {
private readonly eventBus = inject(EventBusService);
private readonly actionRegistry = inject(ActionRegistryService);
initialize(): Observable<void> {
// Listen to user creation events
this.eventBus.on('user:created').subscribe(event => {
this.trackEvent('user_created', {
userId: event.userId,
timestamp: Date.now()
});
});
return of(void 0);
}
// Can also invoke user actions
async getUserData(userId: string) {
const result = await firstValueFrom(
this.actionRegistry.execute('users:get-user', { userId })
);
return result.data;
}
}
Testing Inter-Module Communication
// test/inter-module-communication.spec.ts
describe('Inter-Module Communication', () => {
let eventBus: EventBusService;
let actionRegistry: ActionRegistryService;
beforeEach(() => {
eventBus = TestBed.inject(EventBusService);
actionRegistry = TestBed.inject(ActionRegistryService);
});
it('should emit event when user created', (done) => {
eventBus.on('user:created').subscribe(event => {
expect(event.userId).toBeDefined();
expect(event.email).toBe('[email protected]');
done();
});
// Trigger user creation
actionRegistry.execute('users:create-user', {
email: '[email protected]',
firstName: 'Test',
lastName: 'User',
roleId: 'user',
tenantId: 'test-tenant'
}).subscribe();
});
it('should invoke action from another module', async () => {
const result = await firstValueFrom(
actionRegistry.execute('users:create-user', {
email: '[email protected]',
firstName: 'Test',
lastName: 'User',
roleId: 'user',
tenantId: 'test-tenant'
})
);
expect(result.success).toBe(true);
expect(result.userId).toBeDefined();
});
});
Last Updated: December 9, 2025 - Session 10.2