Tonica Run Modes
Tonica supports four different run modes, allowing you to deploy your application as a unified service or split into specialized components. This flexibility enables better resource utilization and scaling strategies.
Available Run Modes
| Mode | Components | Ports | Use Case |
|---|---|---|---|
| AIO (All-In-One) | gRPC + REST + Workers + Consumers, Metrics | 8080, 50051, 2121 | Development, small deployments |
| Service | gRPC, Metrics | 50051, 2121 | Grpc layer scaling |
| Worker | Temporal workers, Metrics | 2121 | Background task scaling |
| Consumer | Message consumers, Metrics | 2121 | Message processing scaling |
| Gateway | REST, Metrics | 8080, 2121 | Api layer scaling |
Mode Details
ModeAio (All-In-One)
Description: Runs all components in a single process.
What Starts:
- HTTP/REST server (port 8080)
- gRPC server (port 50051)
- Temporal workers
- Message consumers
- Metrics endpoint (port 2121)
- OpenAPI documentation UI
Example:
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/service"
"github.com/tonica-go/tonica/pkg/tonica/worker"
"github.com/tonica-go/tonica/pkg/tonica/consumer"
)
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeAIO),
),
),
)
// Register services
svc := service.NewService(...)
app.GetRegistry().MustRegisterService(svc)
// Register workers
w := worker.NewWorker(...)
app.GetRegistry().MustRegisterWorker(w)
// Register consumers
c := consumer.NewConsumer(...)
app.GetRegistry().MustRegisterConsumer(c)
// Run in AIO mode
err := app.Run()
When to Use:
- ✅ Development environment
- ✅ Small applications with low traffic
- ✅ Proof of concepts
- ✅ Single-server deployments
- ✅ When operational simplicity is more important than scaling
When NOT to Use:
- ❌ High-traffic production (CPU/memory contention)
- ❌ Need to scale components independently
- ❌ CPU-intensive workers affecting API latency
- ❌ Large-scale message processing
Resource Requirements:
# Example Kubernetes resources
resources:
requests:
cpu: "1000m" # All components combined
memory: "1Gi"
limits:
cpu: "2000m"
memory: "2Gi"
ModeService
Description: Runs only the API layer (gRPC and REST).
What Starts:
- gRPC server (port 50051)
- Metrics endpoint (port 2121)
What Does NOT Start:
- Temporal workers
- Message consumers
Example:
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/service"
)
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeService),
),
),
)
// Register services ONLY
svc := service.NewService(...)
app.GetRegistry().MustRegisterService(svc)
// DON'T register workers or consumers in this mode
// Run in Service mode
err := app.Run()
When to Use:
- ✅ Production API deployments
- ✅ Need to scale API independently from workers
- ✅ High request throughput
- ✅ Low-latency requirements
- ✅ Horizontal scaling of API layer
Architecture Pattern:
Scaling Example:
# Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: myservice-api
spec:
replicas: 5 # Scale API horizontally
template:
spec:
containers:
- name: api
image: myservice:latest
env:
- name: RUN_MODE
value: "service"
resources:
requests:
cpu: "500m"
memory: "512Mi"
Resource Requirements:
# Lighter than AIO - no workers/consumers
resources:
requests:
cpu: "500m" # Just API handling
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ModeWorker
Description: Runs only Temporal workers for background task processing.
What Starts:
- Temporal workers
- Metrics endpoint (port 2121)
What Does NOT Start:
- HTTP/REST server
- gRPC server
- Message consumers
Example:
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/worker"
"go.temporal.io/sdk/client"
)
// Create Temporal client
temporalClient, err := client.Dial(client.Options{
HostPort: "localhost:7233",
})
if err != nil {
log.Fatal(err)
}
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeWorker),
),
),
)
// Register workers ONLY
w := worker.NewWorker(
worker.WithName("email-worker"),
worker.WithQueue("email-tasks"),
worker.WithClient(temporalClient),
worker.WithActivities([]interface{}{
SendEmailActivity,
GenerateReportActivity,
}),
)
app.GetRegistry().MustRegisterWorker(w)
// Run in Worker mode
err = app.Run()
When to Use:
- ✅ CPU-intensive background tasks
- ✅ Long-running operations (reports, exports)
- ✅ Batch processing
- ✅ Need to scale workers independently
- ✅ Task-specific resource requirements
Architecture Pattern:
Specialized Workers:
// Email worker - high concurrency, low CPU
emailWorker := tonica.NewWorker(
tonica.WithWorkerName("email-worker"),
tonica.WithTaskQueue("emails"),
tonica.WithMaxConcurrentActivities(50), // Many concurrent emails
)
// Report worker - low concurrency, high CPU
reportWorker := tonica.NewWorker(
tonica.WithWorkerName("report-worker"),
tonica.WithTaskQueue("reports"),
tonica.WithMaxConcurrentActivities(2), // CPU-intensive reports
)
Resource Requirements:
# Email worker (I/O bound)
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
---
# Report worker (CPU bound)
resources:
requests:
cpu: "2000m" # Needs more CPU
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
ModeConsumer
Description: Runs only message consumers for Kafka/PubSub processing.
What Starts:
- Message consumers
- Metrics endpoint (port 2121)
What Does NOT Start:
- HTTP/REST server
- gRPC server
- Temporal workers
Example:
import (
"context"
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/consumer"
"github.com/tonica-go/tonica/pkg/tonica/storage/pubsub"
"github.com/tonica-go/tonica/pkg/tonica/storage/pubsub/kafka"
)
// Create Kafka client
kafkaClient := kafka.New(&kafka.Config{
Brokers: []string{"localhost:9092"},
ConsumerGroupID: "order-processors",
}, nil)
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeConsumer),
),
),
)
// Register consumers ONLY
c := consumer.NewConsumer(
consumer.WithName("order-consumer"),
consumer.WithTopic("orders"),
consumer.WithConsumerGroup("order-processors"),
consumer.WithClient(kafkaClient),
consumer.WithHandler(func(ctx context.Context, msg *pubsub.Message) error {
return processOrder(ctx, msg)
}),
)
app.GetRegistry().MustRegisterConsumer(c)
// Run in Consumer mode
err := app.Run()
When to Use:
- ✅ High-volume message processing
- ✅ Event-driven architectures
- ✅ Stream processing
- ✅ Need to scale consumers independently
- ✅ Topic-specific processing logic
Architecture Pattern:
Multiple Consumers:
// Order consumer - high priority
orderConsumer := tonica.NewConsumer(
tonica.WithConsumerName("order-consumer"),
tonica.WithTopic("orders"),
tonica.WithConsumerGroup("order-processors"),
tonica.WithHandler(processOrder),
)
// Analytics consumer - low priority
analyticsConsumer := tonica.NewConsumer(
tonica.WithConsumerName("analytics-consumer"),
tonica.WithTopic("events"),
tonica.WithConsumerGroup("analytics"),
tonica.WithHandler(processAnalytics),
)
app.GetRegistry().MustRegisterConsumer(orderConsumer)
app.GetRegistry().MustRegisterConsumer(analyticsConsumer)
Resource Requirements:
# Consumer resources depend on processing complexity
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ModeGateway
Description: Runs only the HTTP/REST API gateway layer (without gRPC server).
What Starts:
- HTTP/REST server (port 8080)
- gRPC-Gateway (proxies REST requests to backend gRPC services)
- Custom routes
- OpenAPI documentation UI
- Metrics endpoint (port 2121)
What Does NOT Start:
- gRPC server (connects to external gRPC services instead)
- Temporal workers
- Message consumers
Example:
import (
"github.com/gin-gonic/gin"
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
)
app := tonica.NewApp(
tonica.WithSpec("openapi/spec.json"),
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeGateway),
),
),
)
// Don't register services directly - gateway proxies to external gRPC servers
// Just configure custom routes if needed
tonica.NewRoute(app).
GET("/health").
Handle(func(c *gin.Context) {
c.JSON(200, gin.H{"status": "healthy"})
})
// Run in Gateway mode
err := app.Run()
When to Use:
- ✅ Separate REST API layer from gRPC services
- ✅ Independent scaling of API gateway
- ✅ Edge layer for multiple backend services
- ✅ API rate limiting and caching at gateway level
- ✅ Public-facing REST API with private gRPC backends
Architecture Pattern:
Use Case Example:
┌─────────────┐
│ Clients │ (Mobile, Web, etc.)
└──────┬──────┘
│ HTTP/REST
↓
┌─────────────┐
│ Gateways │ ModeGateway (scaled: 10 replicas)
│ Port 8080 │ - Rate limiting
└──────┬──────┘ - Caching
│ gRPC - Auth
↓
┌─────────────┐
│ Services │ ModeService (scaled: 5 replicas)
│ Port 50051 │ - Business logic
└─────────────┘ - Database access
Resource Requirements:
# Gateway is lightweight - just proxying
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Configuration:
Point gateway to backend gRPC services:
# Service addresses
export USERSERVICE_ADDR="user-service:50051"
export ORDERSERVICE_ADDR="order-service:50051"
export PAYMENTSERVICE_ADDR="payment-service:50051"
Choosing the Right Mode
Decision Tree
Deployment Patterns
Pattern 1: Development
# Single container
services:
app:
image: myservice:latest
environment:
RUN_MODE: aio
ports:
- "8080:8080" # HTTP
- "50051:50051" # gRPC
- "9090:9090" # Metrics
Pattern 2: Small Production
# Separate API and workers
services:
gateway:
image: myservice:latest
environment:
RUN_MODE: gateway
replicas: 3
ports:
- "8080:8080"
api:
image: myservice:latest
environment:
RUN_MODE: service
replicas: 3
ports:
- "8080:8080"
- "50051:50051"
worker:
image: myservice:latest
environment:
RUN_MODE: worker
replicas: 2
Pattern 3: Large Production
# Fully separated components
services:
gateway:
image: myservice:latest
environment:
RUN_MODE: gateway
replicas: 10 # Scale API independently
api:
image: myservice:latest
environment:
RUN_MODE: service
replicas: 10 # Scale API independently
email-worker:
image: myservice:latest
environment:
RUN_MODE: worker
WORKER_TASK_QUEUE: emails
replicas: 5
report-worker:
image: myservice:latest
environment:
RUN_MODE: worker
WORKER_TASK_QUEUE: reports
replicas: 2
resources:
cpu: "4000m" # More CPU for reports
order-consumer:
image: myservice:latest
environment:
RUN_MODE: consumer
CONSUMER_TOPIC: orders
replicas: 3
Mode Configuration
Via Environment Variable
# Set run mode via environment
export APP_MODE="service"
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
)
// In your code, read from environment
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(
config.GetEnv("APP_MODE", config.ModeAIO), // Read from env with default
),
),
),
)
err := app.Run()
Via Code
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
)
// Hardcode the mode
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeService),
),
),
)
err := app.Run()
Via Command Line Flag
import (
"flag"
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
)
func main() {
mode := flag.String("mode", config.ModeAIO, "Run mode: aio, service, worker, consumer, gateway")
flag.Parse()
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(*mode),
),
),
)
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
Graceful Shutdown in All Modes
All modes support graceful shutdown (handled internally by the framework):
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
)
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeAIO),
),
),
)
// Graceful shutdown is handled automatically by the framework
// It listens for SIGINT/SIGTERM signals
err := app.Run()
Shutdown Behavior by Mode:
| Mode | Shutdown Process |
|---|---|
| AIO | 1. Stop accepting new HTTP/gRPC requests 2. Wait for in-flight requests (30s) 3. Stop workers 4. Wait for consumers to finish current message (5s) 5. Close connections |
| Service | 1. Stop accepting new requests 2. Wait for in-flight requests (30s) 3. Close connections |
| Worker | 1. Stop accepting new tasks 2. Wait for current tasks to complete 3. Close Temporal connection |
| Consumer | 1. Stop accepting new messages 2. Wait for current message processing (5s) 3. Close message queue connection |
| Gateway | 1. Stop accepting new HTTP requests |
Monitoring Each Mode
Metrics by Mode
All modes expose metrics on port 2121:
curl http://localhost:2121/metrics
Mode-Specific Metrics:
ModeService:
http_requests_totalhttp_request_duration_secondsgrpc_requests_totalgrpc_request_duration_seconds
ModeWorker:
temporal_activity_execution_totaltemporal_activity_duration_secondstemporal_activity_errors_total
ModeConsumer:
consumer_messages_processed_totalconsumer_messages_failed_totalconsumer_processing_duration_seconds
Health Checks
Add mode-specific health checks:
tonica.NewRoute(app).
GET("/health").
Handle(func (c *gin.Context) {
health := gin.H{
"status": "healthy",
"mode": "service", // or worker, consumer
}
// Add mode-specific checks
if mode == "worker" {
health["workers"] = len(app.GetRegistry().GetAllWorkers())
}
c.JSON(200, health)
})
Common Pitfalls
❌ Registering Wrong Components
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/worker"
)
// DON'T: Register workers when running in Service mode
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeService),
),
),
)
w := worker.NewWorker(...)
app.GetRegistry().MustRegisterWorker(w)
err := app.Run() // Worker won't start in Service mode!
✅ Register Appropriate Components
import (
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/service"
)
// DO: Only register services when running in Service mode
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(config.ModeService),
),
),
)
svc := service.NewService(...)
app.GetRegistry().MustRegisterService(svc)
err := app.Run() // Correct!
❌ Port Conflicts
// DON'T: Run multiple Service mode instances on same host
// Both try to bind to port 8080 and 50051
✅ Use Different Hosts or Containers
# DO: Run in separate containers/hosts
services:
api-1:
ports: [ "8080:8080" ]
api-2:
ports: [ "8081:8080" ] # Map to different external port
Advanced: Dynamic Mode Selection
import (
"os"
"github.com/tonica-go/tonica/pkg/tonica"
"github.com/tonica-go/tonica/pkg/tonica/config"
"github.com/tonica-go/tonica/pkg/tonica/service"
"github.com/tonica-go/tonica/pkg/tonica/worker"
"github.com/tonica-go/tonica/pkg/tonica/consumer"
)
func main() {
mode := config.GetEnv("APP_MODE", config.ModeAIO)
app := tonica.NewApp(
tonica.WithConfig(
config.NewConfig(
config.WithRunMode(mode),
),
),
)
// Create components
svc := service.NewService(...)
w := worker.NewWorker(...)
c := consumer.NewConsumer(...)
// Register based on mode
switch mode {
case config.ModeService:
app.GetRegistry().MustRegisterService(svc)
case config.ModeWorker:
app.GetRegistry().MustRegisterWorker(w)
case config.ModeConsumer:
app.GetRegistry().MustRegisterConsumer(c)
case config.ModeGateway:
// No components needed for gateway
default: // config.ModeAIO
app.GetRegistry().MustRegisterService(svc)
app.GetRegistry().MustRegisterWorker(w)
app.GetRegistry().MustRegisterConsumer(c)
}
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
Summary
| Mode | Development | Production | Scaling | Resource Efficiency |
|---|---|---|---|---|
| AIO | ✅ Perfect | ⚠️ Small only | ❌ Limited | ⚠️ Medium |
| Service | ✅ Good | ✅ Excellent | ✅ Horizontal | ✅ High |
| Worker | ✅ Good | ✅ Excellent | ✅ Horizontal + Vertical | ✅ High |
| Consumer | ✅ Good | ✅ Excellent | ✅ Horizontal | ✅ High |
| Gateway | ✅ Good | ✅ Excellent | ✅ Horizontal | ✅ High |
Recommendation: Start with ModeAio for development, then split into ModeService + ModeWorker + * ModeConsumer* for production.
Next Steps
- Configuration - Configure each mode
- Best Practices - Production deployment patterns
- Testing - Test each mode independently