What It Does

Guarantees that events are published to NATS, even if NATS is temporarily down.

How It Works

  1. Save data + event to PostgreSQL in same transaction
  2. Background worker (relay) publishes events from outbox to NATS
  3. Automatic retry on failure
  4. Events are marked as published once successful

Key Files

FilePurpose
internal/shared/infrastructure/outbox/event.goOutbox event model
internal/shared/infrastructure/outbox/repository.goRepository interface
internal/shared/infrastructure/outbox/postgresql/outbox_repository.goPostgres implementation
internal/shared/infrastructure/outbox/relay.goBackground worker

Usage Example

// In your service/usecase
func (s *UserService) Create(ctx context.Context, user User) error {
    return s.db.Transaction(func(tx *gorm.DB) error {
        // 1. Save user
        if err := tx.Create(&user).Error; err != nil {
            return err
        }

        // 2. Create event
        payload, _ := json.Marshal(user)
        event := outbox.NewOutboxEvent(user.ID, "UserCreated", payload)

        // 3. Save to outbox (using same transaction)
        return outboxRepo.Save(ctx, event)
    })
}

Monitoring

-- Check pending events
SELECT * FROM outbox_events WHERE published = false;

-- Check failed events (dead letters)
SELECT * FROM outbox_events WHERE published = false AND attempts >= 3;

-- Check recently published events
SELECT * FROM outbox_events WHERE published = true ORDER BY published_at DESC LIMIT 10;

Configuration

  • Poll interval: 1 second
  • Max retries: 3 attempts
  • Cleanup: Deletes published events older than 7 days (configurable)

Testing

# Test with NATS down
docker stop go_nats
curl -X POST .../register  # Still works! (stored in DB)
docker start go_nats       # Relay worker picks up and publishes