83 KiB
Golang Backend Blueprint: Clean Architecture with Wire DI & Cassandra
Version: 1.0 Last Updated: November 2025 Based on: maplepress-backend architecture
This document provides a step-by-step guide to building a new Golang backend using the same architecture, authentication system, and reusable components from the MaplePress backend. Use this as a reference when creating new backend projects from scratch.
Table of Contents
- Architecture Overview
- Project Initialization
- Directory Structure
- Core Dependencies
- Configuration System
- Dependency Injection with Wire
- Reusable pkg/ Components
- Authentication System
- Clean Architecture Layers
- Database Setup (Cassandra)
- Middleware Implementation
- HTTP Server Setup
- Docker & Infrastructure
- CLI Commands (Cobra)
- Development Workflow
- Testing Strategy
- Production Deployment
1. Architecture Overview
Core Principles
Our backend architecture follows these fundamental principles:
- Clean Architecture - Separation of concerns with clear dependency direction
- Dependency Injection - Using Google Wire for compile-time DI
- Multi-tenancy - Tenant isolation at the application layer
- Security-first - CWE-compliant security measures throughout
- Code Reuse - Extensive use of shared
pkg/components
Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ Interface Layer (HTTP) │
│ • Handlers (HTTP endpoints) │
│ • DTOs (Data Transfer Objects) │
│ • Middleware (JWT, API Key, Rate Limiting, CORS) │
└──────────────────────┬──────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ • Orchestration logic │
│ • Transaction management (SAGA pattern) │
│ • Cross-use-case coordination │
└──────────────────────┬──────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Use Case Layer │
│ • Focused, single-responsibility operations │
│ • Business logic encapsulation │
│ • Validation and domain rules │
└──────────────────────┬──────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Repository Layer │
│ • Data access implementations │
│ • Database queries (Cassandra CQL) │
│ • Cache operations (Redis) │
└──────────────────────┬──────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ • Entities (domain models) │
│ • Repository interfaces │
│ • Domain errors │
└─────────────────────────────────────────────────────────────┘
Shared Infrastructure (pkg/)
┌─────────────────────────────────────────────────────────────┐
│ • Logger (Zap) • Security (JWT, Password, API Key)│
│ • Database (Cassandra) • Cache (Redis, Two-Tier) │
│ • Storage (S3) • Rate Limiting │
│ • Email (Mailgun) • Search (Meilisearch) │
│ • Distributed Mutex • Validation │
└─────────────────────────────────────────────────────────────┘
Dependency Direction
Critical Rule: Dependencies flow INWARD only (outer layers depend on inner layers).
Interface → Service → Use Case → Repository → Domain
↓ ↓ ↓ ↓
└───────────┴─────────┴───────────┴──→ pkg/ (Infrastructure)
2. Project Initialization
Step 1: Create Project Structure
# Create new project directory
mkdir -p cloud/your-backend-name
cd cloud/your-backend-name
# Initialize Go module
go mod init codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
# Create initial directory structure
mkdir -p {app,cmd,config,internal,pkg,migrations,static,docs}
Step 2: Initialize Go Workspace (if using monorepo)
# From monorepo root
cd /path/to/monorepo
go work use ./cloud/your-backend-name
Step 3: Add Core Dependencies
# Core dependencies
go get github.com/google/wire@v0.7.0
go get github.com/spf13/cobra@v1.10.1
go get go.uber.org/zap@v1.27.0
# Database & Cache
go get github.com/gocql/gocql@v1.7.0
go get github.com/redis/go-redis/v9@v9.16.0
go get github.com/golang-migrate/migrate/v4@v4.19.0
# Security
go get github.com/golang-jwt/jwt/v5@v5.3.0
go get golang.org/x/crypto@v0.41.0
go get github.com/awnumar/memguard@v0.23.0
# HTTP & Utilities
go get github.com/google/uuid@v1.6.0
# Install Wire CLI
go install github.com/google/wire/cmd/wire@latest
3. Directory Structure
Complete Directory Layout
your-backend-name/
├── app/ # Dependency injection (Wire)
│ ├── wire.go # Wire dependency providers
│ ├── wire_gen.go # Generated by Wire (gitignore)
│ └── app.go # Application bootstrapper
│
├── cmd/ # CLI commands (Cobra)
│ ├── root.go # Root command
│ ├── daemon/
│ │ └── daemon.go # Main server command
│ ├── migrate/
│ │ └── migrate.go # Database migrations
│ └── version/
│ └── version.go # Version command
│
├── config/ # Configuration
│ ├── config.go # Config loader
│ └── constants/
│ ├── constants.go # Global constants
│ └── session.go # Session context keys
│
├── internal/ # Private application code
│ ├── domain/ # Domain entities & interfaces
│ │ ├── user/
│ │ │ ├── entity.go # User domain entity
│ │ │ └── repository.go # User repository interface
│ │ └── tenant/
│ │ ├── entity.go # Tenant domain entity
│ │ └── repository.go # Tenant repository interface
│ │
│ ├── repository/ # Repository implementations
│ │ ├── user/
│ │ │ ├── impl.go # Repository struct
│ │ │ ├── create.go # Create operations
│ │ │ ├── get.go # Read operations
│ │ │ ├── update.go # Update operations
│ │ │ ├── delete.go # Delete operations
│ │ │ └── models/
│ │ │ └── user.go # Database models
│ │ └── tenant/
│ │ └── ... # Similar structure
│ │
│ ├── usecase/ # Use cases (focused operations)
│ │ ├── user/
│ │ │ ├── create_user_entity.go
│ │ │ ├── save_user_to_repo.go
│ │ │ ├── validate_user_email_unique.go
│ │ │ └── get.go
│ │ └── gateway/
│ │ ├── login.go
│ │ ├── hash_password.go
│ │ └── verify_password.go
│ │
│ ├── service/ # Service layer (orchestration)
│ │ ├── provider.go # Service providers for Wire
│ │ ├── session.go # Session management service
│ │ ├── gateway/
│ │ │ ├── login.go # Login orchestration
│ │ │ ├── register.go # Registration orchestration
│ │ │ └── provider.go # Gateway service providers
│ │ └── user/
│ │ ├── create.go # User creation orchestration
│ │ └── provider.go # User service providers
│ │
│ ├── interface/http/ # HTTP interface layer
│ │ ├── server.go # HTTP server & routing
│ │ ├── handler/ # HTTP handlers
│ │ │ ├── healthcheck/
│ │ │ │ └── healthcheck_handler.go
│ │ │ ├── gateway/
│ │ │ │ ├── login_handler.go
│ │ │ │ ├── register_handler.go
│ │ │ │ └── refresh_handler.go
│ │ │ └── user/
│ │ │ ├── create_handler.go
│ │ │ └── get_handler.go
│ │ ├── dto/ # Data Transfer Objects
│ │ │ ├── gateway/
│ │ │ │ ├── login_dto.go
│ │ │ │ └── register_dto.go
│ │ │ └── user/
│ │ │ ├── create_dto.go
│ │ │ └── get_dto.go
│ │ └── middleware/
│ │ └── tenant.go # Tenant context middleware
│ │
│ ├── http/middleware/ # Shared HTTP middleware
│ │ ├── jwt.go # JWT authentication
│ │ ├── apikey.go # API key authentication
│ │ ├── ratelimit.go # Rate limiting
│ │ ├── security_headers.go # Security headers (CORS, CSP)
│ │ ├── request_size_limit.go # Request size limits
│ │ └── provider.go # Middleware providers
│ │
│ └── scheduler/ # Background schedulers (cron)
│ ├── quota_reset.go # Monthly quota reset
│ └── ip_cleanup.go # GDPR IP cleanup
│
├── pkg/ # Reusable packages (copy from maplepress-backend)
│ ├── logger/ # Structured logging (Zap)
│ │ ├── logger.go
│ │ └── sanitizer.go # PII sanitization
│ ├── security/ # Security utilities
│ │ ├── jwt/ # JWT provider
│ │ ├── password/ # Password hashing & validation
│ │ ├── apikey/ # API key generation & hashing
│ │ ├── clientip/ # IP extraction & validation
│ │ ├── ipcrypt/ # IP encryption (GDPR)
│ │ └── validator/ # Credential validation
│ ├── storage/
│ │ ├── database/ # Cassandra client
│ │ │ ├── cassandra.go
│ │ │ └── migrator.go
│ │ ├── cache/ # Redis client
│ │ │ └── redis.go
│ │ └── object/ # S3-compatible storage
│ │ └── s3.go
│ ├── cache/ # Two-tier caching
│ │ ├── redis.go
│ │ ├── cassandra.go
│ │ └── twotier.go
│ ├── ratelimit/ # Rate limiting
│ │ ├── ratelimiter.go
│ │ └── login_ratelimiter.go
│ ├── distributedmutex/ # Distributed locking
│ │ └── distributedmutex.go
│ ├── validation/ # Input validation
│ │ └── validator.go
│ ├── httperror/ # HTTP error handling
│ │ └── error.go
│ ├── httpresponse/ # HTTP response helpers
│ │ └── response.go
│ └── transaction/ # SAGA pattern
│ └── saga.go
│
├── migrations/ # Cassandra migrations (CQL)
│ ├── 000001_create_users.up.cql
│ ├── 000001_create_users.down.cql
│ ├── 000002_create_tenants.up.cql
│ └── 000002_create_tenants.down.cql
│
├── static/ # Static files (if needed)
├── docs/ # Documentation
├── .env.sample # Sample environment variables
├── .env # Local environment (gitignored)
├── .gitignore # Git ignore rules
├── docker-compose.dev.yml # Development docker compose
├── Dockerfile # Production dockerfile
├── dev.Dockerfile # Development dockerfile
├── Taskfile.yml # Task runner configuration
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── main.go # Application entry point
└── README.md # Project documentation
4. Core Dependencies
Essential go.mod Dependencies
module codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
go 1.24.4
require (
// Dependency Injection
github.com/google/wire v0.7.0
// CLI Framework
github.com/spf13/cobra v1.10.1
// Logging
go.uber.org/zap v1.27.0
// Database & Cache
github.com/gocql/gocql v1.7.0
github.com/redis/go-redis/v9 v9.16.0
github.com/golang-migrate/migrate/v4 v4.19.0
// Security
github.com/golang-jwt/jwt/v5 v5.3.0
golang.org/x/crypto v0.41.0
github.com/awnumar/memguard v0.23.0 // Secure memory handling
// HTTP & Utilities
github.com/google/uuid v1.6.0
// Distributed Locking
github.com/bsm/redislock v0.9.4
// Background Jobs (optional)
github.com/robfig/cron/v3 v3.0.1
// Optional: Email (Mailgun)
github.com/mailgun/mailgun-go/v4 v4.23.0
// Optional: Search (Meilisearch)
github.com/meilisearch/meilisearch-go v0.34.1
// Optional: GeoIP (for IP country blocking)
github.com/oschwald/geoip2-golang v1.13.0
// Optional: AWS S3
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/config v1.29.14
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0
)
5. Configuration System
config/config.go
Create a centralized configuration system that loads from environment variables:
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
// Config holds all application configuration
type Config struct {
App AppConfig
Server ServerConfig
HTTP HTTPConfig
Security SecurityConfig
Database DatabaseConfig
Cache CacheConfig
Logger LoggerConfig
// Add more config sections as needed
}
// AppConfig holds application-level configuration
type AppConfig struct {
Environment string // development, staging, production
Version string
JWTSecret string
}
// ServerConfig holds HTTP server configuration
type ServerConfig struct {
Host string
Port int
}
// HTTPConfig holds HTTP request handling configuration
type HTTPConfig struct {
MaxRequestBodySize int64 // Maximum request body size in bytes
ReadTimeout time.Duration // Maximum duration for reading the entire request
WriteTimeout time.Duration // Maximum duration before timing out writes
IdleTimeout time.Duration // Maximum amount of time to wait for the next request
}
// SecurityConfig holds security-related configuration
type SecurityConfig struct {
TrustedProxies []string // CIDR blocks of trusted reverse proxies
IPEncryptionKey string // 32-character hex key for IP encryption (GDPR)
AllowedOrigins []string // CORS allowed origins
}
// DatabaseConfig holds Cassandra database configuration
type DatabaseConfig struct {
Hosts []string
Keyspace string
Consistency string
Replication int
MigrationsPath string
}
// CacheConfig holds Redis cache configuration
type CacheConfig struct {
Host string
Port int
Password string
DB int
}
// LoggerConfig holds logging configuration
type LoggerConfig struct {
Level string // debug, info, warn, error
Format string // json, console
}
// Load loads configuration from environment variables
func Load() (*Config, error) {
cfg := &Config{
App: AppConfig{
Environment: getEnv("APP_ENVIRONMENT", "development"),
Version: getEnv("APP_VERSION", "0.1.0"),
JWTSecret: getEnv("APP_JWT_SECRET", "change-me-in-production"),
},
Server: ServerConfig{
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvAsInt("SERVER_PORT", 8000),
},
HTTP: HTTPConfig{
MaxRequestBodySize: getEnvAsInt64("HTTP_MAX_REQUEST_BODY_SIZE", 10*1024*1024), // 10 MB
ReadTimeout: getEnvAsDuration("HTTP_READ_TIMEOUT", 30*time.Second),
WriteTimeout: getEnvAsDuration("HTTP_WRITE_TIMEOUT", 30*time.Second),
IdleTimeout: getEnvAsDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
},
Security: SecurityConfig{
TrustedProxies: getEnvAsSlice("SECURITY_TRUSTED_PROXIES", []string{}),
IPEncryptionKey: getEnv("SECURITY_IP_ENCRYPTION_KEY", "00112233445566778899aabbccddeeff"),
AllowedOrigins: getEnvAsSlice("SECURITY_CORS_ALLOWED_ORIGINS", []string{}),
},
Database: DatabaseConfig{
Hosts: getEnvAsSlice("DATABASE_HOSTS", []string{"localhost"}),
Keyspace: getEnv("DATABASE_KEYSPACE", "your_keyspace"),
Consistency: getEnv("DATABASE_CONSISTENCY", "QUORUM"),
Replication: getEnvAsInt("DATABASE_REPLICATION", 3),
MigrationsPath: getEnv("DATABASE_MIGRATIONS_PATH", "file://migrations"),
},
Cache: CacheConfig{
Host: getEnv("CACHE_HOST", "localhost"),
Port: getEnvAsInt("CACHE_PORT", 6379),
Password: getEnv("CACHE_PASSWORD", ""),
DB: getEnvAsInt("CACHE_DB", 0),
},
Logger: LoggerConfig{
Level: getEnv("LOGGER_LEVEL", "info"),
Format: getEnv("LOGGER_FORMAT", "json"),
},
}
// Validate configuration
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
return cfg, nil
}
// validate checks if the configuration is valid
func (c *Config) validate() error {
if c.Server.Port < 1 || c.Server.Port > 65535 {
return fmt.Errorf("invalid server port: %d", c.Server.Port)
}
if c.Database.Keyspace == "" {
return fmt.Errorf("database keyspace is required")
}
if len(c.Database.Hosts) == 0 {
return fmt.Errorf("at least one database host is required")
}
if c.App.JWTSecret == "" {
return fmt.Errorf("APP_JWT_SECRET is required")
}
// Production security checks
if c.App.Environment == "production" {
if strings.Contains(strings.ToLower(c.App.JWTSecret), "change-me") {
return fmt.Errorf("SECURITY ERROR: JWT secret is using default value in production")
}
if len(c.App.JWTSecret) < 32 {
return fmt.Errorf("SECURITY ERROR: JWT secret is too short for production (minimum 32 characters)")
}
}
return nil
}
// Helper functions
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
func getEnvAsInt(key string, defaultValue int) int {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultValue
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return defaultValue
}
return value
}
func getEnvAsInt64(key string, defaultValue int64) int64 {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultValue
}
value, err := strconv.ParseInt(valueStr, 10, 64)
if err != nil {
return defaultValue
}
return value
}
func getEnvAsSlice(key string, defaultValue []string) []string {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultValue
}
// Simple comma-separated parsing
var result []string
for _, item := range strings.Split(valueStr, ",") {
trimmed := strings.TrimSpace(item)
if trimmed != "" {
result = append(result, trimmed)
}
}
if len(result) == 0 {
return defaultValue
}
return result
}
func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultValue
}
value, err := time.ParseDuration(valueStr)
if err != nil {
return defaultValue
}
return value
}
config/constants/constants.go
Define global constants:
package constants
// Context keys for request context
type ContextKey string
const (
// Session context keys
SessionIsAuthorized ContextKey = "session_is_authorized"
SessionID ContextKey = "session_id"
SessionUserID ContextKey = "session_user_id"
SessionUserUUID ContextKey = "session_user_uuid"
SessionUserEmail ContextKey = "session_user_email"
SessionUserName ContextKey = "session_user_name"
SessionUserRole ContextKey = "session_user_role"
SessionTenantID ContextKey = "session_tenant_id"
// API Key context keys
APIKeyIsAuthorized ContextKey = "apikey_is_authorized"
APIKeySiteID ContextKey = "apikey_site_id"
APIKeyTenantID ContextKey = "apikey_tenant_id"
)
// User roles
const (
RoleAdmin = "admin"
RoleUser = "user"
RoleGuest = "guest"
)
// Tenant status
const (
TenantStatusActive = "active"
TenantStatusInactive = "inactive"
TenantStatusSuspended = "suspended"
)
.env.sample
Create a sample environment file:
# Application Configuration
APP_ENVIRONMENT=development
APP_VERSION=0.1.0
APP_JWT_SECRET=change-me-in-production-use-openssl-rand-base64-64
# HTTP Server Configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8000
# HTTP Timeouts & Limits
HTTP_MAX_REQUEST_BODY_SIZE=10485760 # 10 MB
HTTP_READ_TIMEOUT=30s
HTTP_WRITE_TIMEOUT=30s
HTTP_IDLE_TIMEOUT=60s
# Security Configuration
# Trusted proxies for X-Forwarded-For validation (comma-separated CIDR)
SECURITY_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# IP encryption key for GDPR compliance (32 hex characters)
SECURITY_IP_ENCRYPTION_KEY=00112233445566778899aabbccddeeff
# CORS allowed origins (comma-separated)
SECURITY_CORS_ALLOWED_ORIGINS=https://yourdomain.com
# Cassandra Database Configuration
DATABASE_HOSTS=localhost:9042
DATABASE_KEYSPACE=your_keyspace
DATABASE_CONSISTENCY=QUORUM
DATABASE_REPLICATION=3
DATABASE_MIGRATIONS_PATH=file://migrations
# Redis Cache Configuration
CACHE_HOST=localhost
CACHE_PORT=6379
CACHE_PASSWORD=
CACHE_DB=0
# Logger Configuration
LOGGER_LEVEL=info
LOGGER_FORMAT=json
6. Dependency Injection with Wire
Overview
We use Google Wire for compile-time dependency injection. Wire generates code at build time, eliminating runtime reflection and providing type safety.
app/wire.go
This file defines all providers for Wire:
//go:build wireinject
// +build wireinject
package app
import (
"github.com/google/wire"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/http/middleware"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/healthcheck"
userrepo "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/repository/user"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service"
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/gateway"
gatewayuc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/gateway"
userusecase "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/user"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/password"
rediscache "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/storage/cache"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/storage/database"
)
// InitializeApplication wires up all dependencies
func InitializeApplication(cfg *config.Config) (*Application, error) {
wire.Build(
// Infrastructure layer (pkg/)
logger.ProvideLogger,
database.ProvideCassandraSession,
// Cache layer
rediscache.ProvideRedisClient,
cache.ProvideRedisCache,
cache.ProvideCassandraCache,
cache.ProvideTwoTierCache,
// Security layer
security.ProvideJWTProvider,
password.NewPasswordProvider,
password.NewPasswordValidator,
password.NewBreachChecker,
security.ProvideClientIPExtractor,
// Repository layer
userrepo.ProvideRepository,
// Use case layer
userusecase.ProvideCreateUserEntityUseCase,
userusecase.ProvideSaveUserToRepoUseCase,
userusecase.ProvideGetUserUseCase,
gatewayuc.ProvideLoginUseCase,
gatewayuc.ProvideHashPasswordUseCase,
gatewayuc.ProvideVerifyPasswordUseCase,
// Service layer
service.ProvideSessionService,
gatewaysvc.ProvideLoginService,
// Middleware layer
middleware.ProvideJWTMiddleware,
middleware.ProvideSecurityHeadersMiddleware,
middleware.ProvideRequestSizeLimitMiddleware,
// Handler layer
healthcheck.ProvideHealthCheckHandler,
gateway.ProvideLoginHandler,
// HTTP server
http.ProvideServer,
// Application
ProvideApplication,
)
return nil, nil
}
app/app.go
Application bootstrapper:
package app
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http"
)
// Application represents the main application
type Application struct {
config *config.Config
httpServer *http.Server
logger *zap.Logger
}
// ProvideApplication creates the application instance
func ProvideApplication(
cfg *config.Config,
httpServer *http.Server,
logger *zap.Logger,
) *Application {
return &Application{
config: cfg,
httpServer: httpServer,
logger: logger,
}
}
// Start starts the application
func (app *Application) Start() error {
app.logger.Info("Starting application",
zap.String("environment", app.config.App.Environment),
zap.String("version", app.config.App.Version))
// Start HTTP server in goroutine
go func() {
if err := app.httpServer.Start(); err != nil {
app.logger.Fatal("HTTP server failed", zap.Error(err))
}
}()
// Wait for interrupt signal
app.waitForShutdown()
return nil
}
// waitForShutdown waits for interrupt signal and performs graceful shutdown
func (app *Application) waitForShutdown() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
app.logger.Info("Shutting down application...")
// Create context with timeout for graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown HTTP server
if err := app.httpServer.Shutdown(ctx); err != nil {
app.logger.Error("HTTP server shutdown failed", zap.Error(err))
}
app.logger.Info("Application shutdown complete")
}
Generate Wire Code
# Navigate to app directory
cd app
# Run wire to generate wire_gen.go
wire
# You should see: wire_gen.go created
Add to .gitignore
app/wire_gen.go
7. Reusable pkg/ Components
The pkg/ directory contains infrastructure components that can be copied directly from maplepress-backend and reused across projects.
Components to Copy
7.1 Logger (pkg/logger/)
Copy from: cloud/maplepress-backend/pkg/logger/
Purpose: Structured logging with Zap, including PII sanitization
Key files:
logger.go- Logger provider and configurationsanitizer.go- Email and sensitive data sanitization
Usage:
logger := logger.ProvideLogger(cfg)
logger.Info("User logged in", zap.String("user_id", userID))
logger.Error("Operation failed", zap.Error(err))
7.2 Security (pkg/security/)
Copy from: cloud/maplepress-backend/pkg/security/
Purpose: Comprehensive security utilities
Subpackages:
JWT (pkg/security/jwt/)
// Generate JWT tokens
jwtProvider := security.ProvideJWTProvider(cfg)
accessToken, accessExpiry, err := jwtProvider.GenerateToken(sessionID, 15*time.Minute)
// Validate JWT
sessionID, err := jwtProvider.ValidateToken(tokenString)
Password (pkg/security/password/)
// Hash password
passwordProvider := password.NewPasswordProvider()
hashedPassword, err := passwordProvider.HashPassword("user-password")
// Verify password
isValid, err := passwordProvider.VerifyPassword("user-password", hashedPassword)
// Check for breached passwords (CWE-521)
breachChecker := password.NewBreachChecker()
isBreached, err := breachChecker.CheckPassword("user-password")
API Key (pkg/security/apikey/)
// Generate API key
generator := apikey.ProvideGenerator()
apiKey := generator.Generate() // Returns: "mp_live_abc123..."
// Hash API key for storage
hasher := apikey.ProvideHasher()
hashedKey, err := hasher.Hash(apiKey)
// Verify API key
isValid, err := hasher.Verify(apiKey, hashedKey)
Client IP (pkg/security/clientip/)
// Extract client IP with X-Forwarded-For validation (CWE-348)
extractor := security.ProvideClientIPExtractor(cfg)
clientIP, err := extractor.ExtractIP(r)
IP Encryption (pkg/security/ipcrypt/)
// Encrypt IP for GDPR compliance (CWE-359)
encryptor := ipcrypt.ProvideIPEncryptor(cfg)
encryptedIP, err := encryptor.Encrypt("192.168.1.1")
decryptedIP, err := encryptor.Decrypt(encryptedIP)
7.3 Database (pkg/storage/database/)
Copy from: cloud/maplepress-backend/pkg/storage/database/
Purpose: Cassandra connection and migration management
// Connect to Cassandra
session, err := database.ProvideCassandraSession(cfg, logger)
// Run migrations
migrator := database.NewMigrator(cfg, logger)
err := migrator.Up()
7.4 Cache (pkg/cache/ and pkg/storage/cache/)
Copy from:
cloud/maplepress-backend/pkg/cache/cloud/maplepress-backend/pkg/storage/cache/
Purpose: Redis client and two-tier caching (Redis + Cassandra)
// Two-tier cache (fast Redis + persistent Cassandra)
cache := cache.ProvideTwoTierCache(redisCache, cassandraCache, logger)
// Set with TTL
err := cache.Set(ctx, "key", value, 1*time.Hour)
// Get
value, err := cache.Get(ctx, "key")
// Delete
err := cache.Delete(ctx, "key")
7.5 Rate Limiting (pkg/ratelimit/)
Copy from: cloud/maplepress-backend/pkg/ratelimit/
Purpose: Redis-based rate limiting with configurable limits
// Create rate limiter
limiter := ratelimit.NewRateLimiter(redisClient, logger)
// Check rate limit
allowed, err := limiter.Allow(ctx, "user:123", 100, time.Hour)
if !allowed {
// Rate limit exceeded
}
7.6 Distributed Mutex (pkg/distributedmutex/)
Copy from: cloud/maplepress-backend/pkg/distributedmutex/
Purpose: Redis-based distributed locking to prevent race conditions
// Acquire lock
mutex := distributedmutex.ProvideDistributedMutexAdapter(redisClient, logger)
lock, err := mutex.Lock(ctx, "resource:123", 30*time.Second)
if err != nil {
// Failed to acquire lock
}
defer lock.Unlock(ctx)
// Critical section protected by lock
// ...
7.7 Validation (pkg/validation/)
Copy from: cloud/maplepress-backend/pkg/validation/
Purpose: Input validation utilities
validator := validation.NewValidator()
// Validate email
if !validator.IsValidEmail("user@example.com") {
// Invalid email
}
// Validate UUID
if !validator.IsValidUUID("123e4567-e89b-12d3-a456-426614174000") {
// Invalid UUID
}
7.8 HTTP Utilities (pkg/httperror/ and pkg/httpresponse/)
Copy from:
cloud/maplepress-backend/pkg/httperror/cloud/maplepress-backend/pkg/httpresponse/
Purpose: Consistent HTTP error and response handling
// Send error response
httperror.SendError(w, http.StatusBadRequest, "Invalid input", err)
// Send JSON response
httpresponse.SendJSON(w, http.StatusOK, map[string]interface{}{
"message": "Success",
"data": data,
})
7.9 Transaction SAGA (pkg/transaction/)
Copy from: cloud/maplepress-backend/pkg/transaction/
Purpose: SAGA pattern for distributed transactions
// Create SAGA
saga := transaction.NewSaga(logger)
// Add compensation steps
saga.AddStep(
func(ctx context.Context) error {
// Forward operation
return createUser(ctx, user)
},
func(ctx context.Context) error {
// Compensation (rollback)
return deleteUser(ctx, user.ID)
},
)
// Execute SAGA
err := saga.Execute(ctx)
if err != nil {
// SAGA failed and all compensations were executed
}
Copy Script
Create a script to copy all reusable components:
#!/bin/bash
# copy-pkg.sh - Copy reusable pkg/ components from maplepress-backend
SOURCE_DIR="../maplepress-backend/pkg"
DEST_DIR="./pkg"
# Create destination directory
mkdir -p "$DEST_DIR"
# Copy all pkg components
cp -r "$SOURCE_DIR/logger" "$DEST_DIR/"
cp -r "$SOURCE_DIR/security" "$DEST_DIR/"
cp -r "$SOURCE_DIR/storage" "$DEST_DIR/"
cp -r "$SOURCE_DIR/cache" "$DEST_DIR/"
cp -r "$SOURCE_DIR/ratelimit" "$DEST_DIR/"
cp -r "$SOURCE_DIR/distributedmutex" "$DEST_DIR/"
cp -r "$SOURCE_DIR/validation" "$DEST_DIR/"
cp -r "$SOURCE_DIR/httperror" "$DEST_DIR/"
cp -r "$SOURCE_DIR/httpresponse" "$DEST_DIR/"
cp -r "$SOURCE_DIR/httpvalidation" "$DEST_DIR/"
cp -r "$SOURCE_DIR/transaction" "$DEST_DIR/"
# Optional components (copy if needed)
# cp -r "$SOURCE_DIR/emailer" "$DEST_DIR/"
# cp -r "$SOURCE_DIR/search" "$DEST_DIR/"
# cp -r "$SOURCE_DIR/dns" "$DEST_DIR/"
echo "✅ All pkg/ components copied successfully"
# Update import paths
echo "⚠️ Remember to update import paths in copied files from:"
echo " codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend"
echo " to:"
echo " codeberg.org/mapleopentech/monorepo/cloud/your-backend-name"
Run the script:
chmod +x copy-pkg.sh
./copy-pkg.sh
Update Import Paths
After copying, you'll need to update import paths in all copied files:
# Find and replace import paths
find ./pkg -type f -name "*.go" -exec sed -i '' \
's|codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend|codeberg.org/mapleopentech/monorepo/cloud/your-backend-name|g' \
{} +
8. Authentication System
Our authentication system uses JWT tokens with session management stored in a two-tier cache (Redis + Cassandra).
8.1 Authentication Flow
┌──────────┐
│ Client │
└─────┬────┘
│ 1. POST /api/v1/login
│ {email, password}
↓
┌─────────────────────────┐
│ Login Handler │
└────────┬────────────────┘
│ 2. Validate input
↓
┌─────────────────────────┐
│ Login Service │
└────────┬────────────────┘
│ 3. Verify credentials
│ 4. Create session
│ 5. Generate JWT tokens
↓
┌─────────────────────────┐
│ Session Service │
│ (Two-Tier Cache) │
└────────┬────────────────┘
│ 6. Store in Redis (fast)
│ 7. Store in Cassandra (persistent)
↓
┌─────────────────────────┐
│ Response │
│ - access_token │
│ - refresh_token │
│ - user_info │
└─────────────────────────┘
8.2 Session Service Implementation
internal/service/session.go:
package service
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
)
const (
SessionTTL = 24 * time.Hour // Session expires after 24 hours
)
// SessionService handles session management
type SessionService interface {
CreateSession(ctx context.Context, userID uint64, userUUID uuid.UUID, email, name, role string, tenantID uuid.UUID) (*domain.Session, error)
GetSession(ctx context.Context, sessionID string) (*domain.Session, error)
DeleteSession(ctx context.Context, sessionID string) error
InvalidateUserSessions(ctx context.Context, userUUID uuid.UUID) error
}
type sessionService struct {
cache cache.TwoTierCacher
logger *zap.Logger
}
// ProvideSessionService provides a session service instance
func ProvideSessionService(cache cache.TwoTierCacher, logger *zap.Logger) SessionService {
return &sessionService{
cache: cache,
logger: logger.Named("session-service"),
}
}
// CreateSession creates a new session and stores it in cache
func (s *sessionService) CreateSession(
ctx context.Context,
userID uint64,
userUUID uuid.UUID,
email, name, role string,
tenantID uuid.UUID,
) (*domain.Session, error) {
// Generate unique session ID
sessionID := uuid.New().String()
// Create session object
session := &domain.Session{
ID: sessionID,
UserID: userID,
UserUUID: userUUID,
UserEmail: email,
UserName: name,
UserRole: role,
TenantID: tenantID,
CreatedAt: time.Now().UTC(),
ExpiresAt: time.Now().UTC().Add(SessionTTL),
}
// Serialize to JSON
sessionJSON, err := json.Marshal(session)
if err != nil {
return nil, fmt.Errorf("failed to marshal session: %w", err)
}
// Store in two-tier cache (Redis + Cassandra)
cacheKey := fmt.Sprintf("session:%s", sessionID)
if err := s.cache.Set(ctx, cacheKey, sessionJSON, SessionTTL); err != nil {
return nil, fmt.Errorf("failed to store session: %w", err)
}
s.logger.Info("Session created",
zap.String("session_id", sessionID),
zap.String("user_uuid", userUUID.String()))
return session, nil
}
// GetSession retrieves a session from cache
func (s *sessionService) GetSession(ctx context.Context, sessionID string) (*domain.Session, error) {
cacheKey := fmt.Sprintf("session:%s", sessionID)
// Get from two-tier cache (tries Redis first, falls back to Cassandra)
sessionJSON, err := s.cache.Get(ctx, cacheKey)
if err != nil {
return nil, fmt.Errorf("session not found: %w", err)
}
// Deserialize from JSON
var session domain.Session
if err := json.Unmarshal(sessionJSON, &session); err != nil {
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
}
// Check if session is expired
if time.Now().UTC().After(session.ExpiresAt) {
// Delete expired session
_ = s.DeleteSession(ctx, sessionID)
return nil, fmt.Errorf("session expired")
}
return &session, nil
}
// DeleteSession removes a session from cache
func (s *sessionService) DeleteSession(ctx context.Context, sessionID string) error {
cacheKey := fmt.Sprintf("session:%s", sessionID)
return s.cache.Delete(ctx, cacheKey)
}
// InvalidateUserSessions invalidates all sessions for a user (CWE-384: Session Fixation Prevention)
func (s *sessionService) InvalidateUserSessions(ctx context.Context, userUUID uuid.UUID) error {
// Note: This is a simplified implementation
// In production, you should maintain a user->sessions mapping in cache
// For now, sessions will naturally expire after SessionTTL
s.logger.Info("Invalidating user sessions",
zap.String("user_uuid", userUUID.String()))
return nil
}
8.3 Domain Session Entity
internal/domain/session.go:
package domain
import (
"time"
"github.com/google/uuid"
)
// Session represents a user session
type Session struct {
ID string `json:"id"`
UserID uint64 `json:"user_id"`
UserUUID uuid.UUID `json:"user_uuid"`
UserEmail string `json:"user_email"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"`
TenantID uuid.UUID `json:"tenant_id"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
8.4 JWT Middleware
internal/http/middleware/jwt.go:
package middleware
import (
"context"
"net/http"
"strings"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/jwt"
)
// JWTMiddleware validates JWT tokens and populates session context
type JWTMiddleware struct {
jwtProvider jwt.Provider
sessionService service.SessionService
logger *zap.Logger
}
// NewJWTMiddleware creates a new JWT middleware
func NewJWTMiddleware(
jwtProvider jwt.Provider,
sessionService service.SessionService,
logger *zap.Logger,
) *JWTMiddleware {
return &JWTMiddleware{
jwtProvider: jwtProvider,
sessionService: sessionService,
logger: logger.Named("jwt-middleware"),
}
}
// ProvideJWTMiddleware provides JWT middleware for Wire
func ProvideJWTMiddleware(
jwtProvider jwt.Provider,
sessionService service.SessionService,
logger *zap.Logger,
) *JWTMiddleware {
return NewJWTMiddleware(jwtProvider, sessionService, logger)
}
// Handler validates JWT and populates context
func (m *JWTMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Expected format: "JWT <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "JWT" {
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
token := parts[1]
// Validate token
sessionID, err := m.jwtProvider.ValidateToken(token)
if err != nil {
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Get session from cache
session, err := m.sessionService.GetSession(r.Context(), sessionID)
if err != nil {
ctx := context.WithValue(r.Context(), constants.SessionIsAuthorized, false)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Populate context with session data
ctx := r.Context()
ctx = context.WithValue(ctx, constants.SessionIsAuthorized, true)
ctx = context.WithValue(ctx, constants.SessionID, session.ID)
ctx = context.WithValue(ctx, constants.SessionUserUUID, session.UserUUID.String())
ctx = context.WithValue(ctx, constants.SessionUserEmail, session.UserEmail)
ctx = context.WithValue(ctx, constants.SessionUserName, session.UserName)
ctx = context.WithValue(ctx, constants.SessionUserRole, session.UserRole)
ctx = context.WithValue(ctx, constants.SessionTenantID, session.TenantID.String())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// RequireAuth ensures the request is authenticated
func (m *JWTMiddleware) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isAuthorized, ok := r.Context().Value(constants.SessionIsAuthorized).(bool)
if !ok || !isAuthorized {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
8.5 Login Handler Example
internal/interface/http/handler/gateway/login_handler.go:
package gateway
import (
"encoding/json"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/dto/gateway"
gatewaysvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httpresponse"
)
// LoginHandler handles user login
type LoginHandler struct {
loginService gatewaysvc.LoginService
logger *zap.Logger
}
// ProvideLoginHandler provides a login handler for Wire
func ProvideLoginHandler(
loginService gatewaysvc.LoginService,
logger *zap.Logger,
) *LoginHandler {
return &LoginHandler{
loginService: loginService,
logger: logger.Named("login-handler"),
}
}
// Handle processes login requests
func (h *LoginHandler) Handle(w http.ResponseWriter, r *http.Request) {
// Parse request
var req gateway.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.SendError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
// Validate input
if req.Email == "" || req.Password == "" {
httperror.SendError(w, http.StatusBadRequest, "Email and password are required", nil)
return
}
// Execute login
response, err := h.loginService.Login(r.Context(), &gatewaysvc.LoginInput{
Email: req.Email,
Password: req.Password,
})
if err != nil {
h.logger.Error("Login failed", zap.Error(err))
httperror.SendError(w, http.StatusUnauthorized, "Invalid credentials", err)
return
}
// Send response
httpresponse.SendJSON(w, http.StatusOK, response)
}
9. Clean Architecture Layers
Layer Structure
9.1 Domain Layer (internal/domain/)
Purpose: Core business entities and repository interfaces
Example: User Entity
// internal/domain/user/entity.go
package user
import (
"time"
"github.com/google/uuid"
)
// User represents a user entity
type User struct {
ID uuid.UUID
TenantID uuid.UUID
Email string
Name string
Role string
Password string // Hashed
Status string
CreatedAt time.Time
UpdatedAt time.Time
}
Example: User Repository Interface
// internal/domain/user/repository.go
package user
import (
"context"
"github.com/google/uuid"
)
// Repository defines user data access interface
type Repository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
}
9.2 Repository Layer (internal/repository/)
Purpose: Data access implementations
Example: User Repository Implementation
// internal/repository/user/impl.go
package user
import (
"context"
"github.com/gocql/gocql"
"github.com/google/uuid"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/cache"
)
type repository struct {
session *gocql.Session
cache cache.TwoTierCacher
logger *zap.Logger
}
// ProvideRepository provides a user repository for Wire
func ProvideRepository(
session *gocql.Session,
cache cache.TwoTierCacher,
logger *zap.Logger,
) user.Repository {
return &repository{
session: session,
cache: cache,
logger: logger.Named("user-repository"),
}
}
// Create creates a new user
func (r *repository) Create(ctx context.Context, user *user.User) error {
query := `
INSERT INTO users (id, tenant_id, email, name, role, password, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`
return r.session.Query(query,
user.ID,
user.TenantID,
user.Email,
user.Name,
user.Role,
user.Password,
user.Status,
user.CreatedAt,
user.UpdatedAt,
).WithContext(ctx).Exec()
}
// GetByID retrieves a user by ID
func (r *repository) GetByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
// Try cache first
cacheKey := fmt.Sprintf("user:id:%s", id.String())
if cachedData, err := r.cache.Get(ctx, cacheKey); err == nil {
var u user.User
if err := json.Unmarshal(cachedData, &u); err == nil {
return &u, nil
}
}
// Query database
query := `
SELECT id, tenant_id, email, name, role, password, status, created_at, updated_at
FROM users
WHERE id = ?
`
var u user.User
err := r.session.Query(query, id).
WithContext(ctx).
Scan(&u.ID, &u.TenantID, &u.Email, &u.Name, &u.Role, &u.Password, &u.Status, &u.CreatedAt, &u.UpdatedAt)
if err != nil {
return nil, err
}
// Cache result
if data, err := json.Marshal(u); err == nil {
_ = r.cache.Set(ctx, cacheKey, data, 15*time.Minute)
}
return &u, nil
}
9.3 Use Case Layer (internal/usecase/)
Purpose: Focused, single-responsibility business operations
Example: Create User Entity Use Case
// internal/usecase/user/create_user_entity.go
package user
import (
"context"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
)
// CreateUserEntityUseCase creates a user entity from input
type CreateUserEntityUseCase struct {
logger *zap.Logger
}
// ProvideCreateUserEntityUseCase provides the use case for Wire
func ProvideCreateUserEntityUseCase(logger *zap.Logger) *CreateUserEntityUseCase {
return &CreateUserEntityUseCase{
logger: logger.Named("create-user-entity-uc"),
}
}
// CreateUserEntityInput represents the input
type CreateUserEntityInput struct {
TenantID uuid.UUID
Email string
Name string
Role string
HashedPassword string
}
// Execute creates a user entity
func (uc *CreateUserEntityUseCase) Execute(
ctx context.Context,
input *CreateUserEntityInput,
) (*user.User, error) {
now := time.Now().UTC()
entity := &user.User{
ID: uuid.New(),
TenantID: input.TenantID,
Email: input.Email,
Name: input.Name,
Role: input.Role,
Password: input.HashedPassword,
Status: constants.TenantStatusActive,
CreatedAt: now,
UpdatedAt: now,
}
uc.logger.Info("User entity created",
zap.String("user_id", entity.ID.String()),
zap.String("email", entity.Email))
return entity, nil
}
Example: Save User to Repo Use Case
// internal/usecase/user/save_user_to_repo.go
package user
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/domain/user"
)
// SaveUserToRepoUseCase saves a user to repository
type SaveUserToRepoUseCase struct {
repo user.Repository
logger *zap.Logger
}
// ProvideSaveUserToRepoUseCase provides the use case for Wire
func ProvideSaveUserToRepoUseCase(
repo user.Repository,
logger *zap.Logger,
) *SaveUserToRepoUseCase {
return &SaveUserToRepoUseCase{
repo: repo,
logger: logger.Named("save-user-to-repo-uc"),
}
}
// Execute saves the user
func (uc *SaveUserToRepoUseCase) Execute(
ctx context.Context,
user *user.User,
) error {
if err := uc.repo.Create(ctx, user); err != nil {
uc.logger.Error("Failed to save user",
zap.String("user_id", user.ID.String()),
zap.Error(err))
return err
}
uc.logger.Info("User saved to repository",
zap.String("user_id", user.ID.String()))
return nil
}
9.4 Service Layer (internal/service/)
Purpose: Orchestration logic, transaction management (SAGA)
Example: Create User Service
// internal/service/user/create.go
package user
import (
"context"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
userusecase "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/usecase/user"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/security/password"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/transaction"
)
// CreateUserService handles user creation orchestration
type CreateUserService interface {
Create(ctx context.Context, input *CreateUserInput) (*CreateUserResponse, error)
}
type createUserService struct {
validateEmailUC userusecase.ValidateUserEmailUniqueUseCase
createEntityUC *userusecase.CreateUserEntityUseCase
saveToRepoUC *userusecase.SaveUserToRepoUseCase
passwordProvider password.Provider
logger *zap.Logger
}
// ProvideCreateUserService provides the service for Wire
func ProvideCreateUserService(
validateEmailUC *userusecase.ValidateUserEmailUniqueUseCase,
createEntityUC *userusecase.CreateUserEntityUseCase,
saveToRepoUC *userusecase.SaveUserToRepoUseCase,
passwordProvider password.Provider,
logger *zap.Logger,
) CreateUserService {
return &createUserService{
validateEmailUC: validateEmailUC,
createEntityUC: createEntityUC,
saveToRepoUC: saveToRepoUC,
passwordProvider: passwordProvider,
logger: logger.Named("create-user-service"),
}
}
type CreateUserInput struct {
TenantID uuid.UUID
Email string
Name string
Role string
Password string
}
type CreateUserResponse struct {
UserID string
Email string
Name string
Role string
}
// Create orchestrates user creation with SAGA pattern
func (s *createUserService) Create(
ctx context.Context,
input *CreateUserInput,
) (*CreateUserResponse, error) {
// Validate email uniqueness
if err := s.validateEmailUC.Execute(ctx, input.Email); err != nil {
return nil, fmt.Errorf("email validation failed: %w", err)
}
// Hash password
hashedPassword, err := s.passwordProvider.HashPassword(input.Password)
if err != nil {
return nil, fmt.Errorf("password hashing failed: %w", err)
}
// Create user entity
userEntity, err := s.createEntityUC.Execute(ctx, &userusecase.CreateUserEntityInput{
TenantID: input.TenantID,
Email: input.Email,
Name: input.Name,
Role: input.Role,
HashedPassword: hashedPassword,
})
if err != nil {
return nil, fmt.Errorf("entity creation failed: %w", err)
}
// Use SAGA pattern for transaction management
saga := transaction.NewSaga(s.logger)
// Step 1: Save user to repository
saga.AddStep(
func(ctx context.Context) error {
return s.saveToRepoUC.Execute(ctx, userEntity)
},
func(ctx context.Context) error {
// Compensation: Delete user if subsequent steps fail
// (implement delete use case)
s.logger.Warn("Compensating: deleting user", zap.String("user_id", userEntity.ID.String()))
return nil
},
)
// Execute SAGA
if err := saga.Execute(ctx); err != nil {
return nil, fmt.Errorf("user creation failed: %w", err)
}
return &CreateUserResponse{
UserID: userEntity.ID.String(),
Email: userEntity.Email,
Name: userEntity.Name,
Role: userEntity.Role,
}, nil
}
9.5 Interface Layer (internal/interface/http/)
Purpose: HTTP handlers and DTOs
Example: Create User Handler
// internal/interface/http/handler/user/create_handler.go
package user
import (
"encoding/json"
"net/http"
"github.com/google/uuid"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config/constants"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/dto/user"
usersvc "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/service/user"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/pkg/httpresponse"
)
// CreateHandler handles user creation
type CreateHandler struct {
createService usersvc.CreateUserService
logger *zap.Logger
}
// ProvideCreateHandler provides the handler for Wire
func ProvideCreateHandler(
createService usersvc.CreateUserService,
logger *zap.Logger,
) *CreateHandler {
return &CreateHandler{
createService: createService,
logger: logger.Named("create-user-handler"),
}
}
// Handle processes user creation requests
func (h *CreateHandler) Handle(w http.ResponseWriter, r *http.Request) {
// Get tenant ID from context (populated by JWT middleware)
tenantIDStr, ok := r.Context().Value(constants.SessionTenantID).(string)
if !ok {
httperror.SendError(w, http.StatusUnauthorized, "Tenant ID not found", nil)
return
}
tenantID, err := uuid.Parse(tenantIDStr)
if err != nil {
httperror.SendError(w, http.StatusBadRequest, "Invalid tenant ID", err)
return
}
// Parse request
var req user.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httperror.SendError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
// Validate input
if req.Email == "" || req.Password == "" || req.Name == "" {
httperror.SendError(w, http.StatusBadRequest, "Missing required fields", nil)
return
}
// Create user
response, err := h.createService.Create(r.Context(), &usersvc.CreateUserInput{
TenantID: tenantID,
Email: req.Email,
Name: req.Name,
Role: req.Role,
Password: req.Password,
})
if err != nil {
h.logger.Error("User creation failed", zap.Error(err))
httperror.SendError(w, http.StatusInternalServerError, "Failed to create user", err)
return
}
// Send response
httpresponse.SendJSON(w, http.StatusCreated, response)
}
10. Database Setup (Cassandra)
10.1 Cassandra Schema Design
Design Principles:
- Query-driven modeling - Design tables based on query patterns
- Denormalization - Duplicate data to avoid joins
- Partition keys - Choose keys that distribute data evenly
- Clustering keys - Define sort order within partitions
Example Migration:
-- migrations/000001_create_users.up.cql
-- Users table (by ID)
CREATE TABLE IF NOT EXISTS users (
id uuid,
tenant_id uuid,
email text,
name text,
role text,
password text,
status text,
created_at timestamp,
updated_at timestamp,
PRIMARY KEY (id)
);
-- Users by email (for login lookups)
CREATE TABLE IF NOT EXISTS users_by_email (
email text,
tenant_id uuid,
user_id uuid,
PRIMARY KEY (email)
);
-- Users by tenant (for listing users in a tenant)
CREATE TABLE IF NOT EXISTS users_by_tenant (
tenant_id uuid,
user_id uuid,
email text,
name text,
role text,
status text,
created_at timestamp,
PRIMARY KEY (tenant_id, created_at, user_id)
) WITH CLUSTERING ORDER BY (created_at DESC, user_id ASC);
-- Create indexes
CREATE INDEX IF NOT EXISTS users_status_idx ON users (status);
CREATE INDEX IF NOT EXISTS users_tenant_idx ON users (tenant_id);
-- migrations/000001_create_users.down.cql
DROP TABLE IF EXISTS users_by_tenant;
DROP TABLE IF EXISTS users_by_email;
DROP INDEX IF EXISTS users_tenant_idx;
DROP INDEX IF EXISTS users_status_idx;
DROP TABLE IF EXISTS users;
10.2 Migration Management
pkg/storage/database/migrator.go:
package database
import (
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/cassandra"
_ "github.com/golang-migrate/migrate/v4/source/file"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
)
// Migrator handles database migrations
type Migrator struct {
migrate *migrate.Migrate
logger *zap.Logger
}
// NewMigrator creates a new migrator
func NewMigrator(cfg *config.Config, logger *zap.Logger) *Migrator {
// Build Cassandra connection string
dbURL := fmt.Sprintf("cassandra://%s/%s?consistency=%s",
cfg.Database.Hosts[0],
cfg.Database.Keyspace,
cfg.Database.Consistency,
)
// Create migrate instance
m, err := migrate.New(cfg.Database.MigrationsPath, dbURL)
if err != nil {
logger.Fatal("Failed to create migrator", zap.Error(err))
}
return &Migrator{
migrate: m,
logger: logger.Named("migrator"),
}
}
// Up runs all pending migrations
func (m *Migrator) Up() error {
m.logger.Info("Running migrations...")
if err := m.migrate.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("migration failed: %w", err)
}
version, _, _ := m.migrate.Version()
m.logger.Info("Migrations completed",
zap.Uint("version", uint(version)))
return nil
}
// Down rolls back the last migration
func (m *Migrator) Down() error {
m.logger.Info("Rolling back last migration...")
if err := m.migrate.Steps(-1); err != nil {
return fmt.Errorf("rollback failed: %w", err)
}
version, _, _ := m.migrate.Version()
m.logger.Info("Rollback completed",
zap.Uint("version", uint(version)))
return nil
}
// Version returns current migration version
func (m *Migrator) Version() (uint, bool, error) {
return m.migrate.Version()
}
// ForceVersion forces migration to specific version
func (m *Migrator) ForceVersion(version int) error {
m.logger.Warn("Forcing migration version",
zap.Int("version", version))
return m.migrate.Force(version)
}
11. Middleware Implementation
11.1 Security Headers Middleware
internal/http/middleware/security_headers.go:
package middleware
import (
"net/http"
"strings"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
)
// SecurityHeadersMiddleware adds security headers to responses
type SecurityHeadersMiddleware struct {
config *config.Config
logger *zap.Logger
}
// ProvideSecurityHeadersMiddleware provides the middleware for Wire
func ProvideSecurityHeadersMiddleware(
cfg *config.Config,
logger *zap.Logger,
) *SecurityHeadersMiddleware {
return &SecurityHeadersMiddleware{
config: cfg,
logger: logger.Named("security-headers-middleware"),
}
}
// Handler applies security headers
func (m *SecurityHeadersMiddleware) Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CWE-693: Apply security headers
// CORS headers
origin := r.Header.Get("Origin")
if m.isAllowedOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Tenant-ID")
w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
// Handle preflight requests
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
// Security headers (CWE-693: Protection Mechanism Failure)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
// isAllowedOrigin checks if origin is in allowed list
func (m *SecurityHeadersMiddleware) isAllowedOrigin(origin string) bool {
if origin == "" {
return false
}
// In development, allow localhost
if m.config.App.Environment == "development" {
if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") {
return true
}
}
// Check against configured allowed origins
for _, allowed := range m.config.Security.AllowedOrigins {
if origin == allowed {
return true
}
}
return false
}
11.2 Request Size Limit Middleware
internal/http/middleware/request_size_limit.go:
package middleware
import (
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
)
// RequestSizeLimitMiddleware limits request body size
type RequestSizeLimitMiddleware struct {
config *config.Config
logger *zap.Logger
}
// ProvideRequestSizeLimitMiddleware provides the middleware for Wire
func ProvideRequestSizeLimitMiddleware(
cfg *config.Config,
logger *zap.Logger,
) *RequestSizeLimitMiddleware {
return &RequestSizeLimitMiddleware{
config: cfg,
logger: logger.Named("request-size-limit-middleware"),
}
}
// LimitSmall applies 1MB limit (for auth endpoints)
func (m *RequestSizeLimitMiddleware) LimitSmall() func(http.Handler) http.Handler {
return m.limitWithSize(1 * 1024 * 1024) // 1 MB
}
// LimitMedium applies 5MB limit (for typical API endpoints)
func (m *RequestSizeLimitMiddleware) LimitMedium() func(http.Handler) http.Handler {
return m.limitWithSize(5 * 1024 * 1024) // 5 MB
}
// LimitLarge applies 50MB limit (for file uploads)
func (m *RequestSizeLimitMiddleware) LimitLarge() func(http.Handler) http.Handler {
return m.limitWithSize(50 * 1024 * 1024) // 50 MB
}
// limitWithSize creates a middleware with specific size limit
func (m *RequestSizeLimitMiddleware) limitWithSize(maxBytes int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Limit request body size (CWE-770: Resource Exhaustion)
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
}
12. HTTP Server Setup
internal/interface/http/server.go
package http
import (
"context"
"fmt"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
httpmw "codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/http/middleware"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/handler/healthcheck"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/internal/interface/http/middleware"
)
// Server represents the HTTP server
type Server struct {
server *http.Server
logger *zap.Logger
jwtMiddleware *httpmw.JWTMiddleware
securityHeadersMiddleware *httpmw.SecurityHeadersMiddleware
requestSizeLimitMw *httpmw.RequestSizeLimitMiddleware
config *config.Config
healthHandler *healthcheck.Handler
loginHandler *gateway.LoginHandler
}
// ProvideServer creates a new HTTP server
func ProvideServer(
cfg *config.Config,
logger *zap.Logger,
jwtMiddleware *httpmw.JWTMiddleware,
securityHeadersMiddleware *httpmw.SecurityHeadersMiddleware,
requestSizeLimitMw *httpmw.RequestSizeLimitMiddleware,
healthHandler *healthcheck.Handler,
loginHandler *gateway.LoginHandler,
) *Server {
mux := http.NewServeMux()
s := &Server{
logger: logger,
jwtMiddleware: jwtMiddleware,
securityHeadersMiddleware: securityHeadersMiddleware,
requestSizeLimitMw: requestSizeLimitMw,
config: cfg,
healthHandler: healthHandler,
loginHandler: loginHandler,
}
// Register routes
s.registerRoutes(mux)
// Create HTTP server
s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: s.applyMiddleware(mux),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
IdleTimeout: cfg.HTTP.IdleTimeout,
}
logger.Info("✓ HTTP server configured",
zap.String("address", s.server.Addr),
zap.Duration("read_timeout", cfg.HTTP.ReadTimeout),
zap.Duration("write_timeout", cfg.HTTP.WriteTimeout))
return s
}
// registerRoutes registers all HTTP routes
func (s *Server) registerRoutes(mux *http.ServeMux) {
// ===== PUBLIC ROUTES =====
mux.HandleFunc("GET /health", s.healthHandler.Handle)
// Authentication routes
mux.HandleFunc("POST /api/v1/login",
s.requestSizeLimitMw.LimitSmall()(
http.HandlerFunc(s.loginHandler.Handle),
).ServeHTTP)
// ===== AUTHENTICATED ROUTES =====
// Add your authenticated routes here with JWT middleware
// Example:
// mux.HandleFunc("GET /api/v1/me", s.applyAuthOnly(s.meHandler.Handle))
}
// applyMiddleware applies global middleware to all routes
func (s *Server) applyMiddleware(handler http.Handler) http.Handler {
// Apply middleware in order
handler = middleware.LoggerMiddleware(s.logger)(handler)
handler = s.securityHeadersMiddleware.Handler(handler)
return handler
}
// applyAuthOnly applies only JWT authentication middleware
func (s *Server) applyAuthOnly(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.jwtMiddleware.Handler(
s.jwtMiddleware.RequireAuth(
http.HandlerFunc(handler),
),
).ServeHTTP(w, r)
}
}
// Start starts the HTTP server
func (s *Server) Start() error {
s.logger.Info("")
s.logger.Info("🚀 Backend is ready!")
s.logger.Info("",
zap.String("address", s.server.Addr),
zap.String("url", fmt.Sprintf("http://localhost:%d", s.config.Server.Port)))
s.logger.Info("")
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start server: %w", err)
}
return nil
}
// Shutdown gracefully shuts down the HTTP server
func (s *Server) Shutdown(ctx context.Context) error {
s.logger.Info("shutting down HTTP server")
if err := s.server.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown server: %w", err)
}
s.logger.Info("HTTP server shut down successfully")
return nil
}
13. Docker & Infrastructure
13.1 Development Dockerfile
dev.Dockerfile:
FROM golang:1.24-alpine
# Install development tools
RUN apk add --no-cache git curl bash
# Set working directory
WORKDIR /go/src/codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
# Install Wire
RUN go install github.com/google/wire/cmd/wire@latest
# Copy go.mod and go.sum
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Generate Wire code
RUN cd app && wire
# Build the application
RUN go build -o app-dev .
# Expose port
EXPOSE 8000
# Run the application
CMD ["./app-dev", "daemon"]
13.2 Production Dockerfile
Dockerfile:
###
### Build Stage
###
FROM golang:1.24-alpine AS build-env
# Create app directory
RUN mkdir /app
WORKDIR /app
# Copy dependency list
COPY go.mod go.sum ./
# Install dependencies
RUN go mod download
# Copy all files
COPY . .
# Install Wire
RUN go install github.com/google/wire/cmd/wire@latest
# Generate Wire code
RUN cd app && wire
# Build for Linux AMD64
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app-backend .
###
### Run Stage
###
FROM alpine:latest
WORKDIR /app
# Copy executable
COPY --from=build-env /app/app-backend .
# Copy migrations
COPY --from=build-env /app/migrations ./migrations
# Copy static files (if any)
COPY --from=build-env /app/static ./static
EXPOSE 8000
# Run the server
CMD ["/app/app-backend", "daemon"]
13.3 Docker Compose (Development)
docker-compose.dev.yml:
# Use external network from shared infrastructure
networks:
maple-dev:
external: true
services:
app:
container_name: your-backend-dev
stdin_open: true
build:
context: .
dockerfile: ./dev.Dockerfile
ports:
- "${SERVER_PORT:-8000}:${SERVER_PORT:-8000}"
env_file:
- .env
environment:
# Application
APP_ENVIRONMENT: ${APP_ENVIRONMENT:-development}
APP_VERSION: ${APP_VERSION:-0.1.0-dev}
APP_JWT_SECRET: ${APP_JWT_SECRET:-dev-secret}
# Server
SERVER_HOST: ${SERVER_HOST:-0.0.0.0}
SERVER_PORT: ${SERVER_PORT:-8000}
# Cassandra (connect to shared infrastructure)
DATABASE_HOSTS: ${DATABASE_HOSTS:-cassandra-1:9042,cassandra-2:9042,cassandra-3:9042}
DATABASE_KEYSPACE: ${DATABASE_KEYSPACE:-your_keyspace}
DATABASE_CONSISTENCY: ${DATABASE_CONSISTENCY:-ONE}
DATABASE_REPLICATION: ${DATABASE_REPLICATION:-3}
DATABASE_MIGRATIONS_PATH: ${DATABASE_MIGRATIONS_PATH:-file://migrations}
# Redis (connect to shared infrastructure)
CACHE_HOST: ${CACHE_HOST:-redis}
CACHE_PORT: ${CACHE_PORT:-6379}
CACHE_PASSWORD: ${CACHE_PASSWORD:-}
CACHE_DB: ${CACHE_DB:-0}
# Logger
LOGGER_LEVEL: ${LOGGER_LEVEL:-debug}
LOGGER_FORMAT: ${LOGGER_FORMAT:-console}
volumes:
- ./:/go/src/codeberg.org/mapleopentech/monorepo/cloud/your-backend-name
networks:
- maple-dev
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${SERVER_PORT:-8000}/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
13.4 Task Runner (Taskfile.yml)
Taskfile.yml:
version: "3"
env:
COMPOSE_PROJECT_NAME: your-backend
vars:
DOCKER_COMPOSE_CMD:
sh: |
if command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
elif docker compose version >/dev/null 2>&1; then
echo "docker compose"
else
echo "docker-compose"
fi
tasks:
# Development workflow
dev:
desc: Start app in development mode
cmds:
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml up --build"
dev:down:
desc: Stop development app
cmds:
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml down"
dev:restart:
desc: Quick restart
cmds:
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml restart"
dev:logs:
desc: View app logs
cmds:
- "{{.DOCKER_COMPOSE_CMD}} -f docker-compose.dev.yml logs -f"
dev:shell:
desc: Open shell in running container
cmds:
- docker exec -it your-backend-dev sh
# Database operations
migrate:up:
desc: Run all migrations up
cmds:
- ./app-backend migrate up
migrate:down:
desc: Run all migrations down
cmds:
- ./app-backend migrate down
# Build and test
build:
desc: Build the Go binary
cmds:
- task: wire
- go build -o app-backend
wire:
desc: Generate Wire dependency injection
cmds:
- cd app && wire
test:
desc: Run tests
cmds:
- go test ./... -v
lint:
desc: Run linters
cmds:
- go vet ./...
format:
desc: Format code
cmds:
- go fmt ./...
tidy:
desc: Tidy Go modules
cmds:
- go mod tidy
# Production deployment
deploy:
desc: Build and push production container
cmds:
- docker build -f Dockerfile --rm -t registry.example.com/your-org/your_backend:prod --platform linux/amd64 .
- docker push registry.example.com/your-org/your_backend:prod
14. CLI Commands (Cobra)
14.1 Root Command
cmd/root.go:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/daemon"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/migrate"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd/version"
)
var rootCmd = &cobra.Command{
Use: "your-backend",
Short: "Your Backend Service",
Long: `Your Backend - Clean Architecture with Wire DI and Cassandra`,
}
// Execute runs the root command
func Execute() {
rootCmd.AddCommand(daemon.DaemonCmd())
rootCmd.AddCommand(migrate.MigrateCmd())
rootCmd.AddCommand(version.VersionCmd())
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
14.2 Daemon Command
cmd/daemon/daemon.go:
package daemon
import (
"log"
"github.com/spf13/cobra"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/app"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
)
// DaemonCmd returns the daemon command
func DaemonCmd() *cobra.Command {
return &cobra.Command{
Use: "daemon",
Short: "Start the HTTP server",
Run: func(cmd *cobra.Command, args []string) {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize application with Wire
application, err := app.InitializeApplication(cfg)
if err != nil {
log.Fatalf("Failed to initialize application: %v", err)
}
// Start application
if err := application.Start(); err != nil {
log.Fatalf("Application failed: %v", err)
}
},
}
}
14.3 Main Entry Point
main.go:
package main
import (
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/cmd"
)
func main() {
cmd.Execute()
}
15. Development Workflow
Daily Development Flow
# 1. Start shared infrastructure (first time)
cd cloud/infrastructure/development
task dev:start
# 2. Start your backend
cd cloud/your-backend-name
task dev
# 3. Run migrations (first time)
task migrate:up
# 4. Make code changes...
# 5. Quick restart (after code changes)
task dev:restart
# 6. View logs
task dev:logs
# 7. Run tests
task test
# 8. Format and lint
task format
task lint
# 9. Stop backend
task dev:down
Common Development Tasks
# Generate Wire dependencies
task wire
# Build binary locally
task build
# Run locally (without Docker)
./app-backend daemon
# Create new migration
./app-backend migrate create create_new_table
# Check migration version
./app-backend migrate version
# Reset database
task db:reset
# Open shell in container
task dev:shell
# Check application version
./app-backend version
16. Testing Strategy
16.1 Unit Testing
Example: Use Case Test
// internal/usecase/user/create_user_entity_test.go
package user
import (
"context"
"testing"
"github.com/google/uuid"
"go.uber.org/zap"
)
func TestCreateUserEntityUseCase_Execute(t *testing.T) {
logger := zap.NewNop()
uc := ProvideCreateUserEntityUseCase(logger)
input := &CreateUserEntityInput{
TenantID: uuid.New(),
Email: "test@example.com",
Name: "Test User",
Role: "user",
HashedPassword: "hashed-password",
}
entity, err := uc.Execute(context.Background(), input)
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if entity.Email != input.Email {
t.Errorf("Expected email %s, got %s", input.Email, entity.Email)
}
if entity.ID == uuid.Nil {
t.Error("Expected non-nil ID")
}
}
16.2 Integration Testing
Use test containers for integration tests:
// internal/repository/user/integration_test.go
package user
import (
"context"
"testing"
"github.com/gocql/gocql"
"github.com/testcontainers/testcontainers-go"
)
func TestUserRepository_Integration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
// Start Cassandra container
ctx := context.Background()
cassandraContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "cassandra:4.1",
ExposedPorts: []string{"9042/tcp"},
},
Started: true,
})
if err != nil {
t.Fatalf("Failed to start container: %v", err)
}
defer cassandraContainer.Terminate(ctx)
// Run tests...
}
17. Production Deployment
17.1 Build Production Container
# Build for Linux AMD64
task deploy
17.2 Environment Variables (Production)
# .env (production)
APP_ENVIRONMENT=production
APP_VERSION=1.0.0
APP_JWT_SECRET=<use-openssl-rand-base64-64>
SERVER_HOST=0.0.0.0
SERVER_PORT=8000
DATABASE_HOSTS=cassandra-prod-1:9042,cassandra-prod-2:9042,cassandra-prod-3:9042
DATABASE_KEYSPACE=your_keyspace_prod
DATABASE_CONSISTENCY=QUORUM
DATABASE_REPLICATION=3
CACHE_HOST=redis-prod
CACHE_PORT=6379
CACHE_PASSWORD=<strong-password>
CACHE_DB=0
SECURITY_IP_ENCRYPTION_KEY=<use-openssl-rand-hex-16>
SECURITY_CORS_ALLOWED_ORIGINS=https://yourdomain.com
LOGGER_LEVEL=info
LOGGER_FORMAT=json
17.3 Health Checks
internal/interface/http/handler/healthcheck/healthcheck_handler.go:
package healthcheck
import (
"encoding/json"
"net/http"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/your-backend-name/config"
)
// Handler handles health check requests
type Handler struct {
config *config.Config
logger *zap.Logger
}
// ProvideHealthCheckHandler provides the handler for Wire
func ProvideHealthCheckHandler(
cfg *config.Config,
logger *zap.Logger,
) *Handler {
return &Handler{
config: cfg,
logger: logger.Named("healthcheck-handler"),
}
}
// Handle responds to health check requests
func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"status": "ok",
"timestamp": time.Now().UTC(),
"environment": h.config.App.Environment,
"version": h.config.App.Version,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
Summary
You now have a complete blueprint for building Golang backends with:
✅ Clean Architecture - Proper layer separation and dependency flow ✅ Wire Dependency Injection - Compile-time, type-safe DI ✅ JWT Authentication - Secure session management with two-tier caching ✅ Cassandra Database - Query-driven schema design with migrations ✅ Reusable pkg/ Components - Copy-paste infrastructure utilities ✅ Security-First - CWE-compliant middleware and validation ✅ Docker Infrastructure - Development and production containers ✅ CLI Commands - Cobra-based command structure
Next Steps
- Copy this document to your new project's
docs/directory - Run the copy-pkg.sh script to copy reusable components
- Update import paths throughout the codebase
- Customize domain entities, use cases, and services for your specific needs
- Add business logic while maintaining the architectural patterns
Reference Implementation
Always refer back to cloud/maplepress-backend for:
- Complete working examples
- Advanced patterns (SAGA, rate limiting, etc.)
- Production-tested code
- Security best practices
Questions or Issues? Review the maplepress-backend codebase or create a new issue in the repository.