Skip to content

GoCodeAlone/modular

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

modular

Modular Go

GitHub License Go Reference CodeQL Dependabot Updates CI Modules CI Examples CI Go Report Card codecov

Testing

Run all tests:

go test ./... -v

Parallel Module BDD Suites

To speed up BDD feedback locally you can execute module BDD suites in parallel:

chmod +x scripts/run-module-bdd-parallel.sh
scripts/run-module-bdd-parallel.sh 6   # 6 workers; omit number to auto-detect CPUs

The script prefers GNU parallel and falls back to xargs -P.

Overview

Modular is a package that provides a structured way to create modular applications in Go. It allows you to build applications as collections of modules that can be easily added, removed, or replaced. Key features include:

  • Module lifecycle management: Initialize, start, and gracefully stop modules
  • Dependency management: Automatically resolve and order module dependencies
  • Service registry: Register and retrieve application services
  • Configuration management: Handle configuration for modules and services
  • Configuration validation: Validate configurations with defaults, required fields, and custom logic
  • Sample config generation: Generate sample configuration files in various formats
  • Dependency injection: Inject required services into modules
  • Multi-tenancy support: Build applications that serve multiple tenants with isolated configurations
  • Observer pattern: Event-driven communication with CloudEvents support for standardized event handling

🧩 Available Modules

Modular comes with a rich ecosystem of pre-built modules that you can easily integrate into your applications:

Module Description Configuration Documentation
auth Authentication and authorization with JWT, sessions, password hashing, and OAuth2/OIDC support Yes Documentation
cache Multi-backend caching with Redis and in-memory support Yes Documentation
chimux Chi router integration with middleware support Yes Documentation
database Database connectivity and SQL operations with multiple driver support Yes Documentation
eventbus Asynchronous event handling and pub/sub messaging Yes Documentation
eventlogger Structured logging for Observer pattern events with CloudEvents support Yes Documentation
httpclient Configurable HTTP client with connection pooling, timeouts, and verbose logging Yes Documentation
httpserver HTTP/HTTPS server with TLS support, graceful shutdown, and configurable timeouts Yes Documentation
jsonschema JSON Schema validation services No Documentation
letsencrypt SSL/TLS certificate automation with Let's Encrypt Yes Documentation
reverseproxy Reverse proxy with load balancing, circuit breaker, and health monitoring Yes Documentation
scheduler Job scheduling with cron expressions and worker pools Yes Documentation

Each module is designed to be:

  • Plug-and-play: Easy integration with minimal configuration
  • Configurable: Extensive configuration options via YAML, environment variables, or code
  • Production-ready: Built with best practices, proper error handling, and comprehensive testing
  • Well-documented: Complete documentation with examples and API references

πŸ“– For detailed information about each module, see the modules directory or click on the individual module links above.

Governance & Engineering Standards

Core, non-negotiable project principles (TDD, lifecycle determinism, API stability, performance baselines, multi-tenancy isolation) are codified in the versioned Project Constitution. Day-to-day implementation checklists (interfaces, reflection usage, error style, logging fields, concurrency annotations, export review) live in Go Best Practices. Concurrency rules and race avoidance patterns are documented in Concurrency & Race Guidelines.

Always update docs & examples in the same PR as feature code; stale documentation is considered a failing gate.

🌩️ Observer Pattern with CloudEvents Support

Modular includes a powerful Observer pattern implementation with CloudEvents specification support, enabling event-driven communication between components while maintaining full backward compatibility.

Key Features

  • Traditional Observer Pattern: Subject/Observer interfaces for event emission and handling
  • CloudEvents Integration: Industry-standard event format with built-in validation and serialization
  • Dual Event Support: Emit and handle both traditional ObserverEvents and CloudEvents
  • ObservableApplication: Enhanced application with automatic lifecycle event emission
  • EventLogger Module: Structured logging for all events with multiple output targets
  • Transport Independence: Events ready for HTTP, gRPC, AMQP, and other transports

Quick Example

// Create observable application with CloudEvents support
app := modular.NewObservableApplication(configProvider, logger)

// Register event logger for structured logging
app.RegisterModule(eventlogger.NewModule())

