hermes

module
v0.0.0-...-fc278c0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT

README ΒΆ

⚑ HERMES

Production-ready email notification service with queue-based processing, circuit breakers, and automatic retry logic

Go Redis Docker Prometheus

GitHub license git-last-commit GitHub commit activity GitHub top language

πŸ“– Table of Contents


🎯 Why Hermes?

Hermes transforms email sending from a fragile, blocking operation into a resilient, observable microservice. Unlike simple SMTP wrappers, Hermes provides:

  • πŸ”„ Guaranteed Delivery: Dead Letter Queue with automatic retry (up to 5 attempts)
  • πŸ›‘οΈ Production Resilience: Circuit breakers prevent cascading SMTP failures
  • πŸ“Š Full Observability: Prometheus metrics for email success rates, queue depth, and latency
  • βš–οΈ Horizontal Scaling: Redis-backed distributed queue and rate limiting
  • 🎨 Template Management: Dynamic HTML templates with caching
  • πŸ” Multi-App Support: Isolated API keys and rate limits per application

When to Use Hermes

βœ… Perfect for:

  • Microservices needing reliable transactional emails
  • Multi-tenant applications requiring isolated email sending
  • High-volume notification systems (marketing, alerts, reports)
  • Teams wanting email observability without vendor lock-in

❌ Not ideal for:

  • Simple scripts needing one-off emails (use net/smtp directly)
  • Real-time chat applications (consider WebSockets/SSE instead)

οΏ½ Quick Start

Prerequisites

  • Go 1.25+
  • (Optional) Redis for distributed features
  • SMTP server credentials (Gmail, SendGrid, Mailgun, etc.)

1. Install and Configure

# Clone the repository
git clone https://github.com/mauriciofsnts/hermes
cd hermes

# Install dependencies
go mod download

# Create config from example
make start  # Auto-creates config.yaml

2. Configure Your SMTP & App

Edit config.yaml:

smtp:
  host: "smtp.gmail.com"
  port: 587
  username: "your-email@gmail.com"
  password: "your-app-password"
  sender: "noreply@yourapp.com"

apps:
  my-app:
    enabled: true
    apiKey: "7a28c3e0-83e4-426f-89a4-d932cdcadac4"  # Change this!
    limitPerIPPerHour: 1000
    enabledFeatures: [email]

3. Create a Template

# Create templates/welcome.html
cat > templates/welcome.html << 'EOF'
<!DOCTYPE html>
<html>
<body>
  <h1>Welcome, {{.Name}}!</h1>
  <p>{{.Message}}</p>
</body>
</html>
EOF

4. Send Your First Email

# Start the server
make dev

# Send email via API
curl -X POST http://localhost:3000/api/v1/app/notify/notification \
  -H "x-api-key: 7a28c3e0-83e4-426f-89a4-d932cdcadac4" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "welcome",
    "subject": "Welcome to Our Service!",
    "recipients": [{
      "type": "mail",
      "data": {
        "to": "user@example.com",
        "Name": "Alice",
        "Message": "Thanks for joining us!"
      }
    }]
  }'

βœ… Response: {"message": "Email sent successfully"}


πŸ—οΈ Architecture

Hermes follows a clean architecture with dependency injection and interface-based providers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Client    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚ POST /api/v1/app/notify/notification
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         HTTP Server (Chi)               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚ Middleware Chain:            β”‚       β”‚
β”‚  β”‚  β†’ Auth (API Key)            β”‚       β”‚
β”‚  β”‚  β†’ Rate Limiter              β”‚       β”‚
β”‚  β”‚  β†’ Metrics (Prometheus)      β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Template Service                   β”‚
β”‚  β€’ Parse HTML with dynamic data         β”‚
β”‚  β€’ In-memory cache (sync.RWMutex)       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Queue (Redis/Memory)               β”‚
β”‚  β€’ Async processing                     β”‚
β”‚  β€’ Worker reads from queue              β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      SMTP Provider                      β”‚
β”‚  β€’ Circuit breaker (3 failures β†’ open) β”‚
β”‚  β€’ Automatic retry logic                β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€ Success βœ“
       β”‚
       └─ Failure βœ—
          β”‚
          β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Dead Letter Queue (SQLite) β”‚
    β”‚  β€’ Max 5 retry attempts     β”‚
    β”‚  β€’ Background worker        β”‚
    β”‚  β€’ Admin API for monitoring β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Patterns

  • Provider Interface Pattern: All external services (SMTP, Queue, Templates) implement interfaces for easy testing/mocking
  • Circuit Breaker: Prevents cascading SMTP failures; opens after 3 failures, half-opens after 30s
  • Template Caching: Parsed templates cached in-memory with thread-safe access
  • Queue Abstraction: Swap between Redis (distributed) and Memory (development) seamlessly
  • WrappedHandler: Custom router pattern that returns Response objects instead of writing directly to http.ResponseWriter

