Decorators
Decorators wrap services to add behavior like logging, caching, or metrics without changing the original service.
Basic Example
// Original service
type UserService interface {
GetUser(id string) (*User, error)
}
type userService struct {
db Database
}
func NewUserService(db Database) UserService {
return &userService{db: db}
}
// Logging decorator
func LoggingDecorator(service UserService, logger Logger) UserService {
return &loggingUserService{
inner: service,
logger: logger,
}
}
type loggingUserService struct {
inner UserService
logger Logger
}
func (s *loggingUserService) GetUser(id string) (*User, error) {
s.logger.Info("Getting user", "id", id)
user, err := s.inner.GetUser(id)
if err != nil {
s.logger.Error("Failed to get user", "id", id, "error", err)
return nil, err
}
s.logger.Info("Got user", "id", id, "name", user.Name)
return user, nil
}
// Register with decorator
var UserModule = godi.NewModule("user",
godi.AddScoped(NewUserService),
godi.AddDecorator(LoggingDecorator),
)
Common Decorators
Caching Decorator
func CachingDecorator(service ProductService, cache Cache) ProductService {
return &cachingService{
inner: service,
cache: cache,
ttl: 5 * time.Minute,
}
}
type cachingService struct {
inner ProductService
cache Cache
ttl time.Duration
}
func (s *cachingService) GetProduct(id string) (*Product, error) {
// Check cache first
if cached, ok := s.cache.Get(id); ok {
return cached.(*Product), nil
}
// Get from inner service
product, err := s.inner.GetProduct(id)
if err != nil {
return nil, err
}
// Cache for next time
s.cache.Set(id, product, s.ttl)
return product, nil
}
Metrics Decorator
func MetricsDecorator(service OrderService, metrics Metrics) OrderService {
return &metricsService{
inner: service,
metrics: metrics,
}
}
type metricsService struct {
inner OrderService
metrics Metrics
}
func (s *metricsService) CreateOrder(order *Order) error {
start := time.Now()
err := s.inner.CreateOrder(order)
duration := time.Since(start)
s.metrics.RecordDuration("order.create", duration)
if err != nil {
s.metrics.IncrementCounter("order.create.error")
} else {
s.metrics.IncrementCounter("order.create.success")
}
return err
}
Retry Decorator
func RetryDecorator(service PaymentService, maxAttempts int) PaymentService {
return &retryService{
inner: service,
maxAttempts: maxAttempts,
}
}
type retryService struct {
inner PaymentService
maxAttempts int
}
func (s *retryService) ProcessPayment(payment *Payment) error {
var err error
for attempt := 1; attempt <= s.maxAttempts; attempt++ {
err = s.inner.ProcessPayment(payment)
if err == nil {
return nil // Success!
}
if attempt < s.maxAttempts {
time.Sleep(time.Duration(attempt) * time.Second)
}
}
return fmt.Errorf("failed after %d attempts: %w", s.maxAttempts, err)
}
Chaining Decorators
Decorators are applied in order:
var PaymentModule = godi.NewModule("payment",
godi.AddScoped(NewPaymentService),
// Applied in this order:
godi.AddDecorator(ValidationDecorator), // 1. Validates input
godi.AddDecorator(LoggingDecorator), // 2. Logs the request
godi.AddDecorator(MetricsDecorator), // 3. Records metrics
godi.AddDecorator(RetryDecorator), // 4. Retries on failure
)
// Result: Retry(Metrics(Logging(Validation(PaymentService))))
When to Use Decorators
Use decorators for cross-cutting concerns:
✅ Logging
✅ Metrics/Monitoring
✅ Caching
✅ Retry logic
✅ Rate limiting
✅ Authorization
✅ Validation
Don’t use decorators for:
❌ Business logic (put in service)
❌ Data transformation (use separate layer)
❌ Complex workflows (use patterns)
Best Practices
Keep decorators focused - One concern per decorator
Maintain the interface - Decorator must return same type
Make decorators optional - Service should work without them
Order matters - Think about decoration sequence
Testing with Decorators
func TestUserServiceWithLogging(t *testing.T) {
// Create mock logger to verify behavior
mockLogger := &MockLogger{}
testModule := godi.NewModule("test",
godi.AddSingleton(func() Database { return &MockDB{} }),
godi.AddSingleton(func() Logger { return mockLogger }),
godi.AddScoped(NewUserService),
godi.AddDecorator(LoggingDecorator),
)
// ... test and verify mockLogger was called
}
Decorators are powerful for adding behavior without modifying your services. Use them wisely!