// Emit CloudEvents using standardized format
event := modular.NewCloudEvent(
    "com.myapp.user.created",   // Type
    "user-service",             // Source  
    userData,                   // Data
    metadata,                   // Extensions
)
err := app.NotifyCloudEventObservers(context.Background(), event)

Documentation

Examples

The examples/ directory contains complete, working examples that demonstrate how to use Modular with different patterns and module combinations:

Example Description Features
basic-app Simple modular application HTTP server, routing, configuration
reverse-proxy HTTP reverse proxy server Load balancing, backend routing, CORS
http-client HTTP client with proxy backend HTTP client integration, request routing
advanced-logging Advanced HTTP client logging Verbose logging, file output, request/response inspection
observer-pattern Event-driven architecture demo Observer pattern, CloudEvents, event logging, real-time events

Quick Start with Examples

Each example is a complete, standalone application that you can run immediately:

cd examples/basic-app
GOWORK=off go build
./basic-app

Visit the examples directory for detailed documentation, configuration guides, and step-by-step instructions for each example.

Learning Path

Installation

go get github.com/GoCodeAlone/modular

Usage

Basic Application

package main

import (
    "github.com/GoCodeAlone/modular"
    "log/slog"
    "os"
)

func main() {
    // Create a logger
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    
    // Create config provider with application configuration
    config := &AppConfig{
        Name: "MyApp",
        Version: "1.0.0",
    }
    configProvider := modular.NewStdConfigProvider(config)
    
    // Create the application
    app := modular.NewStdApplication(configProvider, logger)
    
    // Register modules
    app.RegisterModule(NewDatabaseModule())
    app.RegisterModule(NewAPIModule())
    
    // Run the application (this will block until the application is terminated)
    if err := app.Run(); err != nil {
        logger.Error("Application error", "error", err)
        os.Exit(1)
    }
}

Creating a Module

type DatabaseModule struct {
    db     *sql.DB
    config *DatabaseConfig
}

func NewDatabaseModule() modular.Module {
    return &DatabaseModule{}
}

// RegisterConfig registers the module's configuration
func (m *DatabaseModule) RegisterConfig(app modular.Application) error {
    m.config = &DatabaseConfig{
        DSN: "postgres://user:password@localhost:5432/dbname",
    }
    app.RegisterConfigSection("database", modular.NewStdConfigProvider(m.config))
    return nil
}

// Name returns the module's unique name
func (m *DatabaseModule) Name() string {
    return "database"
}

// Dependencies returns other modules this module depends on
func (m *DatabaseModule) Dependencies() []string {
    return []string{} // No dependencies
}

// Init initializes the module
func (m *DatabaseModule) Init(app modular.Application) error {
    // Initialize database connection
    db, err := sql.Open("postgres", m.config.DSN)
    if (err != nil) {
        return err
    }
    m.db = db
    return nil
}

// ProvidesServices returns services provided by this module
func (m *DatabaseModule) ProvidesServices() []modular.ServiceProvider {
    return []modular.ServiceProvider{
        {
            Name:        "database",
            Description: "Database connection",
            Instance:    m.db,
        },
    }
}

// RequiresServices returns services required by this module
func (m *DatabaseModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{} // No required services
}

// Start starts the module
func (m *DatabaseModule) Start(ctx context.Context) error {
    return nil // Database is already connected
}

// Stop stops the module
func (m *DatabaseModule) Stop(ctx context.Context) error {
    return m.db.Close()
}

Service Dependencies

// A module that depends on another service
func (m *APIModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{
        {
            Name:     "database",
            Required: true,  // Application won't start if this service is missing
        },
        {
            Name:     "cache",
            Required: false, // Optional dependency
        },
    }
}

// Using constructor injection
func (m *APIModule) Constructor() modular.ModuleConstructor {
    return func(app modular.Application, services map[string]any) (modular.Module, error) {
        // Services that were requested in RequiresServices() are available here
        db := services["database"].(*sql.DB)
        
        // Create a new module instance with injected services
        return &APIModule{
            db: db,
        }, nil
    }
}

Interface-Based Service Matching

Modular supports finding and injecting services based on interface compatibility, regardless of the service name:

// Define an interface that services should implement
type LoggerService interface {
    Log(level string, message string)
}