οΏ½πŸ“¦ Features

πŸ“¦ Features

πŸ”„ Dead Letter Queue (DLQ)

Automatic failure handling with persistent retry logic:

Email Send Failed β†’ DLQ (SQLite)
                     ↓
            Background Worker (5min interval)
                     ↓
        Retry Attempt (max 5 times)
                     ↓
         Success βœ“ or Permanent Failure βœ—

Admin API:

  • GET /api/v1/admin/dlq/stats - View retry statistics
  • GET /api/v1/admin/dlq/pending - List pending retries
  • GET /api/v1/admin/dlq/failed - View permanently failed emails

πŸ›‘οΈ Circuit Breaker

Protects against cascading SMTP failures:

  • Closed (normal): Requests pass through
  • Open (failing): Fast-fail for 30s after 3 failures
  • Half-Open (testing): Allow 1 request to test recovery
// Distributed Redis version shares state across instances
type CircuitBreaker interface {
    CanExecute() bool
    RecordSuccess()
    RecordFailure()
    GetState() string  // "closed", "open", "half-open"
}

πŸ“Š Prometheus Metrics

Production-grade observability out of the box:

# Email metrics
hermes_emails_sent_total{status="success|failed"}
hermes_email_send_duration_seconds

# Queue metrics
hermes_queue_depth
hermes_queue_processing_duration_seconds

# Circuit breaker
hermes_circuit_breaker_state{state="closed|open|half-open"}

# Rate limiting
hermes_rate_limit_events_total{action="allowed|blocked"}

Access at: http://localhost:3000/metrics

βš–οΈ Distributed Features

Run multiple Hermes instances with shared state:

Feature Single Instance Multi-Instance (Redis)
Queue Processing βœ… Memory βœ… Redis (shared jobs)
Circuit Breaker βœ… Local state βœ… Redis (cluster-wide)
Rate Limiting βœ… In-memory βœ… Redis (global limits)
DLQ βœ… SQLite βœ… SQLite (per-instance)

Enable Redis:

redis:
  address: "localhost:6379"
  password: "your-password"
  topic: hermes

🎨 Dynamic Templates

Go template engine with caching:

<!-- templates/invoice.html -->
<!DOCTYPE html>
<html>
<body>
  <h1>Invoice #{{.InvoiceID}}</h1>
  <p>Dear {{.CustomerName}},</p>
  <p>Amount due: ${{.Amount}}</p>
  {{if .IsPastDue}}
    <p style="color: red;">⚠️ Payment overdue!</p>
  {{end}}
</body>
</html>

Template API:

  • POST /api/v1/app/templates - Upload template
  • GET /api/v1/app/templates/{id} - Retrieve template
  • DELETE /api/v1/app/templates/{id} - Delete template

πŸ” Multi-App Support

Isolate email sending per application:

apps:
  app-production:
    enabled: true
    apiKey: "prod-key-xxx"
    limitPerIPPerHour: 5000
    allowedOrigins: ["https://app.example.com"]

  app-staging:
    enabled: true
    apiKey: "staging-key-yyy"
    limitPerIPPerHour: 100
    allowedOrigins: ["https://staging.example.com"]

Each app gets:

  • βœ… Unique API key for authentication
  • βœ… Independent rate limits
  • βœ… Custom CORS origins
  • βœ… Feature flags (email, discord)

πŸ“‘ API Reference

Send Notification

Endpoint: POST /api/v1/app/notify/notification

Headers:

x-api-key: your-api-key
Content-Type: application/json

Request Body:

{
  "templateId": "welcome",
  "subject": "Welcome to Our Service",
  "recipients": [
    {
      "type": "mail",
      "data": {
        "to": "user@example.com",
        "Name": "John Doe",
        "CustomField": "Any value you need in template"
      }
    }
  ]
}

Success Response (200):

{
  "message": "Email sent successfully"
}

Error Response (4xx/5xx):

{
  "error": "Failed to send email: template not found"
}

Health Check

Endpoint: GET /api/v1/health

Response:

{
  "status": "healthy",
  "queue": "redis connected"
}

Template Management

Upload Template:

POST /api/v1/app/templates
x-api-key: your-api-key
Content-Type: application/json

{
  "name": "welcome",
  "content": "<html>...</html>"
}

