This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Architecture Decision Records

Documentation of architectural decisions made in Go Micro, following the ADR pattern.

What are ADRs?

Architecture Decision Records (ADRs) capture important architectural decisions along with their context and consequences. They help understand why certain design choices were made.

Planned

Core Design

  • ADR-002: Interface-First Design
  • ADR-003: Default Implementations

Service Discovery

  • ADR-005: Registry Plugin Scope

Communication

  • ADR-006: HTTP as Default Transport
  • ADR-007: Content-Type Based Codecs

Configuration

  • ADR-008: Environment Variable Support

Status Values

  • Proposed: Under consideration
  • Accepted: Decision approved
  • Deprecated: No longer recommended
  • Superseded: Replaced by another ADR

Contributing

To propose a new ADR:

  1. Number it sequentially (check existing ADRs)
  2. Follow the structure of existing ADRs
  3. Include: Status, Context, Decision, Consequences, Alternatives
  4. Submit a PR for discussion
  5. Update status based on review

ADRs are immutable once accepted. To change a decision, create a new ADR that supersedes the old one.

1 - Adr 001 Plugin Architecture

Microservices frameworks need to support multiple infrastructure backends (registries, brokers, transports, stores). Different teams have different preferences and existing infrastructure.

Status

Accepted

Context

Microservices frameworks need to support multiple infrastructure backends (registries, brokers, transports, stores). Different teams have different preferences and existing infrastructure.

Hard-coding specific implementations:

  • Limits framework adoption
  • Forces migration of existing infrastructure
  • Prevents innovation and experimentation

Decision

Go Micro uses a pluggable architecture where:

  1. Core interfaces define contracts (Registry, Broker, Transport, Store, etc.)
  2. Multiple implementations live in the same repository under interface directories
  3. Plugins are imported directly and passed via options
  4. Default implementations work without any infrastructure

Structure

go-micro/
├── registry/          # Interface definition
│   ├── registry.go
│   ├── mdns.go       # Default implementation
│   ├── consul/       # Plugin
│   ├── etcd/         # Plugin
│   └── nats/         # Plugin
├── broker/
├── transport/
└── store/

Consequences

Positive

  • No version hell: Plugins versioned with core framework
  • Discovery: Users browse available plugins in same repo
  • Consistency: All plugins follow same patterns
  • Testing: Plugins tested together
  • Zero config: Default implementations require no setup

Negative

  • Repo size: More code in one repository
  • Plugin maintenance: Core team responsible for plugin quality
  • Breaking changes: Harder to evolve individual plugins independently

Neutral

  • Plugins can be extracted to separate repos if they grow complex
  • Community can contribute plugins via PR
  • Plugin-specific issues easier to triage

Alternatives Considered

Separate Plugin Repositories

Used by go-kit and other frameworks. Rejected because:

  • Version compatibility becomes user’s problem
  • Discovery requires documentation
  • Testing integration harder
  • Splitting community

Single Implementation

Like standard net/http. Rejected because:

  • Forces infrastructure choices
  • Limits adoption
  • Can’t leverage existing infrastructure

Dynamic Plugin Loading

Using Go plugins or external processes. Rejected because:

  • Complexity for users
  • Compatibility issues
  • Performance overhead
  • Debugging difficulty
  • ADR-002: Interface-First Design (planned)
  • ADR-005: Registry Plugin Scope (planned)

2 - Adr 004 Mdns Default Registry

Service discovery is critical for microservices. Common approaches:

Status

Accepted

Context

Service discovery is critical for microservices. Common approaches:

  1. Central registry (Consul, Etcd) - Requires infrastructure
  2. DNS-based (Kubernetes DNS) - Platform-specific
  3. Static configuration - Doesn’t scale
  4. Multicast DNS (mDNS) - Zero-config, local network

For local development and getting started, requiring infrastructure setup is a barrier. Production deployments typically have existing service discovery infrastructure.

Decision

Use mDNS as the default registry for service discovery.

  • Works immediately on local networks
  • No external dependencies
  • Suitable for development and simple deployments
  • Easily swapped for production registries (Consul, Etcd, Kubernetes)

Implementation

// Default - uses mDNS automatically
svc := micro.NewService("myservice")

// Production - swap to Consul
reg := consul.NewConsulRegistry()
svc := micro.NewService("myservice",
    micro.Registry(reg),
)

Consequences

Positive

  • Zero setup: go run main.go just works
  • Fast iteration: No infrastructure for local dev
  • Learning curve: Newcomers start immediately
  • Progressive complexity: Add infrastructure as needed

Negative

  • Local network only: mDNS doesn’t cross subnets/VLANs
  • Not for production: Needs proper registry in production
  • Port 5353: May conflict with existing mDNS services
  • Discovery delay: Can take 1-2 seconds

Mitigations

  • Clear documentation on production alternatives
  • Environment variables for easy swapping (MICRO_REGISTRY=consul)
  • Examples for all major registries
  • Health checks and readiness probes for production