// Require a service that implements a specific interface
func (m *MyModule) RequiresServices() []modular.ServiceDependency {
    return []modular.ServiceDependency{
        {
            Name:               "logger", // The name you'll use to access it in the Constructor
            Required:           true,
            MatchByInterface:   true, // Enable interface-based matching
            SatisfiesInterface: reflect.TypeOf((*LoggerService)(nil)).Elem(),
        },
    }
}

// Constructor will receive any service implementing LoggerService
func (m *MyModule) Constructor() modular.ModuleConstructor {
    return func(app modular.Application, services map[string]any) (modular.Module, error) {
        // This will work even if the actual registered service name is different
        logger := services["logger"].(LoggerService)
        return &MyModule{logger: logger}, nil
    }
}

See DOCUMENTATION.md for more advanced details about service dependencies and interface matching.

Logger Management

The framework provides methods to get and set the application logger, allowing for dynamic logger configuration at runtime:

// Get the current logger
currentLogger := app.Logger()

// Switch to a different logger (e.g., for different log levels or output destinations)
newLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
app.SetLogger(newLogger)

// The new logger is now used by the application and all modules
app.Logger().Info("Logger has been switched to JSON format with debug level")

This is useful for scenarios such as:

  • Dynamic log level changes: Switch between debug and production logging based on runtime conditions
  • Configuration-driven logging: Update logger configuration based on config file changes
  • Environment-specific loggers: Use different loggers for development vs production
  • Log rotation: Switch to new log files without restarting the application

Example: Dynamic log level switching

// Switch to debug logging when needed
debugLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
app.SetLogger(debugLogger)

// Later, switch back to info level
infoLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
app.SetLogger(infoLogger)

Configuration Management

// Define your configuration struct
type AppConfig struct {
    Name    string `json:"name" yaml:"name" default:"DefaultApp" desc:"Application name"`
    Version string `json:"version" yaml:"version" required:"true" desc:"Application version"`
    Debug   bool   `json:"debug" yaml:"debug" default:"false" desc:"Enable debug mode"`
}

// Implement ConfigValidator interface for custom validation
func (c *AppConfig) Validate() error {
    // Custom validation logic
    if c.Version == "0.0.0" {
        return fmt.Errorf("invalid version: %s", c.Version)
    }
    return nil
}

Configuration Validation and Default Values

Modular now includes powerful configuration validation features:

Default Values with Struct Tags

// Define struct with default values
type ServerConfig struct {
    Host        string `yaml:"host" default:"localhost" desc:"Server host"`
    Port        int    `yaml:"port" default:"8080" desc:"Server port"`
    ReadTimeout int    `yaml:"readTimeout" default:"30" desc:"Read timeout in seconds"`
    Debug       bool   `yaml:"debug" default:"false" desc:"Enable debug mode"`
}

Default values are automatically applied to fields that have zero/empty values when configurations are loaded.

Required Field Validation

type DatabaseConfig struct {
    Host     string `yaml:"host" default:"localhost" desc:"Database host"`
    Port     int    `yaml:"port" default:"5432" desc:"Database port"`
    Name     string `yaml:"name" required:"true" desc:"Database name"` // Must be provided
    User     string `yaml:"user" default:"postgres" desc:"Database user"`
    Password string `yaml:"password" required:"true" desc:"Database password"` // Must be provided
}

Required fields are validated during configuration loading, and appropriate errors are returned if they're missing.

Custom Validation Logic

// Implement the ConfigValidator interface
func (c *AppConfig) Validate() error {
    // Validate environment is one of the expected values
    validEnvs := map[string]bool{"dev": true, "test": true, "prod": true}
    if !validEnvs[c.Environment] {
        return fmt.Errorf("%w: environment must be one of [dev, test, prod]", modular.ErrConfigValidationFailed)
    }
    
    // Additional custom validation
    if c.Server.Port < 1024 || c.Server.Port > 65535 {
        return fmt.Errorf("%w: server port must be between 1024 and 65535", modular.ErrConfigValidationFailed)
    }
    
    return nil
}

Generating Sample Configuration Files

// Generate a sample configuration file
cfg := &AppConfig{}
err := modular.SaveSampleConfig(cfg, "yaml", "config-sample.yaml")
if err != nil {
    log.Fatalf("Error generating sample config: %v", err)
}

Sample configurations can be generated in YAML, JSON, or TOML formats, with all default values pre-populated.

