Design principles¶
Overview¶
muster's architecture is built on a foundation of proven design principles that enable scalability, maintainability, and testability. These principles guide all architectural decisions and development practices within the project.
Core Architectural Principles¶
1. API Service Locator Pattern¶
Principle Statement¶
All inter-package communication MUST go through the central API layer. This is the foundational architectural principle that governs the entire system design.
Problem Statement¶
Modern software systems face the challenge of coordinating multiple complex components while maintaining: - Loose coupling for independent development - Easy testing with dependency injection - Clear interfaces for component communication - Prevention of circular dependencies - Ability to evolve components independently
Solution: Central API Registry¶
The internal/api package acts as a service locator that manages all inter-component communication through:
- Interface-driven design: All communication through well-defined contracts
- Service registration: Components register their capabilities with the API
- Service discovery: Components retrieve dependencies through the API
- Dependency inversion: High-level policy doesn't depend on low-level details
Implementation Pattern¶
Step 1: Define Interface in API
// internal/api/handlers.go
type ServiceHandler interface {
CreateService(ctx context.Context, req CreateServiceRequest) (*Service, error)
GetService(ctx context.Context, name string) (*Service, error)
StartService(ctx context.Context, name string) error
StopService(ctx context.Context, name string) error
}
Step 2: Implement Adapter in Service Package
// internal/services/api_adapter.go
type Adapter struct {
registry *Registry
logger *slog.Logger
}
func (a *Adapter) CreateService(ctx context.Context, req CreateServiceRequest) (*Service, error) {
service, err := a.registry.CreateService(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create service: %w", err)
}
return service, nil
}
func (a *Adapter) GetService(ctx context.Context, name string) (*Service, error) {
return a.registry.GetService(ctx, name)
}
Step 3: Register with API
// internal/services/api_adapter.go
func (a *Adapter) Register() {
api.RegisterServiceHandler(a)
}
// internal/api/service.go
var serviceHandler ServiceHandler
func RegisterServiceHandler(handler ServiceHandler) {
serviceHandler = handler
}
func GetServiceHandler() ServiceHandler {
return serviceHandler
}
Step 4: Consume via API
// internal/workflow/executor.go
func (e *Executor) startService(ctx context.Context, name string) error {
handler := api.GetServiceHandler()
if handler == nil {
return fmt.Errorf("service handler not available")
}
return handler.StartService(ctx, name)
}
Benefits¶
- Independent Development: Teams can develop components without knowledge of other implementations
- Testing Simplicity: Easy mocking and dependency injection for unit tests
- Clear Boundaries: Well-defined system boundaries and responsibilities
- Evolutionary Architecture: Components can be replaced without affecting others
- Circular Dependency Prevention: One-way dependency flow prevents architectural rot
Trade-offs¶
- Additional Indirection: Extra layer between components adds complexity
- Learning Curve: Developers must understand the pattern to contribute effectively
- Boilerplate Code: More code required for simple component interactions
- Discipline Required: Pattern must be consistently applied across the codebase
2. One-Way Dependency Rule¶
Principle Statement¶
All packages can depend on internal/api, but internal/api depends on NO other internal package.
Architectural Constraint¶
This creates a clean layered architecture:
┌─────────────────────────────────────────┐
│ Application Layer (cmd/) │
│ ├─ Depends on: internal/api, internal/app │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Service Layer (internal/*) │
│ ├─ aggregator/ → depends on api │
│ ├─ services/ → depends on api │
│ ├─ workflow/ → depends on api │
│ ├─ mcpserver/ → depends on api │
│ └─ app/ → depends on api │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ API Layer (internal/api) │
│ ├─ Depends on: NO internal packages │
│ ├─ Provides: Interface definitions │
│ └─ Manages: Service registry │
└─────────────────────────────────────────┘
Benefits¶
- Prevents Circular Dependencies: Impossible to create import cycles
- Clear Layering: Obvious architectural layers with defined responsibilities
- Independent Testing: API layer can be tested without any service implementations
- Modular Design: Services can be added/removed without affecting the API layer
Implementation Guidelines¶
- API Package Constraints: Never import from
internal/aggregator,internal/services, etc. - Service Package Guidelines: Always access other services through
internal/api - Dependency Direction: Dependencies always point toward the API layer
3. Progressive Enhancement Architecture¶
Principle Statement¶
Start with simple, working solutions and add sophistication incrementally while maintaining backward compatibility.
Implementation Strategy¶
Layer 1: Basic Functionality - Simple MCP server aggregation - Basic service lifecycle management - Linear workflow execution
Layer 2: Enhanced Capabilities - Intelligent tool discovery and filtering - Service dependency management - Parallel workflow execution
Layer 3: Advanced Features - Dynamic service scaling - Complex workflow orchestration - Advanced monitoring and observability
Benefits¶
- Reduced Complexity: Start simple and add complexity only when needed
- User Adoption: Users can adopt incrementally without overwhelming complexity
- Risk Mitigation: Each layer can be validated before adding the next
- Maintainability: Simpler foundations are easier to maintain and debug
4. Configuration as Code¶
Principle Statement¶
All system configuration should be declarative, version-controlled, and validated.
Implementation Approaches¶
Kubernetes-Native Configuration (Production)
apiVersion: muster.giantswarm.io/v1alpha1
kind: Workflow
metadata:
name: deploy-prometheus
spec:
description: "Prometheus deployment workflow"
args:
port:
type: integer
default: 9090
retention:
type: string
default: "15d"
steps:
- id: start
tool: prometheus_start
args:
port: "{{ .input.port }}"
retention: "{{ .input.retention }}"
Filesystem Configuration (Development)
# ~/.config/muster/workflows/deploy-prometheus.yaml
apiVersion: muster.giantswarm.io/v1alpha1
kind: Workflow
metadata:
name: deploy-prometheus
namespace: default
spec:
description: "Prometheus deployment workflow"
args:
port:
type: integer
default: 9090
steps:
- id: start
tool: prometheus_start
args:
port: "{{ .input.port }}"
Benefits¶
- Version Control: All configuration changes tracked in git
- Reproducibility: Identical deployments across environments
- Validation: Schema validation prevents configuration errors
- GitOps Integration: Native integration with GitOps workflows
5. Testing-First Architecture¶
Principle Statement¶
The architecture must enable comprehensive testing at all levels, from unit tests to end-to-end scenarios.
Testing Strategy¶
Unit Testing with Mocks
func TestWorkflowExecution(t *testing.T) {
// Mock service handler
mockHandler := &MockServiceHandler{}
api.RegisterServiceHandler(mockHandler)
// Configure mock expectations
mockHandler.On("StartService", "prometheus").Return(nil)
// Test workflow execution
executor := NewExecutor()
err := executor.ExecuteWorkflow(ctx, workflow)
assert.NoError(t, err)
mockHandler.AssertExpectations(t)
}
Integration Testing with BDD Scenarios
# internal/testing/scenarios/mcpserver-lifecycle.yaml
name: "MCPServer Lifecycle Management"
description: "Test complete MCPServer lifecycle from creation to deletion"
steps:
- name: "Create MCPServer"
tool: "core_mcpserver_create"
args:
name: "test-server"
type: "stdio"
command: ["mcp-test"]
expect:
status: "success"
- name: "Verify MCPServer running"
tool: "core_mcpserver_get"
args:
name: "test-server"
expect:
status: "success"
result.state: "Running"
Benefits¶
- Confidence: Comprehensive test coverage ensures system reliability
- Regression Prevention: Automated tests catch breaking changes
- Documentation: Tests serve as executable documentation
- Refactoring Safety: Tests enable safe refactoring and architectural changes
Design Patterns and Anti-Patterns¶
✅ Recommended Patterns¶
Adapter Pattern for API Integration¶
Always use adapters to integrate services with the API layer:
type Adapter struct {
implementation *ServiceImplementation
}
func (a *Adapter) HandleRequest(ctx context.Context, req Request) Response {
return a.implementation.ProcessRequest(ctx, req)
}
Factory Pattern for Dynamic Creation¶
Use factories for creating dynamic components:
Observer Pattern for Events¶
Use observers for loose coupling of event handling:
type EventObserver interface {
OnServiceStarted(service *Service)
OnServiceStopped(service *Service)
}
❌ Anti-Patterns to Avoid¶
Direct Service Dependencies¶
Never import service packages directly:
// ❌ WRONG - Direct import
import "github.com/giantswarm/muster/v5/internal/services"
func (w *Workflow) startService() {
services.GetRegistry().StartService("prometheus")
}
// ✅ CORRECT - Through API
func (w *Workflow) startService() {
handler := api.GetServiceHandler()
handler.StartService(context.Background(), "prometheus")
}
Circular Dependencies¶
Never create circular imports:
// ❌ WRONG - services importing workflow
// internal/services/manager.go
import "github.com/giantswarm/muster/v5/internal/workflow"
// internal/workflow/executor.go
import "github.com/giantswarm/muster/v5/internal/services"
Synchronous Blocking Operations¶
Never use sleep or blocking operations in tests:
// ❌ WRONG - Non-deterministic timing
func TestAsyncOperation(t *testing.T) {
triggerAsyncOperation()
time.Sleep(100 * time.Millisecond) // Flaky!
assert.True(t, operationComplete)
}
// ✅ CORRECT - Deterministic waiting
func TestAsyncOperation(t *testing.T) {
done := make(chan bool)
onComplete := func() { done <- true }
triggerAsyncOperation(onComplete)
select {
case <-done:
assert.True(t, operationComplete)
case <-time.After(5 * time.Second):
t.Fatal("Operation timed out")
}
}
Quality Assurance Principles¶
Code Quality Standards¶
Minimum Test Coverage¶
- 80% minimum coverage for all new code
- 100% coverage for critical paths (API layer, core business logic)
- Integration test coverage for all user-facing features
Error Handling¶
Always wrap errors with context:
func (s *Service) processRequest(ctx context.Context, req Request) error {
result, err := s.backend.Process(req)
if err != nil {
return fmt.Errorf("failed to process request %s: %w", req.ID, err)
}
return nil
}
Logging Standards¶
Use structured logging with appropriate levels:
func (s *Service) startService(name string) error {
s.logger.Info("Starting service", "name", name)
err := s.doStart(name)
if err != nil {
s.logger.Error("Failed to start service", "name", name, "error", err)
return err
}
s.logger.Info("Service started successfully", "name", name)
return nil
}
Documentation Standards¶
Interface Documentation¶
Every exported interface must have comprehensive documentation:
// ServiceHandler manages the lifecycle of service instances.
//
// Services are long-running processes that provide tools to the MCP aggregator.
// The handler is responsible for creating, starting, stopping, and monitoring
// service instances.
//
// All methods are safe for concurrent use.
type ServiceHandler interface {
// CreateService creates a new service instance.
// Returns an error if creation fails.
CreateService(ctx context.Context, req CreateServiceRequest) (*Service, error)
// GetService retrieves an existing service by name.
// Returns nil if the service doesn't exist.
GetService(ctx context.Context, name string) (*Service, error)
}
Package Documentation¶
Every package must have a doc.go file explaining its purpose:
// Package services provides service instance lifecycle management for muster.
//
// This package implements the service management capabilities that allow muster
// to create, monitor, and manage long-running service processes. Services can
// be started, stopped, and queried through the ServiceHandler interface.
//
// Key concepts:
// - Service: Running instance managed by muster
// - Registry: Central repository for service instances and their state
//
// The package integrates with the central API through the adapter pattern,
// implementing the ServiceHandler interface defined in internal/api.
package services
Evolution and Maintenance¶
Architectural Decision Process¶
When making architectural decisions:
- Document the Context: What problem are we solving?
- Consider Alternatives: What are the different approaches?
- Evaluate Trade-offs: What are the costs and benefits?
- Make the Decision: Choose the approach and document rationale
- Record the Decision: Create an ADR for future reference
Refactoring Guidelines¶
Safe Refactoring Process¶
- Ensure Test Coverage: Add tests before refactoring
- Small Changes: Make incremental changes with tests after each step
- Maintain Interfaces: Keep API contracts stable during refactoring
- Update Documentation: Keep documentation synchronized with changes
When to Refactor Architecture¶
- Performance Issues: When current architecture doesn't meet performance requirements
- Scaling Challenges: When adding new features becomes difficult
- Technical Debt: When maintenance costs exceed development velocity
- New Requirements: When new requirements don't fit existing patterns
Continuous Improvement¶
Metrics for Architectural Health¶
- Dependency Violations: Monitor for API pattern violations
- Test Coverage: Maintain high test coverage across all components
- Code Complexity: Monitor and reduce cyclomatic complexity
- Documentation Currency: Ensure documentation stays up-to-date
Regular Architecture Reviews¶
- Monthly Reviews: Assess adherence to design principles
- Quarterly Planning: Evaluate architectural roadmap and priorities
- Annual Assessment: Comprehensive review of architectural decisions and outcomes
These design principles form the foundation for building a maintainable, scalable, and testable system that can evolve with changing requirements while preserving architectural integrity.