Decorators
Decorators allow you to modify or enhance services after they’re created but before they’re used. This is useful for adding cross-cutting concerns like logging, caching, metrics, or validation.
What are Decorators?
A decorator is a function that:
Takes an existing service as input
Returns a modified or wrapped version of that service
Maintains the same interface as the original service
Basic Decorator
Simple Logging Decorator
// Original service interface
type UserService interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, user *User) error
}
// Decorator function
func LoggingDecorator(service UserService, logger Logger) UserService {
return &loggingUserService{
inner: service,
logger: logger,
}
}
// Decorator implementation
type loggingUserService struct {
inner UserService
logger Logger
}
func (s *loggingUserService) GetUser(ctx context.Context, id string) (*User, error) {
s.logger.Info("GetUser called", "id", id)
start := time.Now()
user, err := s.inner.GetUser(ctx, id)
duration := time.Since(start)
if err != nil {
s.logger.Error("GetUser failed", "id", id, "error", err, "duration", duration)
return nil, err
}
s.logger.Info("GetUser succeeded", "id", id, "duration", duration)
return user, nil
}
func (s *loggingUserService) CreateUser(ctx context.Context, user *User) error {
s.logger.Info("CreateUser called", "username", user.Username)
err := s.inner.CreateUser(ctx, user)
if err != nil {
s.logger.Error("CreateUser failed", "username", user.Username, "error", err)
} else {
s.logger.Info("CreateUser succeeded", "username", user.Username)
}
return err
}
Registering Decorators
// Register the service
collection.AddScoped(NewUserService)
// Register the decorator
collection.Decorate(LoggingDecorator)
// When UserService is resolved, it will be wrapped with logging
Common Decorator Patterns
Caching Decorator
func CachingDecorator(service ProductService, cache Cache) ProductService {
return &cachingProductService{
inner: service,
cache: cache,
ttl: 5 * time.Minute,
}
}
type cachingProductService struct {
inner ProductService
cache Cache
ttl time.Duration
}
func (s *cachingProductService) GetProduct(ctx context.Context, id string) (*Product, error) {
// Try cache first
cacheKey := fmt.Sprintf("product:%s", id)
if cached, found := s.cache.Get(cacheKey); found {
return cached.(*Product), nil
}
// Cache miss - call inner service
product, err := s.inner.GetProduct(ctx, id)
if err != nil {
return nil, err
}
// Cache the result
s.cache.Set(cacheKey, product, s.ttl)
return product, nil
}
func (s *cachingProductService) ListProducts(ctx context.Context) ([]*Product, error) {
// Some methods might not be cached
return s.inner.ListProducts(ctx)
}
Metrics Decorator
func MetricsDecorator(service OrderService, metrics Metrics) OrderService {
return &metricsOrderService{
inner: service,
metrics: metrics,
}
}
type metricsOrderService struct {
inner OrderService
metrics Metrics
}
func (s *metricsOrderService) CreateOrder(ctx context.Context, order *Order) error {
timer := s.metrics.NewTimer("order.create.duration")
defer timer.ObserveDuration()
err := s.inner.CreateOrder(ctx, order)
if err != nil {
s.metrics.IncrementCounter("order.create.error")
} else {
s.metrics.IncrementCounter("order.create.success")
s.metrics.RecordValue("order.create.amount", order.Total)
}
return err
}
Retry Decorator
func RetryDecorator(service PaymentService, maxRetries int) PaymentService {
return &retryPaymentService{
inner: service,
maxRetries: maxRetries,
backoff: time.Second,
}
}
type retryPaymentService struct {
inner PaymentService
maxRetries int
backoff time.Duration
}
func (s *retryPaymentService) ProcessPayment(ctx context.Context, payment *Payment) error {
var err error
for attempt := 0; attempt <= s.maxRetries; attempt++ {
if attempt > 0 {
// Exponential backoff
time.Sleep(s.backoff * time.Duration(1<<(attempt-1)))
}
err = s.inner.ProcessPayment(ctx, payment)
// Success or non-retryable error
if err == nil || !isRetryable(err) {
return err
}
}
return fmt.Errorf("max retries exceeded: %w", err)
}
Validation Decorator
func ValidationDecorator(service UserService, validator Validator) UserService {
return &validatingUserService{
inner: service,
validator: validator,
}
}
type validatingUserService struct {
inner UserService
validator Validator
}
func (s *validatingUserService) CreateUser(ctx context.Context, user *User) error {
// Validate input
if err := s.validator.ValidateStruct(user); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Additional business rules
if len(user.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
if !strings.Contains(user.Email, "@") {
return errors.New("invalid email format")
}
return s.inner.CreateUser(ctx, user)
}
Chaining Decorators
Multiple decorators can be applied to the same service:
// Register service
collection.AddScoped(NewOrderService)
// Register multiple decorators - applied in order
collection.Decorate(ValidationDecorator) // Applied first
collection.Decorate(LoggingDecorator) // Applied second
collection.Decorate(MetricsDecorator) // Applied third
// Result: Metrics(Logging(Validation(OrderService)))
Advanced Decorator Patterns
Conditional Decorator
func ConditionalCachingDecorator(service ProductService, cache Cache, config *Config) ProductService {
// Only apply caching in production
if !config.CachingEnabled {
return service
}
return &cachingProductService{
inner: service,
cache: cache,
ttl: config.CacheTTL,
}
}
Context-Aware Decorator
func AuthorizationDecorator(service AdminService, authz AuthorizationService) AdminService {
return &authorizingAdminService{
inner: service,
authz: authz,
}
}
type authorizingAdminService struct {
inner AdminService
authz AuthorizationService
}
func (s *authorizingAdminService) DeleteUser(ctx context.Context, userID string) error {
// Extract current user from context
currentUser, ok := ctx.Value("user").(*User)
if !ok {
return errors.New("unauthorized: no user in context")
}
// Check permissions
if !s.authz.Can(currentUser, "users:delete") {
return errors.New("forbidden: insufficient permissions")
}
return s.inner.DeleteUser(ctx, userID)
}
Generic Decorator
// Generic logging decorator for any service
func LoggingDecoratorFor[T any](service T, logger Logger, serviceName string) T {
// Use reflection to create a proxy
serviceType := reflect.TypeOf(service)
if serviceType.Kind() != reflect.Interface {
panic("service must be an interface")
}
handler := &genericLoggingHandler{
inner: service,
logger: logger,
serviceName: serviceName,
}
proxy := reflect.MakeFunc(serviceType, handler.invoke)
return proxy.Interface().(T)
}
Circuit Breaker Decorator
func CircuitBreakerDecorator(service ExternalService, breaker CircuitBreaker) ExternalService {
return &circuitBreakerService{
inner: service,
breaker: breaker,
}
}
type circuitBreakerService struct {
inner ExternalService
breaker CircuitBreaker
}
func (s *circuitBreakerService) CallExternal(ctx context.Context, req *Request) (*Response, error) {
// Check circuit breaker state
if !s.breaker.AllowRequest() {
return nil, errors.New("circuit breaker open")
}
// Make the call
resp, err := s.inner.CallExternal(ctx, req)
// Record result
if err != nil {
s.breaker.RecordFailure()
} else {
s.breaker.RecordSuccess()
}
return resp, err
}
Testing Decorators
Unit Testing Decorators
func TestLoggingDecorator(t *testing.T) {
// Create a mock service
mockService := &MockUserService{
GetUserFunc: func(ctx context.Context, id string) (*User, error) {
if id == "123" {
return &User{ID: "123", Name: "Test"}, nil
}
return nil, errors.New("not found")
},
}
// Create a test logger
var logs []string
testLogger := &TestLogger{
InfoFunc: func(msg string, args ...interface{}) {
logs = append(logs, msg)
},
}
// Apply decorator
decorated := LoggingDecorator(mockService, testLogger)
// Test successful call
user, err := decorated.GetUser(context.Background(), "123")
assert.NoError(t, err)
assert.Equal(t, "Test", user.Name)
assert.Contains(t, logs, "GetUser called")
assert.Contains(t, logs, "GetUser succeeded")
// Test error call
_, err = decorated.GetUser(context.Background(), "999")
assert.Error(t, err)
assert.Contains(t, logs, "GetUser failed")
}
Integration Testing
func TestDecoratorChain(t *testing.T) {
collection := godi.NewServiceCollection()
// Register base service
collection.AddScoped(NewUserService)
// Register decorators
collection.Decorate(ValidationDecorator)
collection.Decorate(LoggingDecorator)
collection.Decorate(MetricsDecorator)
provider, _ := collection.BuildServiceProvider()
defer provider.Close()
// Resolve decorated service
service, _ := godi.Resolve[UserService](provider)
// Test that all decorators are applied
err := service.CreateUser(context.Background(), &User{
Username: "test",
Email: "invalid-email", // Should fail validation
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "validation failed")
}
Best Practices
1. Keep Decorators Focused
Each decorator should handle one concern:
// ✅ Good - single responsibility
func LoggingDecorator(service Service, logger Logger) Service
func CachingDecorator(service Service, cache Cache) Service
func MetricsDecorator(service Service, metrics Metrics) Service
// ❌ Bad - multiple concerns
func LoggingAndCachingDecorator(service Service, logger Logger, cache Cache) Service
2. Maintain Interface Compatibility
Decorators must implement the same interface:
// ✅ Good - returns same interface
func CachingDecorator(service UserService, cache Cache) UserService {
return &cachingUserService{inner: service, cache: cache}
}
// ❌ Bad - returns different type
func CachingDecorator(service UserService, cache Cache) *CachingUserService {
return &CachingUserService{inner: service, cache: cache}
}
3. Make Decorators Optional
Allow services to work without decorators:
// Service works without any decorators
collection.AddScoped(NewUserService)
// Decorators are optional additions
if config.EnableLogging {
collection.Decorate(LoggingDecorator)
}
if config.EnableCaching {
collection.Decorate(CachingDecorator)
}
4. Document Decorator Order
// Decorators are applied in registration order:
// 1. Validation - validates input
// 2. Logging - logs the validated request
// 3. Metrics - measures the entire operation
collection.Decorate(ValidationDecorator)
collection.Decorate(LoggingDecorator)
collection.Decorate(MetricsDecorator)
5. Consider Performance
// Lightweight decorators for hot paths
func SimpleMetricsDecorator(service Service, counter Counter) Service {
return &metricsService{
inner: service,
counter: counter,
}
}
// Heavier decorators for less frequent operations
func FullAuditDecorator(service Service, audit AuditLog) Service {
return &auditService{
inner: service,
audit: audit,
}
}
Common Use Cases
1. Cross-Cutting Concerns
Logging
Metrics
Tracing
Error handling
2. Security
Authentication
Authorization
Input validation
Rate limiting
3. Resilience
Retry logic
Circuit breakers
Timeouts
Fallbacks
4. Performance
Caching
Batching
Lazy loading
Connection pooling
5. Business Logic
Audit trails
Notifications
Event publishing
Data transformation
Summary
Decorators provide a powerful way to:
Add behavior without modifying original services
Apply cross-cutting concerns uniformly
Keep services focused on their core responsibility
Build flexible, composable systems
Use decorators to enhance your services while maintaining clean separation of concerns.