Command-Line Integration

func main() {
    // Generate sample config file if requested
    if len(os.Args) > 1 && os.Args[1] == "--generate-config" {
        format := "yaml"
        if len(os.Args) > 2 {
            format = os.Args[2]
        }
        outputFile := "config-sample." + format
        if len(os.Args) > 3 {
            outputFile = os.Args[3]
        }
        
        cfg := &AppConfig{}
        if err := modular.SaveSampleConfig(cfg, format, outputFile); err != nil {
            fmt.Printf("Error generating sample config: %v\n", err)
            os.Exit(1)  // Error condition should exit with non-zero code
        }
        fmt.Printf("Sample config generated at %s\n", outputFile)
        os.Exit(0)
    }
    
    // Continue with normal application startup...
}

Multi-Tenant Support

Modular provides built-in support for multi-tenant applications through:

Tenant Contexts

// Creating a tenant context
tenantID := modular.TenantID("tenant1")
ctx := modular.NewTenantContext(context.Background(), tenantID)

// Using tenant context with the application
tenantCtx, err := app.WithTenant(tenantID)
if err != nil {
    log.Fatal("Failed to create tenant context:", err)
}

// Extract tenant ID from a context
if id, ok := modular.GetTenantIDFromContext(ctx); ok {
    fmt.Println("Current tenant:", id)
}

Tenant-Aware Configuration

// Register a tenant service in your module
func (m *MultiTenantModule) ProvidesServices() []modular.ServiceProvider {
    return []modular.ServiceProvider{
        {
            Name:        "tenantService",
            Description: "Tenant management service",
            Instance:    modular.NewStandardTenantService(m.logger),
        },
        {
            Name:        "tenantConfigLoader",
            Description: "Tenant configuration loader",
            Instance:    modular.DefaultTenantConfigLoader("./configs/tenants"),
        },
    }
}

// Create tenant-aware configuration
func (m *MultiTenantModule) RegisterConfig(app *modular.Application) {
    // Default config
    defaultConfig := &MyConfig{
        Setting: "default",
    }
    
    // Get tenant service (must be provided by another module)
    var tenantService modular.TenantService
    app.GetService("tenantService", &tenantService)
    
    // Create tenant-aware config provider
    tenantAwareConfig := modular.NewTenantAwareConfig(
        modular.NewStdConfigProvider(defaultConfig),
        tenantService,
        "mymodule",
    )
    
    app.RegisterConfigSection("mymodule", tenantAwareConfig)
}

// Using tenant-aware configs in your code
func (m *MultiTenantModule) ProcessRequestWithTenant(ctx context.Context) {
    // Get config specific to the tenant in the context
    config, ok := m.config.(*modular.TenantAwareConfig)
    if !ok {
        // Handle non-tenant-aware config
        return
    }
    
    // Get tenant-specific configuration
    myConfig := config.GetConfigWithContext(ctx).(*MyConfig)
    
    // Use tenant-specific settings
    fmt.Println("Tenant setting:", myConfig.Setting)
}

Tenant-Aware Modules

// Implement the TenantAwareModule interface
type MultiTenantModule struct {
    modular.Module
    tenantData map[modular.TenantID]*TenantData
}

func (m *MultiTenantModule) OnTenantRegistered(tenantID modular.TenantID) {
    // Initialize resources for this tenant
    m.tenantData[tenantID] = &TenantData{
        initialized: true,
    }
}

func (m *MultiTenantModule) OnTenantRemoved(tenantID modular.TenantID) {
    // Clean up tenant resources
    delete(m.tenantData, tenantID)
}

Loading Tenant Configurations

// Set up a file-based tenant config loader
configLoader := modular.NewFileBasedTenantConfigLoader(modular.TenantConfigParams{
    ConfigNameRegex: regexp.MustCompile("^tenant-[\\w-]+\\.(json|yaml)$"),
    ConfigDir:       "./configs/tenants",
    // Prefer per-app feeders (via app.SetConfigFeeders) over global when testing; examples use explicit slice for clarity
    ConfigFeeders:   []modular.Feeder{},
})

// Register the loader as a service
app.RegisterService("tenantConfigLoader", configLoader)

Key Interfaces

Module

The core interface that all modules must implement:

