Events
Note: The Events module is not a built-in Goose feature. This documentation describes an event-driven pattern that you can implement manually in your application.
Overview
Events enable loose coupling between components:
- Publish events when things happen
- Subscribe to events to react
- Decouple services
- Enable extensibility
Manual Implementation
Event Dispatcher
package events
import "sync"
type Event interface {
EventName() string
}
type EventHandler func(event interface{}) error
type Dispatcher struct {
handlers map[string][]EventHandler
mutex sync.RWMutex
}
func NewDispatcher() *Dispatcher {
return &Dispatcher{
handlers: make(map[string][]EventHandler),
}
}
func (d *Dispatcher) Subscribe(eventName string, handler EventHandler) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.handlers[eventName] = append(d.handlers[eventName], handler)
}
func (d *Dispatcher) Dispatch(event Event) error {
d.mutex.RLock()
handlers := d.handlers[event.EventName()]
d.mutex.RUnlock()
for _, handler := range handlers {
if err := handler(event); err != nil {
return err
}
}
return nil
}
func (d *Dispatcher) DispatchAsync(event Event) {
d.mutex.RLock()
handlers := d.handlers[event.EventName()]
d.mutex.RUnlock()
for _, handler := range handlers {
go handler(event)
}
}
Defining Events
package events
import "time"
type UserRegistered struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Timestamp time.Time `json:"timestamp"`
}
func (e *UserRegistered) EventName() string {
return "user.registered"
}
type OrderPlaced struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Total float64 `json:"total"`
Timestamp time.Time `json:"timestamp"`
}
func (e *OrderPlaced) EventName() string {
return "order.placed"
}
Event Listeners
type SendWelcomeEmailListener struct {
emailService *EmailService
}
func (l *SendWelcomeEmailListener) Handle(event interface{}) error {
e := event.(*UserRegistered)
return l.emailService.SendWelcomeEmail(e.Email, e.Name)
}
Publishing Events
In Service
type UserService struct {
db *gorm.DB `inject:""`
dispatcher *Dispatcher `inject:""`
}
func (s *UserService) Register(dto RegisterDTO) (*User, error) {
user := &User{
ID: uuid.New().String(),
Email: dto.Email,
Name: dto.Name,
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
// Dispatch event asynchronously
s.dispatcher.DispatchAsync(&UserRegistered{
UserID: user.ID,
Email: user.Email,
Name: user.Name,
Timestamp: time.Now(),
})
return user, nil
}
In Controller
type OrderController struct {
orderService *OrderService `inject:""`
dispatcher *Dispatcher `inject:""`
}
func (c *OrderController) Create(ctx types.Context) any {
body, _ := ctx.Request().Body()
var dto CreateOrderDTO
json.Unmarshal(body, &dto)
order, err := c.orderService.Create(dto)
if err != nil {
return map[string]any{"error": err.Error(), "_status": 500}
}
c.dispatcher.DispatchAsync(&OrderPlaced{
OrderID: order.ID,
UserID: order.UserID,
Total: order.Total,
Timestamp: time.Now(),
})
return map[string]any{"data": order, "_status": 201}
}
Registering the Dispatcher
Register the dispatcher as a singleton using the types.Configurable interface:
func (m *AppModule) Configure(c types.Container) error {
dispatcher := events.NewDispatcher()
// Subscribe handlers
dispatcher.Subscribe("user.registered", func(e interface{}) error {
event := e.(*events.UserRegistered)
// Handle event (e.g., send welcome email)
return nil
})
return c.Register(func() *events.Dispatcher {
return dispatcher
}, "", true)
}
Using with Queues Module
For more reliable event processing, use the queues module:
import "github.com/awesome-goose/goose/modules/queues"
type ProcessEventJob struct {
EventName string `json:"event_name"`
Payload string `json:"payload"`
}
// Queue event processing instead of handling immediately
func (s *UserService) Register(dto RegisterDTO) (*User, error) {
// ... create user ...
// Queue the event processing for reliability
payload, _ := json.Marshal(&UserRegistered{
UserID: user.ID,
Email: user.Email,
Name: user.Name,
Timestamp: time.Now(),
})
s.queue.Dispatch(&ProcessEventJob{
EventName: "user.registered",
Payload: string(payload),
})
return user, nil
}
Best Practices
- Name events in past tense -
UserRegistered, notRegisterUser - Include timestamps - Always include when the event occurred
- Keep events immutable - Don't modify events after creation
- Make handlers idempotent - Safe to process multiple times
- Use async dispatch for non-critical handlers
- Handle failures gracefully - Don't break main flow
- Log event processing for debugging
Common Event Types
// User events
"user.registered"
"user.updated"
"user.deleted"
"user.logged_in"
// Order events
"order.placed"
"order.paid"
"order.shipped"
"order.cancelled"
// Product events
"product.created"
"product.updated"
"product.out_of_stock"