Tonica Architecture Overview
This document explains the internal architecture and design principles of the Tonica framework.
Design Philosophy
Tonica is built on several core principles:
- Proto-First: APIs are defined using Protocol Buffers, ensuring type safety and contract-first design
- Convention over Configuration: Sensible defaults with escape hatches for customization
- Modular: Use only what you need - run as a unified service or separate components
- Observable by Default: Built-in metrics, tracing, and logging
- Production-Ready: Graceful shutdown, health checks, and error handling out of the box
High-Level Architecture
Core Components
1. Application Core (App)
The App struct is the heart of Tonica. It manages the application lifecycle and coordinates all components.
Responsibilities:
- Initialize and configure all subsystems
- Manage component registry
- Coordinate graceful shutdown
- Provide access to shared resources (logger, metrics, etc.)
Key Fields:
type App struct {
Name string
cfg *config.Config
logger *log.Logger // Standard logger
router *gin.Engine // HTTP router
metricRouter *gin.Engine // Metrics endpoint
metricsManager *metrics.Manager
registry *Registry
shutdown *Shutdown
customRoutes []RouteMetadata // Custom route definitions
}
Lifecycle:
NewApp() → Configure() → RegisterComponents() → Run() → Shutdown()
2. Registry
The Registry is a central repository for all application components (services, workers, consumers).
Purpose:
- Store and retrieve services by name
- Store and retrieve workers by name
- Store and retrieve consumers by name
- Prevent duplicate registrations
- Enable discovery of components during startup
Implementation:
type Registry struct {
services map[string]*Service
workers map[string]*Worker
consumers map[string]*Consumer
mu sync.RWMutex // Thread-safe access
}
Thread Safety: All registry operations are protected by a read-write mutex, allowing safe concurrent access.
3. Service Layer
Services handle gRPC and REST API requests.
Service Structure:
type Service struct {
name string
grpcServer *grpc.Server
db *DB
redis *Redis
}
Request Flow:
Features:
- Automatic REST API generation via gRPC-Gateway
- OpenAPI spec generation from proto annotations
- Built-in middleware (CORS, logging, metrics)
- Custom route support
4. Worker Layer
Workers process background tasks using Temporal.
Worker Structure:
type Worker struct {
name string
taskQueue string
client client.Client
worker worker.Worker
}
Execution Flow:
Features:
- Activity registration
- Workflow registration
- Automatic retries and error handling
- Distributed tracing integration
5. Consumer Layer
Consumers process messages from Kafka, PubSub, or other message queues.
Consumer Structure:
type Consumer struct {
name string
topic string
consumerGroup string
client storage.Client
handler func(context.Context, *pubsub.Message) error
}
Processing Flow:
Features:
- Context-based cancellation
- Automatic error handling and logging
- Graceful shutdown (waits for message completion)
- Consumer group support
6. Infrastructure Layer
Database (Bun ORM)
Tonica uses Bun as its ORM, supporting PostgreSQL, MySQL, and SQLite.
Connection Management:
type DB struct {
driver string
dsn string
db *bun.DB // Cached connection
}
func (d *DB) GetClient() *bun.DB {
if d.db != nil {
return d.db // Return cached connection
}
// Create new connection
// ...
}
Features:
- Connection pooling
- Automatic driver detection
- Migration support (via Bun)
- Query builder
Redis
Redis client wrapper with connection caching.
Connection Management:
type Redis struct {
addr string
password string
database int
conn *redis.Client // Cached connection
}
func (r *Redis) GetClient() *redis.Client {
if r.conn == nil {
r.conn = redis.NewClient(&redis.Options{
Addr: r.addr,
Password: r.password,
DB: r.database,
})
}
return r.conn
}
Use Cases:
- Caching
- Session storage
- Rate limiting
- Distributed locking
7. Observability Layer
OpenTelemetry Tracing
Automatic distributed tracing for all requests.
Instrumentation Points:
- HTTP requests (incoming/outgoing)
- gRPC calls (incoming/outgoing)
- Database queries
- Redis operations
- Custom spans (user-defined)
Configuration:
OTEL_ENABLED=true
OTEL_ENDPOINT=localhost:4317
OTEL_SERVICE_NAME=myservice
Prometheus Metrics
Built-in metrics collection and export.
Default Metrics:
app_info- Application metadata- HTTP request metrics (duration, count, status)
- gRPC request metrics
- Go runtime metrics (goroutines, memory, GC)
Custom Metrics:
counter := app.GetMetricManager().NewCounter("orders_total", "Total orders")
gauge := app.GetMetricManager().NewGauge("active_connections", "Active connections")
histogram := app.GetMetricManager().NewHistogram("request_duration", "Request duration")
Metrics Endpoint:
http://localhost:9090/metrics
Structured Logging
Uses Go's standard log/slog for structured logging.
Log Levels:
DEBUG- Detailed debugging informationINFO- General informational messagesWARN- Warning messagesERROR- Error messages
Usage:
import "log/slog"
// Use slog for structured logging
slog.Info("User created", "user_id", userID, "email", email)
slog.Error("Failed to process order", "order_id", orderID, "error", err)
// Or use app's logger (*log.Logger)
app.GetLogger().Printf("User created: %s", userID)
8. Shutdown Coordinator
The Shutdown coordinator ensures graceful termination of all components.
Shutdown Flow:
Registration:
// Register HTTP server
app.shutdown.RegisterHTTPServer(httpServer)
// Register gRPC server
app.shutdown.RegisterGRPCServer(grpcServer)
// Register cleanup function
app.shutdown.RegisterCleanup(func(ctx context.Context) error {
// Close database connections, flush metrics, etc.
return db.Close()
})
Features:
- Parallel shutdown (fast)
- Timeout protection (no hanging)
- Error aggregation
- Thread-safe registration
Run Modes
Tonica supports four run modes, each activating different components:
ModeAio (All-In-One)
Runs everything: gRPC + REST + Workers + Consumers
ModeService
Runs only gRPC and REST APIs
ModeWorker
Runs only Temporal workers (+ metrics)
ModeConsumer
Runs only message consumers (+ metrics)
See Run Modes for detailed comparison.
Request Flow Examples
gRPC Request
REST Request (Generated from Proto)
REST Request (Custom Route)
Worker Task Processing
Consumer Message Processing
Configuration Management
Configuration follows this priority order (highest to lowest):
- Code - Options passed to constructors
- Environment Variables -
APP_NAME,DB_DSN, etc. - Defaults - Built-in sensible defaults
Example:
// 1. Code (highest priority)
app := tonica.NewApp(tonica.WithName("myservice"))
// 2. Environment variable
// export APP_NAME="envservice"
// 3. Default (lowest priority)
// Falls back to "tonica-app" if nothing specified
See Configuration for all options.
Error Handling Strategy
Tonica uses a layered error handling approach:
Layer 1: Application Errors
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user ID")
}
Layer 2: Middleware Errors
Automatically handled by framework:
- HTTP 500 for internal errors
- HTTP 4xx for client errors
- Logged with context
Layer 3: Infrastructure Errors
Database, Redis, etc.:
import "log/slog"
user, err := db.GetUser(ctx, userID)
if err != nil {
slog.Error("Failed to get user", "error", err, "user_id", userID)
return nil, status.Error(codes.Internal, "database error")
}
Layer 4: Panic Recovery
Automatic panic recovery in HTTP/gRPC handlers:
- Logs panic with stack trace
- Returns 500 error to client
- Doesn't crash the application
Concurrency Model
Thread Safety Guarantees
App:
- Safe to call getters concurrently
- Registry access is mutex-protected
- Metrics manager is thread-safe
Registry:
- All operations use RWMutex
- Multiple readers allowed
- Exclusive writer lock
Services/Workers/Consumers:
- Each request/task/message in separate goroutine
- No shared mutable state (unless explicitly designed)
- Context-based cancellation
Goroutine Management
HTTP Server:
- One goroutine per request
- Automatically cleaned up after response
gRPC Server:
- One goroutine per stream
- Cleaned up when stream closes
Workers:
- Configurable number of concurrent activities
- Managed by Temporal worker
Consumers:
- One goroutine per consumer
- Message processing can be sequential or parallel (user choice)
Performance Considerations
Connection Pooling
Database:
// Bun handles connection pooling internally
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
Redis:
// Redis client has built-in connection pool
// Configured via redis.Options
&redis.Options{
PoolSize: 10,
MinIdleConns: 5,
}
Caching Strategy
- Application-level - In-memory caches in your code
- Redis - Distributed cache for multi-instance deployments
- Database - Query result caching via Bun
Metrics Overhead
Metrics collection is lightweight:
- Counter increment: ~100ns
- Histogram observation: ~500ns
- Negligible impact on performance
Security Considerations
Authentication & Authorization
Not built-in - implement as middleware:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if !validateToken(token) {
c.AbortWithStatus(401)
return
}
c.Next()
}
}
app.GetRouter().Use(AuthMiddleware())
CORS
Built-in CORS support:
# Allow all origins (default)
# No configuration needed
# Restrict origins
export APP_CORS_ORIGINS="https://myapp.com,https://api.myapp.com"
TLS/HTTPS
Configure at deployment level:
- Use reverse proxy (nginx, Traefik) with TLS termination
- Or configure TLS in Gin router directly
Extending Tonica
Custom Middleware
HTTP Middleware:
app.GetRouter().Use(MyCustomMiddleware())
gRPC Interceptors:
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(MyInterceptor),
)
Custom Metrics
myCounter := app.GetMetricManager().NewCounter("my_metric", "Description")
myCounter.Inc()
Custom Health Checks
tonica.NewRoute(app).
GET("/health/detailed").
Handle(func(c *gin.Context) {
health := map[string]string{
"database": checkDB(),
"redis": checkRedis(),
"workers": checkWorkers(),
}
c.JSON(200, health)
})
Next Steps
- Run Modes - Learn when to use each mode
- Configuration - Configure your application
- Testing - Write tests for your services
- Best Practices - Production patterns