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:
- Number it sequentially (check existing ADRs)
- Follow the structure of existing ADRs
- Include: Status, Context, Decision, Consequences, Alternatives
- Submit a PR for discussion
- 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:
- Core interfaces define contracts (Registry, Broker, Transport, Store, etc.)
- Multiple implementations live in the same repository under interface directories
- Plugins are imported directly and passed via options
- 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:
- Central registry (Consul, Etcd) - Requires infrastructure
- DNS-based (Kubernetes DNS) - Platform-specific
- Static configuration - Doesn’t scale
- 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:
- Zero config works for development
- Environment variables provide simple overrides
- Code-based options enable fine-grained control
- 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:
micro run gateway (cmd/micro/run/gateway/) - Simple HTTP-to-RPC proxy for developmentmicro 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:
Extracting reusable gateway module (cmd/micro/server/gateway.go):
GatewayOptions struct for configurationStartGateway() function that returns a *Gateway immediatelyRunGateway() function that blocks until shutdown- Configurable authentication (enabled/disabled)
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
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
- Single Source of Truth: Gateway logic lives in one place
- Automatic Feature Propagation: New features (like MCP) added to the unified gateway benefit both commands
- Simplified Testing: Test gateway once, works everywhere
- Reduced Code Size: Eliminated ~300 lines of duplicate code
- 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:
- MCP Integration: Add
mcp.go to server package, both commands get MCP support - GraphQL API: Single implementation serves both dev and prod
- gRPC Gateway: Expose services via gRPC alongside HTTP
- 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)