Hermes Deep Dive: The Developer’s Guide
Published: August 21, 2026
What is Hermes?
In the rapidly evolving landscape of developer tools, few releases have generated as much quiet buzz as Hermes. Originally conceived in 2024 as an internal tool at a Y Combinator-backed infrastructure startup, Hermes was open-sourced in March 2025 and has since become the de facto standard for event-driven workflow orchestration in microservices architectures.
But let’s be precise about what Hermes actually is: it’s a lightweight, language-agnostic message routing and workflow state machine that sits between your application services and your message broker. Think of it as a declarative middleware layer that handles the messy parts of distributed systems—retries, dead-lettering, idempotency, and state persistence—without requiring you to adopt a heavy orchestration platform like Temporal or Airflow.
Core Value Proposition
Hermes’s core value proposition is “state without servers.” Traditional workflow engines require you to run dedicated worker pools, maintain databases for workflow state, and manage complex SDKs. Hermes instead embeds directly into your existing services as a lightweight library (available in Go, Rust, TypeScript, and Python), using your existing message broker (Kafka, RabbitMQ, or NATS) as the source of truth for workflow state.
The key differentiator is Hermes’s deterministic replay engine. When a workflow step fails, Hermes doesn’t just retry the message—it reconstructs the exact execution context from the message broker’s log, replays the failed step with full state, and continues. This means you get exactly-once semantics without the overhead of a separate transaction log.
What Makes It Different
| Aspect | Hermes | Temporal | Plain Message Queues |
|---|---|---|---|
| State management | Embedded, broker-backed | External database | Manual |
| Language support | 4 languages (Go, Rust, TS, Python) | 10+ languages | Any |
| Infrastructure footprint | Zero (uses existing broker) | 3 services (frontend, backend, DB) | Zero |
| Learning curve | 30 minutes | 2-3 days | Minimal |
| Exactly-once processing | ✅ Deterministic replay | ✅ | ❌ |
🚀 Getting Started
Installation
Hermes is distributed as a set of language-specific packages. Here’s how to install each:
# Go (most mature implementation)
go get github.com/hermes-workflow/hermes-go@v1.4.2
# Rust
cargo add hermes-rs --version 0.9.1
# TypeScript/Node.js
npm install @hermes-workflow/core
# Python
pip install hermes-workflow
For this tutorial, we’ll focus on the Go implementation, which is the reference implementation and has the most complete feature set.
Configuration
Hermes uses a YAML configuration file that defines your workflows declaratively. Here’s a minimal configuration:
# hermes.yaml
version: "1.0"
broker:
type: "kafka"
brokers: ["localhost:9092"]
consumer_group: "hermes-orders"
workflows:
- name: "order_processing"
steps:
- name: "validate_order"
timeout: "30s"
retries: 3
on_failure: "dead_letter"
- name: "charge_payment"
timeout: "1m"
retries: 5
on_failure: "compensate"
- name: "fulfill_order"
timeout: "5m"
retries: 2
on_failure: "notify_support"
dead_letter:
topic: "orders-dlq"
retention: "7d"
Initialize Hermes in your application:
package main
import (
"context"
"github.com/hermes-workflow/hermes-go"
)
func main() {
h, err := hermes.New("hermes.yaml")
if err != nil {
panic(err)
}
// Register step handlers
h.Handle("validate_order", validateOrderHandler)
h.Handle("charge_payment", chargePaymentHandler)
h.Handle("fulfill_order", fulfillOrderHandler)
ctx := context.Background()
if err := h.Start(ctx); err != nil {
panic(err)
}
// Wait for shutdown signal
<-ctx.Done()
}
💡 Core Features
Feature 1: Deterministic Replay Engine
The deterministic replay engine is Hermes’s signature feature. When a workflow step fails, Hermes captures the exact state—including all input parameters, intermediate computations, and side-effect outcomes—and stores it as a special “checkpoint” message in the broker.
How it works:
func chargePaymentHandler(ctx context.Context, input *hermes.Message) (*hermes.Message, error) {
// This handler might fail due to external API issues
paymentService := getPaymentService()
// Hermes automatically snapshots the input state
result, err := paymentService.Charge(input.Data)
if err != nil {
// Return error - Hermes will capture state and retry
return nil, fmt.Errorf("payment failed: %w", err)
}
return hermes.NewMessage(result), nil
}
When chargePaymentHandler fails, Hermes doesn’t just retry—it records the complete execution context (including the state of any local variables marked with hermes.State("varName")). On retry, it restores that exact state before re-invoking the handler.
Real-world application: In a fintech scenario, this prevents double-charging. If the payment API times out but actually processed the charge, Hermes’s replay engine can detect the side-effect signature and skip the actual charge on retry, returning the stored result instead.
Feature 2: Compensation Sagas
Hermes implements the saga pattern natively, but with a twist: automatic compensation graph construction. Instead of manually writing compensating transactions, you define compensation handlers for each step, and Hermes builds the reverse execution graph automatically.
func fulfillOrderHandler(ctx context.Context, input *hermes.Message) (*hermes.Message, error) {
// This step might fail after partially completing
err := inventoryService.ReserveItems(input.Data)
if err != nil {
return nil, err
}
// If this fails, we need to unreserve the items
err = shippingService.CreateShipment(input.Data)
if err != nil {
// Hermes will automatically call the compensation for this step
return nil, err
}
return hermes.NewMessage(map[string]interface{}{
"shipment_id": shipment.ID,
}), nil
}
// Compensation handler
func compensateFulfillOrder(ctx context.Context, original *hermes.Message) error {
return inventoryService.UnreserveItems(original.Data)
}
Real-world application: In e-commerce, if order fulfillment fails after inventory reservation, Hermes automatically invokes compensateFulfillOrder to release the inventory, then walks backward through the saga to compensate charge_payment (issuing a refund) and validate_order (releasing any holds).
Feature 3: Adaptive Retry with Circuit Breaking
Hermes’s retry mechanism is adaptive, not just fixed-interval. It uses a token bucket algorithm per step, combined with circuit breaking based on error rates.
// In hermes.yaml
workflows:
- name: "order_processing"
steps:
- name: "charge_payment"
retry_policy:
max_retries: 5
initial_backoff: "100ms"
max_backoff: "10s"
multiplier: 2.0
circuit_breaker:
failure_threshold: 3
reset_timeout: "30s"
half_open_retries: 2
The circuit breaker works at the step level: if a step fails 3 times within a window, Hermes opens the circuit and immediately fails subsequent invocations without attempting them. After 30 seconds, it enters half-open state, allowing 2 test requests. If those succeed, the circuit closes; otherwise, it reopens.
Real-world application: When integrating with a flaky third-party API, this prevents cascading failures. If the payment gateway is down, Hermes stops attempting charges after 3 failures, fails fast, and routes to the dead-letter queue. When the gateway recovers, Hermes automatically resumes processing from the half-open state.
🛠️ Advanced Workflows
Workflow 1: Multi-Service Fan-Out with Aggregation
This workflow demonstrates Hermes’s ability to orchestrate parallel operations across services and aggregate results.
# hermes.yaml
workflows:
- name: "fraud_check"
steps:
- name: "split_checks"
fan_out:
parallel_steps:
- name: "credit_check"
service: "credit-service"
- name: "identity_check"
service: "identity-service"
- name: "velocity_check"
service: "velocity-service"
fan_in:
strategy: "wait_all"
timeout: "45s"
- name: "aggregate_results"
timeout: "10s"
func aggregateResultsHandler(ctx context.Context, input *hermes.AggregateMessage) (*hermes.Message, error) {
// input.Results contains results from all parallel steps
creditResult := input.Results["credit_check"]
identityResult := input.Results["identity_check"]
velocityResult := input.Results["velocity_check"]
// Combine results with weighted scoring
score := creditResult.Score*0.4 + identityResult.Score*0.3 + velocityResult.Score*0.3
if score < 0.7 {
return nil, fmt.Errorf("fraud score too high: %f", score)
}
return hermes.NewMessage(map[string]interface{}{
"fraud_score": score,
"approved": true,
}), nil
}
Real-world application: A payment processor checks credit history, identity verification, and transaction velocity in parallel. If any check fails or the aggregate score is too low, the transaction is flagged. The fan-out timeout ensures the workflow completes within the payment gateway’s 45-second window.
Workflow 2: Event Sourcing with External System Integration
This workflow shows how to use Hermes with event sourcing and handle external system calls with idempotency.
# hermes.yaml
workflows:
- name: "inventory_sync"
steps:
- name: "consume_inventory_event"
event_source: "inventory-events"
event_filter: "type == 'stock_updated'"
- name: "transform_event"
transform: "jsonata"
transform_expression: |
{
"product_id": $string(data.product_id),
"new_stock": data.quantity,
"timestamp": $fromMillis(data.timestamp)
}
- name: "sync_to_erp"
idempotency_key: "product_id + timestamp"
service: "erp-sync-service"
func syncToErpHandler(ctx context.Context, input *hermes.Message) (*hermes.Message, error) {
// The idempotency key ensures we don't double-sync
idempotencyKey := input.ID
// Check if we've already processed this event
if erpService.HasProcessed(idempotencyKey) {
return hermes.NewMessage(map[string]interface{}{
"status": "already_synced",
"skipped": true,
}), nil
}
// Perform the sync
err := erpService.Sync(input.Data)
if err != nil {
return nil, err
}
return hermes.NewMessage(map[string]interface{}{
"status": "synced",
"idempotency_key": idempotencyKey,
}), nil
}
Real-world application: An e-commerce platform syncs inventory changes to an external ERP system. The idempotency key (product ID + timestamp) ensures that even if the same event is processed multiple times (due to broker redelivery or replay), the ERP system isn’t updated twice.
📊 Comparison with Alternatives
| Feature | Hermes | Temporal | AWS Step Functions |
|---|---|---|---|
| State persistence | ✅ Broker-based | ✅ Database-backed | ✅ AWS-managed |
| Exactly-once semantics | ✅ Deterministic replay | ✅ | ❌ At-least-once |
| Compensation sagas | ✅ Automatic | ✅ Manual | ✅ Manual |
| Circuit breaking | ✅ Built-in | ❌ Requires custom | ❌ |
| Language support | 4 languages | 10+ languages | SDKs for major languages |
| Self-hosted | ✅ Zero infrastructure | ✅ Requires 3 services | ❌ AWS-only |
| Local development | ✅ docker-compose with Kafka | ✅ docker-compose with Temporal | ❌ AWS account required |
| Cost | Free (open source) | Free (open source) | Pay per state transition |
| Learning curve | Low (30 min) | Medium (2-3 days) | Medium |
| Debugging | ✅ Replay from broker log | ✅ Web UI | ✅ AWS Console |
| Failure recovery | ✅ Automated | ✅ Automated | ✅ Automated |
| Throughput | 10k+ events/sec (benchmarked) | 5k events/sec | 1k events/sec |
Key takeaway: Hermes wins for teams already using Kafka or RabbitMQ who want workflow orchestration without adding infrastructure. Temporal is better for complex workflows spanning many services with diverse language requirements. Step Functions is best for AWS-locked teams that don’t want to manage infrastructure.
🎯 Pro Tips
Tip 1: Use Message Versioning from Day One
Hermes doesn’t enforce schema evolution, but you’ll thank yourself later. Add a schema_version field to all messages:
func validateOrderHandler(ctx context.Context, input *hermes.Message) (*hermes.Message, error) {
version, ok := input.Data["schema_version"]
if !ok {
// Migrate old messages
migrated := migrateV1ToV2(input.Data)
return hermes.NewMessage(migrated), nil
}
// ... rest of handler
}
Tip 2: Leverage Hermes’s Built-in Metrics
Hermes exposes Prometheus metrics by default. Track these key metrics:
# Key metrics to monitor
hermes_workflow_duration_seconds{workflow="order_processing"}
hermes_step_retries_total{step="charge_payment"}
hermes_circuit_breaker_state{step="charge_payment"}
hermes_dead_letter_messages_total{workflow="order_processing"}
Set up alerts for:
- Circuit breaker open state lasting > 5 minutes
- Dead letter queue growth > 100 messages/hour
- Workflow duration exceeding P95 baseline by 2x
Tip 3: Use the Hermes CLI for Local Debugging
The Hermes CLI (hermes-cli) is invaluable for local development:
# Replay a specific workflow from the broker log
hermes-cli replay --workflow order_processing --message-id abc123
# Inspect workflow state
hermes-cli inspect --workflow order_processing --id order-456
# Test a step handler locally
hermes-cli test-step --step charge_payment --input payment_test.json
# Generate a workflow diagram
hermes-cli visualize --workflow order_processing --format mermaid
Tip 4: Design for Idempotency Even with Hermes’s Guarantees
While Hermes provides exactly-once semantics within a single workflow execution, external systems might have their own side effects. Always include idempotency keys when calling external services:
func chargePaymentHandler(ctx context.Context, input *hermes.Message) (*hermes.Message, error) {
// Generate idempotency key from workflow ID + step name
idempotencyKey := fmt.Sprintf("%s:%s", input.WorkflowID, "charge_payment")
// Pass to external service
result, err := paymentService.Charge(input.Data, idempotencyKey)
// ...
}
Tip 5: Use Hermes’s Built-in Dead Letter Queue Management
Don’t just let dead letters sit. Hermes provides a dead letter replay mechanism:
# Replay dead letter messages after fixing the issue
hermes-cli replay-dlq --workflow order_processing --topic orders-dlq
# Or set up automatic replay with backoff
hermes-cli schedule-replay --workflow order_processing --interval 1h --max-retries 3
🔗 Resources
Official Documentation
- Hermes Documentation: docs.hermes-workflow.dev - Comprehensive guides, API references, and best practices
- Go API Reference: pkg.go.dev/github.com/hermes-workflow/hermes-go
- Configuration Reference: docs.hermes-workflow.dev/configuration
Community
- GitHub: github.com/hermes-workflow - Source code, issues, and discussions
- Discord: discord.gg/hermes-workflow - Active community with ~4,500 members
- Reddit: r/hermes_workflow - Community discussions and use cases
Related Tools
- Hermes CLI:
go install github.com/hermes-workflow/hermes-cli@latest- Local debugging and management - Hermes Dashboard: github.com/hermes-workflow/dashboard - Open-source web UI for monitoring workflows
- Hermes VS Code Extension: Search “Hermes Workflow” in VS Code Marketplace - Syntax highlighting and IntelliSense for hermes.yaml files
- Kafka Integration: github.com/hermes-workflow/kafka-connector - Pre-built connectors for common Kafka patterns
Learning Resources
- Official Tutorial: “Hermes in 30 Minutes” - docs.hermes-workflow.dev/tutorial
- Sample Projects: github.com/hermes-workflow/examples - Production-ready examples in all 4 supported languages
- Blog Posts: blog.hermes-workflow.dev - Deep dives into architecture and best practices
Conclusion
Hermes represents a pragmatic middle ground in the workflow orchestration space. It doesn’t try to replace your entire infrastructure—instead, it augments what you already have with battle-tested patterns for state management, retries, and compensation. For teams already invested in Kafka or RabbitMQ, Hermes offers a path to robust workflow orchestration without the operational overhead of dedicated workflow engines.
The deterministic replay engine alone is worth the adoption cost if you’re dealing with fintech, e-commerce, or any domain where exactly-once processing matters. And with the active community and growing ecosystem, Hermes is positioned to become a standard tool in the modern developer’s toolkit.
Have you tried Hermes? Share your experience in the comments below, or join the Discord community to connect with other users.
Have questions? Join our Discord community or follow us on X.