web-production-saas-starter/go-b2b-starter/docs/event-bus.md
2025-12-16 18:54:41 +04:00

4.9 KiB

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)

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:

type Event interface {
    EventName() string
    EventID() string
    OccurredAt() time.Time
}

BaseEvent

Provides common event fields:

type BaseEvent struct {
    ID         string    `json:"id"`
    Name       string    `json:"name"`
    Timestamp  time.Time `json:"timestamp"`
}

Publishing Events

Emit events when something happens:

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:

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:

// 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:

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:

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:

// 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