π 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.)
# 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
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
- Create controller in
internal/server/api/your-feature/
- Implement handler returning
api.Response
- Register route in
internal/server/router/main.go
- 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
- Fork the repository
- Create feature branch:
git checkout -b feature/my-feature
- Make changes and add tests
- Run quality checks:
make inspect
- Commit:
git commit -m 'Add feature X'
- Push:
git push origin feature/my-feature
- 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.