Use Cases

Good for mDNS

  • Local development
  • Testing
  • Simple internal services on same network
  • Learning and prototyping

Need Production Registry

  • Cross-datacenter communication
  • Cloud deployments
  • Large service mesh (100+ services)
  • Require advanced features (health checks, metadata filtering)

Alternatives Considered

No Default (Force Configuration)

Rejected because:

  • Poor first-run experience
  • Increases barrier to entry
  • Users must setup infrastructure before trying framework

Static Configuration

Rejected because:

  • Doesn’t support dynamic service discovery
  • Manual configuration doesn’t scale
  • Doesn’t reflect real microservices usage

Consul as Default

Rejected because:

  • Requires running Consul for “Hello World”
  • Platform-specific
  • Adds complexity for beginners

Migration Path

Start with mDNS, migrate to production registry:

# Development
go run main.go

# Staging
MICRO_REGISTRY=consul MICRO_REGISTRY_ADDRESS=consul:8500 go run main.go

# Production (Kubernetes)
MICRO_REGISTRY=nats MICRO_REGISTRY_ADDRESS=nats://nats:4222 ./service

3 - Adr 009 Progressive Configuration

Microservices frameworks face a paradox:

Status

Accepted

Context

Microservices frameworks face a paradox:

  • Beginners want “Hello World” to work immediately
  • Production needs sophisticated configuration

Too simple: Framework is toy, not production-ready Too complex: High barrier to entry, discourages adoption

Decision

Implement progressive configuration where:

  1. Zero config works for development
  2. Environment variables provide simple overrides
  3. Code-based options enable fine-grained control
  4. Defaults are production-aware but not production-ready

Levels of Configuration

Level 1: Zero Config (Development)

svc := micro.NewService("hello")
svc.Run()

Uses defaults:

  • mDNS registry (local)
  • HTTP transport
  • Random available port
  • Memory broker/store

Level 2: Environment Variables (Staging)

MICRO_REGISTRY=consul \
MICRO_REGISTRY_ADDRESS=consul:8500 \
MICRO_BROKER=nats \
MICRO_BROKER_ADDRESS=nats://nats:4222 \
./service

No code changes, works with CLI flags.

Level 3: Code Options (Production)

reg := consul.NewConsulRegistry(
    registry.Addrs("consul1:8500", "consul2:8500"),
    registry.TLSConfig(tlsConf),
)

b := nats.NewNatsBroker(
    broker.Addrs("nats://nats1:4222", "nats://nats2:4222"),
    nats.DrainConnection(),
)

svc := micro.NewService("myservice",
    micro.Version("1.2.3"),
    micro.Registry(reg),
    micro.Broker(b),
    micro.Address(":8080"),
)

Full control over initialization and configuration.

Level 4: External Config (Enterprise)

cfg := config.NewConfig(
    config.Source(file.NewSource("config.yaml")),
    config.Source(env.NewSource()),
    config.Source(vault.NewSource()),
)

// Use cfg to initialize plugins with complex configs

Environment Variable Patterns

Standard vars for all plugins:

MICRO_REGISTRY=<type>              # consul, etcd, nats, mdns
MICRO_REGISTRY_ADDRESS=<addrs>     # Comma-separated
MICRO_BROKER=<type>
MICRO_BROKER_ADDRESS=<addrs>
MICRO_TRANSPORT=<type>
MICRO_TRANSPORT_ADDRESS=<addrs>
MICRO_STORE=<type>
MICRO_STORE_ADDRESS=<addrs>
MICRO_STORE_DATABASE=<name>
MICRO_STORE_TABLE=<name>

Plugin-specific vars:

ETCD_USERNAME=user
ETCD_PASSWORD=pass
CONSUL_TOKEN=secret

Consequences

Positive

  • Fast start: Beginners productive immediately
  • Easy deployment: Env vars for different environments
  • Power when needed: Full programmatic control available
  • Learn incrementally: Complexity introduced as required

Negative

  • Three config sources: Environment, code, and CLI flags can conflict
  • Documentation: Must explain all levels clearly
  • Testing: Need to test all configuration methods

Mitigations

  • Clear precedence: Code options > Environment > Defaults
  • Comprehensive examples for each level
  • Validation and helpful error messages

Validation Example

func (s *service) Init() error {
    if s.opts.Name == "" {
        return errors.New("service name required")
    }
    
    // Warn about development defaults in production
    if isProduction() && usingDefaults() {
        log.Warn("Using development defaults in production")
    }
    
    return nil
}

4 - Adr 010 Unified Gateway

Authors: Go Micro Team

Status: Accepted Date: 2026-02-11 Authors: Go Micro Team

Context

Previously, the go-micro CLI had two separate gateway implementations:

  1. micro run gateway (cmd/micro/run/gateway/) - Simple HTTP-to-RPC proxy for development
  2. micro server gateway (cmd/micro/server/) - Production gateway with authentication, web UI, and API documentation

