199 lines
4.9 KiB
Markdown
199 lines
4.9 KiB
Markdown
# Event Bus Guide
|
|
|
|
The event bus enables event-driven architecture for loose coupling between modules using an in-memory publish-subscribe pattern.
|
|
|
|
## Architecture
|
|
|
|
**In-memory event bus** - Simple, fast, synchronous
|
|
**Publisher-subscriber pattern** - Decouple event producers from consumers
|
|
**Type-safe events** - Events are Go structs implementing Event interface
|
|
|
|
## Core Concepts
|
|
|
|
### Events
|
|
|
|
Events represent things that have happened in the system.
|
|
|
|
**Naming**: Past tense (ResourceCreated, ResourceUpdated, ResourceDeleted)
|
|
|
|
```go
|
|
type ResourceCreatedEvent struct {
|
|
BaseEvent
|
|
ResourceID int32 `json:"resource_id"`
|
|
Name string `json:"name"`
|
|
CreatedBy int32 `json:"created_by"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
```
|
|
|
|
### Event Interface
|
|
|
|
All events implement the Event interface:
|
|
|
|
```go
|
|
type Event interface {
|
|
EventName() string
|
|
EventID() string
|
|
OccurredAt() time.Time
|
|
}
|
|
```
|
|
|
|
### BaseEvent
|
|
|
|
Provides common event fields:
|
|
|
|
```go
|
|
type BaseEvent struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
```
|
|
|
|
## Publishing Events
|
|
|
|
Emit events when something happens:
|
|
|
|
```go
|
|
func (s *service) CreateResource(ctx context.Context, req *Request) (*Resource, error) {
|
|
// Create resource
|
|
resource, err := s.repo.Create(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Publish event
|
|
event := &ResourceCreatedEvent{
|
|
ResourceID: resource.ID,
|
|
Name: resource.Name,
|
|
CreatedBy: req.UserID,
|
|
CreatedAt: resource.CreatedAt,
|
|
}
|
|
s.eventBus.Publish(ctx, event)
|
|
|
|
return resource, nil
|
|
}
|
|
```
|
|
|
|
**Note**: Publish is fire-and-forget. Failures don't block the operation.
|
|
|
|
## Subscribing to Events
|
|
|
|
Listen for events and react:
|
|
|
|
```go
|
|
func (l *ResourceListener) Init(eventBus eventbus.EventBus) {
|
|
// Subscribe to events
|
|
eventBus.Subscribe("resource.created", l.HandleResourceCreated)
|
|
eventBus.Subscribe("resource.updated", l.HandleResourceUpdated)
|
|
}
|
|
|
|
func (l *ResourceListener) HandleResourceCreated(ctx context.Context, event eventbus.Event) error {
|
|
resourceEvent := event.(*ResourceCreatedEvent)
|
|
|
|
// React to event
|
|
log.Info("Resource created", zap.Int32("id", resourceEvent.ResourceID))
|
|
|
|
// Trigger other actions
|
|
return l.notificationService.NotifyResourceCreated(ctx, resourceEvent.ResourceID)
|
|
}
|
|
```
|
|
|
|
## Event Flow
|
|
|
|
```
|
|
Service → Publish Event → Event Bus → Notify Subscribers → Execute Handlers
|
|
```
|
|
|
|
**Synchronous**: Subscribers execute in the same request context
|
|
**Ordered**: Subscribers execute in registration order
|
|
**Error handling**: Subscriber errors are logged but don't fail the operation
|
|
|
|
## Common Patterns
|
|
|
|
### Cross-Module Communication
|
|
|
|
Module A publishes events, Module B subscribes:
|
|
|
|
```go
|
|
// Module A (Resources)
|
|
func (s *resourceService) Delete(ctx context.Context, id int32) error {
|
|
err := s.repo.Delete(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.eventBus.Publish(ctx, &ResourceDeletedEvent{ResourceID: id})
|
|
return nil
|
|
}
|
|
|
|
// Module B (Analytics)
|
|
func (l *analyticsListener) HandleResourceDeleted(ctx context.Context, event eventbus.Event) error {
|
|
evt := event.(*ResourceDeletedEvent)
|
|
return l.analyticsService.RecordDeletion(ctx, evt.ResourceID)
|
|
}
|
|
```
|
|
|
|
### Audit Logging
|
|
|
|
Subscribe to all events for audit trail:
|
|
|
|
```go
|
|
func (l *auditListener) Init(eventBus eventbus.EventBus) {
|
|
eventBus.Subscribe("*.created", l.HandleCreated)
|
|
eventBus.Subscribe("*.updated", l.HandleUpdated)
|
|
eventBus.Subscribe("*.deleted", l.HandleDeleted)
|
|
}
|
|
|
|
func (l *auditListener) HandleCreated(ctx context.Context, event eventbus.Event) error {
|
|
return l.auditService.Log(ctx, "created", event)
|
|
}
|
|
```
|
|
|
|
### Async Processing
|
|
|
|
Trigger background jobs from events:
|
|
|
|
```go
|
|
func (l *processingListener) HandleFileUploaded(ctx context.Context, event eventbus.Event) error {
|
|
evt := event.(*FileUploadedEvent)
|
|
|
|
// Queue async job
|
|
return l.jobQueue.Enqueue(ctx, &ProcessFileJob{
|
|
FileID: evt.FileID,
|
|
})
|
|
}
|
|
```
|
|
|
|
## Registration
|
|
|
|
Register listeners during module initialization:
|
|
|
|
```go
|
|
// src/app/resources/cmd/init.go
|
|
func Init(container *dig.Container) error {
|
|
return container.Invoke(func(
|
|
eventBus eventbus.EventBus,
|
|
listener *listeners.ResourceListener,
|
|
) {
|
|
listener.Init(eventBus)
|
|
})
|
|
}
|
|
```
|
|
|
|
## File Locations
|
|
|
|
| Component | Path |
|
|
|-----------|------|
|
|
| Event bus interface | `src/pkg/eventbus/eventbus.go` |
|
|
| Event interface | `src/pkg/eventbus/event.go` |
|
|
| Base event | `src/pkg/eventbus/base_event.go` |
|
|
| Implementation | `src/pkg/eventbus/memory_eventbus.go` |
|
|
| Domain events | `src/app/*/domain/events/` |
|
|
| Event listeners | `src/app/*/domain/listeners/` |
|
|
|
|
## Next Steps
|
|
|
|
- **Define events**: Create event structs in `domain/events/`
|
|
- **Implement listeners**: Handle events in `domain/listeners/`
|
|
- **Publish events**: Emit events in service layer
|