threedotslab/templates/event_publisher.md

3.4 KiB

Event Publisher Adapter Scaffold Template

Generate a Watermill publisher adapter that implements a domain/app-layer interface. The adapter lives in adapters/ and translates domain operations into published messages. The interface lives in app/command/services.go.

Placeholders

  • {{Name}} — PascalCase aggregate name (e.g., Training)
  • {{name}} — camelCase (e.g., training)
  • {{name_snake}} — snake_case (e.g., training)
  • {{name_lower}} — all lowercase (e.g., training)
  • {{module}} — Go module path from go.mod
  • {{event}} — PascalCase first event name (e.g., TrainingScheduled)
  • {{topic}} — Dot-notation topic (e.g., training.scheduled)

File 1: app/command/services.go

If this file already exists, add the interface. Otherwise create it:

package command

import "context"

// {{Name}}EventPublisher defines events that can be emitted for {{name_lower}} operations.
// Implemented by adapters (e.g., Watermill AMQP adapter).
type {{Name}}EventPublisher interface {
	{{event}}(ctx context.Context) error
	// TODO: Add more event methods as needed
	// Example:
	// {{Name}}Cancelled(ctx context.Context, uuid string) error
}

File 2: adapters/{{name_snake}}_event_publisher.go

package adapters

import (
	"context"
	"encoding/json"

	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/message/router/middleware"
)

type Watermill{{Name}}EventPublisher struct {
	pub message.Publisher
}

func NewWatermill{{Name}}EventPublisher(pub message.Publisher) Watermill{{Name}}EventPublisher {
	return Watermill{{Name}}EventPublisher{pub: pub}
}

// {{event}}Event is the wire format for the {{topic}} topic.
type {{event}}Event struct {
	// TODO: Add event payload fields
	// Example:
	// UUID string    `json:"uuid"`
	// Hour time.Time `json:"hour"`
}

func (p Watermill{{Name}}EventPublisher) {{event}}(ctx context.Context) error {
	event := {{event}}Event{
		// TODO: Map domain data to event fields
	}

	payload, err := json.Marshal(event)
	if err != nil {
		return err
	}

	msg := message.NewMessage(watermill.NewUUID(), payload)
	middleware.SetCorrelationID(watermill.NewUUID(), msg)

	return p.pub.Publish("{{topic}}", msg)
}

Update service/application.go

Wire the publisher adapter in the composition root:

func NewApplication(ctx context.Context) (app.Application, func()) {
    // ... existing clients ...

    publisher, closePub, err := client.NewWatermillPublisher()
    if err != nil { panic(err) }

    eventPublisher := adapters.NewWatermill{{Name}}EventPublisher(publisher)

    return newApplication(ctx, eventPublisher),
        func() {
            // ... existing cleanup ...
            _ = closePub()
        }
}

Update the private newApplication to accept the publisher interface:

func newApplication(
    ctx context.Context,
    eventPublisher command.{{Name}}EventPublisher,
    // ... existing deps ...
) app.Application {
    // ... pass eventPublisher to command handlers that need it
}

Update command handler

Inject the publisher into the command handler that triggers the event:

type {{name}}Handler struct {
    {{name_lower}}Repo    {{name_lower}}.Repository
    eventPublisher command.{{Name}}EventPublisher
}

func (h {{name}}Handler) Handle(ctx context.Context, cmd {{command}}) error {
    // ... domain logic ...
    return h.eventPublisher.{{event}}(ctx)
}