This duplication created several problems:

  • Code maintenance: Gateway logic (HTTP-to-RPC translation, service discovery, health checks) was implemented twice
  • Feature parity: Improvements to one gateway didn’t automatically benefit the other
  • Complexity: New features (like MCP integration) would need to be implemented twice
  • Testing burden: Each gateway required separate testing

Decision

We unified the gateway implementation by:

  1. Extracting reusable gateway module (cmd/micro/server/gateway.go):

    • GatewayOptions struct for configuration
    • StartGateway() function that returns a *Gateway immediately
    • RunGateway() function that blocks until shutdown
    • Configurable authentication (enabled/disabled)
  2. Refactoring micro server:

    • Gateway logic remains in cmd/micro/server/
    • registerHandlers() now uses instance-specific *http.ServeMux instead of global mux
    • Authentication middleware is conditional based on GatewayOptions.AuthEnabled
    • Auth routes only register when authentication is enabled
  3. Updating micro run:

    • Removed duplicate gateway implementation (cmd/micro/run/gateway/)
    • Now calls server.StartGateway() with AuthEnabled: true
    • Retains process management and hot reload functionality
    • Same auth, scopes, and token management as micro server

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Unified Gateway                         │
│              (cmd/micro/server/gateway.go)                  │
│                                                             │
│  • HTTP → RPC translation                                  │
│  • Service discovery via registry                          │
│  • Web UI (dashboard, logs, API docs)                      │
│  • Health checks                                           │
│  • Configurable authentication                             │
│  • Endpoint scopes for access control                      │
│  • MCP tool integration with scope enforcement             │
└─────────────────────────────────────────────────────────────┘
           ▲                              ▲
           │                              │
    ┌──────┴──────┐              ┌────────┴────────┐
    │  micro run  │              │  micro server   │
    │             │              │                 │
    │  + Process  │              │  + Auth enabled │
    │    mgmt     │              │  + JWT tokens   │
    │  + Hot      │              │  + Scopes       │
    │    reload   │              │  + Production   │
    │  + Auth     │              │                 │
    │  + Scopes   │              │                 │
    └─────────────┘              └─────────────────┘

Usage

Development Mode (micro run)

# Start services with gateway (auth enabled, default admin/micro)
micro run

# Gateway provides:
# - HTTP API at /api/{service}/{endpoint}
# - Web dashboard at /
# - JWT authentication (admin/micro default)
# - Endpoint scopes at /auth/scopes

Production Mode (micro server)

# Start gateway with authentication
micro server --address :8080

# Gateway provides:
# - HTTP API at /api/{service}/{endpoint} (auth required)
# - Web dashboard with login
# - JWT-based authentication
# - User/token management UI
# - Endpoint scopes at /auth/scopes

Benefits

  1. Single Source of Truth: Gateway logic lives in one place
  2. Automatic Feature Propagation: New features (like MCP) added to the unified gateway benefit both commands
  3. Simplified Testing: Test gateway once, works everywhere
  4. Reduced Code Size: Eliminated ~300 lines of duplicate code
  5. Clear Separation:
    • micro server = API gateway (HTTP + future MCP)
    • micro run = Development tool (gateway + process management + hot reload)

Implementation Details

GatewayOptions

type GatewayOptions struct {
    Address     string        // Listen address (e.g., ":8080")
    AuthEnabled bool          // Enable JWT authentication
    Store       store.Store   // Storage for auth data
    Context     context.Context // Cancellation context
}

Starting the Gateway

// Non-blocking start
gw, err := server.StartGateway(server.GatewayOptions{
    Address:     ":8080",
    AuthEnabled: false,
})

// Blocking start
err := server.RunGateway(server.GatewayOptions{
    Address:     ":8080",
    AuthEnabled: true,
})

Authentication

When AuthEnabled: true:

  • Auth middleware checks JWT tokens on all requests
  • Auth routes are registered: /auth/login, /auth/logout, /auth/tokens, /auth/users
  • Web UI requires login
  • API endpoints require Authorization: Bearer <token> header

When AuthEnabled: false (dev mode):

  • No authentication middleware
  • Auth routes are not registered
  • All endpoints are publicly accessible

Consequences

Positive

  • Easier to add new features (only implement once)
  • Better code maintainability
  • Consistent behavior between development and production
  • Foundation for MCP integration

Negative

  • cmd/micro/run now depends on cmd/micro/server (acceptable for CLI tools)
  • Slightly more complex initialization in micro run (but cleaner overall)

Future Work

With unified gateway architecture, we can now add:

  1. MCP Integration: Add mcp.go to server package, both commands get MCP support
  2. GraphQL API: Single implementation serves both dev and prod
  3. gRPC Gateway: Expose services via gRPC alongside HTTP
  4. API Versioning: Consistent versioning strategy across all deployments

References

  • Original issue: Gateway duplication between micro run and micro server
  • Implementation: PR #XXX (gateway unification)
  • Related: ADR-001 (Plugin Architecture), ADR-009 (Progressive Configuration)