type Module interface {
    RegisterConfig(app *Application)
    Init(app *Application) error
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
    Name() string
    Dependencies() []string
    ProvidesServices() []ServiceProvider
    RequiresServices() []ServiceDependency
}

TenantAwareModule

Interface for modules that need to respond to tenant lifecycle events:

type TenantAwareModule interface {
    Module
    OnTenantRegistered(tenantID TenantID)
    OnTenantRemoved(tenantID TenantID)
}

TenantService

Interface for managing tenants:

type TenantService interface {
    GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)
    GetTenants() []TenantID
    RegisterTenant(tenantID TenantID, configs map[string]ConfigProvider) error
}

ConfigProvider

Interface for configuration providers:

type ConfigProvider interface {
    GetConfig() any
}

ConfigValidator

Interface for implementing custom configuration validation logic:

type ConfigValidator interface {
    Validate() error
}

CLI Tool

Modular comes with a command-line tool (modcli) to help you create new modules and configurations.

Test Isolation and Configuration Feeders

Historically tests mutated the package-level modular.ConfigFeeders slice directly to control configuration sources. This created hidden coupling and prevented safe use of t.Parallel(). The framework now supports per-application feeders via:

app.(*modular.StdApplication).SetConfigFeeders([]modular.Feeder{feeders.NewYamlFeeder("config.yaml"), feeders.NewEnvFeeder()})

Guidelines:

  1. In tests, prefer app.SetConfigFeeders(...) immediately after creating the application (before Init()).
  2. Pass nil to revert an app back to using global feeders (rare in tests now).
  3. Avoid mutating modular.ConfigFeeders in tests; example applications may still set the global slice once at startup for simplicity.
  4. The legacy isolation helper no longer snapshots feeders; only environment variables are isolated.

Benefit: tests become self-contained and can run in parallel without feeder race conditions.

Parallel Testing Guidelines

Short rules for adding t.Parallel() safely:

DO:

  • Pre-create apps and call app.SetConfigFeeders(...) instead of mutating global ConfigFeeders.
  • Set all required environment variables up-front in the parent test (one t.Setenv per variable) and then parallelize independent subtests that do NOT call t.Setenv themselves.
  • Keep tests idempotent: no shared global mutation, no time-dependent ordering.
  • Use isolated temp dirs (t.TempDir()) and unique filenames.

DO NOT:

  • Call t.Parallel() on a test that itself calls t.Setenv or t.Chdir (Go 1.25 restriction: mixing causes a panic: "test using t.Setenv or t.Chdir can not use t.Parallel").
  • Rely on mutation of package-level singletons (e.g. modifying global slices) across parallel tests.
  • Write to the same file or network port from multiple parallel tests.

Patterns:

  • Serial parent + parallel children: parent sets env vars; each child t.Parallel() if it doesn't modify env/working dir.
  • Fully serial tests: keep serial when per-case env mutation is unavoidable.

If in doubt, leave the test serial and add a brief comment explaining why (// NOTE: cannot parallelize because ...).

Installation

You can install the CLI tool using one of the following methods:

Using go install (recommended)

go install github.com/GoCodeAlone/modular/cmd/modcli@latest

This will download, build, and install the latest version of the CLI tool directly to your GOPATH's bin directory, which should be in your PATH.

Download pre-built binaries

Download the latest release from the GitHub Releases page and add it to your PATH.

Build from source

# Clone the repository
git clone https://github.com/GoCodeAlone/modular.git
cd modular/cmd/modcli

# Build the CLI tool
go build -o modcli

# Move to a directory in your PATH
mv modcli /usr/local/bin/  # Linux/macOS
# or add the current directory to your PATH

Usage

Generate a new module:

modcli generate module --name MyFeature

Generate a configuration:

modcli generate config --name Server

For more details on available commands:

modcli --help

Each command includes interactive prompts to guide you through the process of creating modules or configurations with the features you need.

πŸ“š Additional Resources

Having Issues?

If you're experiencing problems with module interfaces (e.g., "Module does not implement Startable"), check out the debugging section which includes diagnostic tools like:

// Debug module interface implementations
modular.DebugModuleInterfaces(app, "your-module-name")

// Check all modules at once
modular.DebugAllModuleInterfaces(app)

License

MIT License

About

Modular Go

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 8