Get Template:

GET /api/v1/app/templates/welcome
x-api-key: your-api-key

DLQ Management

View Statistics:

GET /api/v1/admin/dlq/stats

Response:

{
  "pending": 5,
  "processing": 2,
  "failed": 1,
  "succeeded": 234
}

Swagger Documentation

Interactive API docs available at: http://localhost:3000/swagger/index.html


πŸ› οΈ Development

Local Development

# Hot reload with Air
make dev

# Run tests
make test

# Integration tests (requires Docker)
make test-integration

# Code quality checks
make inspect  # Runs revive + gosec + staticcheck

# Generate Swagger docs
make swagger

Project Structure

hermes/
β”œβ”€β”€ cmd/hermes/          # Application entry point
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ bootstrap/       # Initialization logic
β”‚   β”œβ”€β”€ config/          # Configuration loading & validation
β”‚   β”œβ”€β”€ metrics/         # Prometheus metrics
β”‚   β”œβ”€β”€ providers/       # External service interfaces
β”‚   β”‚   β”œβ”€β”€ database/    # DLQ persistence (SQLite)
β”‚   β”‚   β”œβ”€β”€ discord/     # Discord webhook integration
β”‚   β”‚   β”œβ”€β”€ queue/       # Queue abstraction (Redis/Memory)
β”‚   β”‚   β”œβ”€β”€ smtp/        # Email sending with circuit breaker
β”‚   β”‚   └── template/    # Template parsing & caching
β”‚   β”œβ”€β”€ server/          # HTTP server & middleware
β”‚   β”‚   β”œβ”€β”€ api/         # Controllers & routing
β”‚   β”‚   β”œβ”€β”€ middleware/  # Auth, rate limiting, logging
β”‚   β”‚   └── router/      # Route definitions
β”‚   └── types/           # Shared data structures
β”œβ”€β”€ templates/           # Email HTML templates
β”œβ”€β”€ config.yaml          # Runtime configuration
└── Makefile             # Build & dev commands

Adding a New Endpoint

  1. Create controller in internal/server/api/your-feature/
  2. Implement handler returning api.Response
  3. Register route in internal/server/router/main.go
  4. Add Swagger comments and run make swagger

Example:

// internal/server/api/myfeature/controller.go
type MyController struct {
    provider providers.SomeProvider
}

func (c *MyController) Route(r api.Router) {
    r.Post("/my-endpoint", c.HandleRequest)
}

func (c *MyController) HandleRequest(r *http.Request) api.Response {
    // Your logic here
    return api.SuccessResponse("Done!")
}

πŸ“š Advanced Examples

Multi-Recipient Email

curl -X POST http://localhost:3000/api/v1/app/notify/notification \
  -H "x-api-key: your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "newsletter",
    "subject": "Monthly Update",
    "recipients": [
      {
        "type": "mail",
        "data": {
          "to": "alice@example.com",
          "Name": "Alice",
          "Content": "Custom content for Alice"
        }
      },
      {
        "type": "mail",
        "data": {
          "to": "bob@example.com",
          "Name": "Bob",
          "Content": "Custom content for Bob"
        }
      }
    ]
  }'

Conditional Template Logic

<!-- templates/order-confirmation.html -->
<!DOCTYPE html>
<html>
<body>
  <h1>Order #{{.OrderID}} Confirmed</h1>

  {{if .IsExpressShipping}}
    <p style="color: green;">⚑ Express shipping - arrives tomorrow!</p>
  {{else}}
    <p>Standard shipping - arrives in 3-5 days</p>
  {{end}}

  <h2>Items ({{len .Items}}):</h2>
  <ul>
    {{range .Items}}
      <li>{{.Name}} - ${{.Price}}</li>
    {{end}}
  </ul>

  <p><strong>Total: ${{.Total}}</strong></p>
</body>
</html>

🀝 Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

Development Workflow

  1. Fork the repository
  2. Create feature branch: git checkout -b feature/my-feature
  3. Make changes and add tests
  4. Run quality checks: make inspect
  5. Commit: git commit -m 'Add feature X'
  6. Push: git push origin feature/my-feature
  7. Open Pull Request

Areas for Contribution

  • πŸ”Œ New Providers: SMS, Slack, Teams integrations
  • πŸ“Š Enhanced Metrics: Custom business metrics
  • πŸ§ͺ Test Coverage: Integration tests, benchmarks
  • πŸ“š Documentation: Tutorials, architecture diagrams
  • πŸ› Bug Fixes: Check Issues

πŸ“„ License

This project is licensed under the MIT License. See LICENSE for details.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL