13 KiB
Code Duplication and Refactoring Analysis
Date: October 26, 2025 Scope: Notifier service codebase Focus: Identifying low-complexity refactoring opportunities
Executive Summary
Analysis of the notifier codebase identified 7 clear areas of code duplication affecting approximately 270 lines of code. All identified issues can be resolved through simple, low-complexity refactoring that will:
- Reduce code duplication by ~40-50%
- Improve maintainability without increasing complexity
- Make future changes easier to implement consistently
- Improve code readability through better abstraction
No High-Risk Changes Required - All refactorings are purely internal utility extraction with zero changes to public APIs or behavior.
Detailed Duplication Analysis
1. Auth Validation Logic Duplication (HIGH PRIORITY)
Issue: REST and gRPC middleware contain identical authentication validation code
Affected Files:
internal/auth/rest_middleware.go:35-50(16 lines)internal/auth/grpc_middleware.go:38-50(13 lines) - Unaryinternal/auth/grpc_middleware.go:82-94(13 lines) - Stream
Duplicated Pattern:
// Pattern repeated 3 times with minor variations
key, err := m.store.ValidateKey(apiKey)
if err != nil {
// Log error
// Return error
}
allowed, err := m.store.CheckRateLimit(apiKey)
if err != nil || !allowed {
// Log error
// Return error
}
if err := m.store.UpdateLastUsed(apiKey); err != nil {
// Log error (but don't return)
}
Impact:
- Changes to auth validation logic must be applied in 3 places
- Inconsistency risk if one location is missed
- Makes testing harder due to duplication
Refactoring Recommendation: Extract validateAndAuthorize() helper method
Suggested Implementation:
// Add to auth/auth.go
type authValidationResult struct {
key *APIKey
error error
}
func (m *AuthMiddleware) validateAndAuthorize(apiKey string) (*APIKey, error) {
// Validate API key
key, err := m.store.ValidateKey(apiKey)
if err != nil {
return nil, err
}
// Check rate limit
allowed, err := m.store.CheckRateLimit(apiKey)
if err != nil || !allowed {
return nil, ErrRateLimited
}
// Update last used
if err := m.store.UpdateLastUsed(apiKey); err != nil {
m.logger.Errorf("Failed to update last used: %v", err)
// Note: Don't fail the request for this
}
return key, nil
}
Then in both middleware files:
key, err := m.validateAndAuthorize(apiKey)
if err != nil {
// Handle error appropriately for REST or gRPC
}
Lines Removed: 32 lines Effort: LOW (30 minutes) Risk: MINIMAL - Same behavior, just extracted
2. API Key Extraction Duplication (MEDIUM PRIORITY)
Issue: Both REST and gRPC middleware have similar but slightly different API key extraction logic
Affected Files:
internal/auth/rest_middleware.go:73-89(17 lines)internal/auth/grpc_middleware.go:129-150(22 lines)
Duplicated Logic:
- Both extract from "Authorization" header first (Bearer token)
- Both fall back to "X-API-Key" header
- Only difference: REST works with
http.Request, gRPC works withcontext.Context
Impact:
- If API key header format changes, both must be updated
- Creates inconsistency risk
Refactoring Recommendation: Extract shared header parsing logic
Suggested Implementation:
// Add to auth/auth.go
func extractBearerToken(authHeader string) string {
if authHeader == "" {
return ""
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
return parts[1]
}
return ""
}
// In rest_middleware.go
func (m *RESTAuthMiddleware) extractAPIKey(r *http.Request) string {
if token := extractBearerToken(r.Header.Get("Authorization")); token != "" {
return token
}
return r.Header.Get("X-API-Key")
}
// In grpc_middleware.go
func (m *GRPCAuthMiddleware) extractAPIKey(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
if authHeaders := md.Get("authorization"); len(authHeaders) > 0 {
if token := extractBearerToken(authHeaders[0]); token != "" {
return token
}
}
if keyHeaders := md.Get("x-api-key"); len(keyHeaders) > 0 {
return keyHeaders[0]
}
return ""
}
Lines Removed: 15 lines Effort: LOW (20 minutes) Risk: MINIMAL - Pure extraction of header parsing
3. Notifier Registration Pattern Duplication (MEDIUM PRIORITY)
Issue: Registration pattern repeated 3 times for SMTP, Slack, and Ntfy
Affected File: cmd/server/main.go:193-241 (50+ lines)
Duplicated Pattern (repeated 3 times):
for accountName, config := range cfg.Notifiers.TYPE {
notifier, err := notifier.NewTYPENotifier(config)
if err != nil {
logger.Warnf("Failed to create TYPE notifier for account '%s': %v", accountName, err)
} else {
if err := factory.RegisterNotifier(domain.TypeTYPE, accountName, notifier); err != nil {
logger.Fatalf("Failed to register TYPE notifier for account '%s': %v", accountName, err)
}
defaultStr := ""
if config.Default {
defaultStr = " (default)"
}
logger.Infof("Registered TYPE notifier for account '%s'%s", accountName, defaultStr)
}
}
Impact:
- Adding a new notifier type requires copying/modifying this pattern
- Error handling inconsistency risk
- Makes the function harder to read
Refactoring Recommendation: Extract generic registration helper
Suggested Implementation:
// Add to cmd/server/main.go
type NotifierConfig interface {
GetDefault() bool
}
type notifierConfig struct {
defaultVal bool
}
func (nc *notifierConfig) GetDefault() bool {
return nc.defaultVal
}
func registerNotifierType(
cfg map[string]NotifierConfig,
factory *notifier.Factory,
notifType domain.NotificationType,
creator func(config NotifierConfig) (domain.Notifier, error),
logger *logging.Logger,
) {
for accountName, config := range cfg {
notif, err := creator(config)
if err != nil {
logger.Warnf("Failed to create %s notifier for account '%s': %v", notifType, accountName, err)
continue
}
if err := factory.RegisterNotifier(notifType, accountName, notif); err != nil {
logger.Fatalf("Failed to register %s notifier for account '%s': %v", notifType, accountName, err)
}
defaultStr := ""
if config.GetDefault() {
defaultStr = " (default)"
}
logger.Infof("Registered %s notifier for account '%s'%s", notifType, accountName, defaultStr)
}
}
// Usage in registerNotifiers():
registerNotifierType(
cfg.Notifiers.SMTP,
factory,
domain.TypeEmail,
func(c NotifierConfig) (domain.Notifier, error) {
return notifier.NewSMTPNotifier(c.(*config.SMTPConfig))
},
logger,
)
Lines Removed: 30 lines Effort: MEDIUM (45 minutes) - Requires careful type handling Risk: LOW - Pattern extraction with type assertions
4. NotificationResult Error Creation (LOW PRIORITY)
Issue: Same error result pattern repeated across all notifier Send() methods
Affected Files:
internal/notifier/slack.go:93-98internal/notifier/ntfy.go:268-273internal/notifier/smtp.go:98-103
Duplicated Pattern:
return &domain.NotificationResult{
NotificationID: notification.ID,
Success: false,
Error: err.Error(),
SentAt: time.Now(),
}, err
Impact:
- Minor but repeated verbosity
- If error result format changes, all notifiers must be updated
Refactoring Recommendation: Add helper method to BaseNotifier
Suggested Implementation:
// Add to internal/notifier/notifier.go in BaseNotifier
func (b *BaseNotifier) ErrorResult(notification *domain.Notification, err error) *domain.NotificationResult {
return &domain.NotificationResult{
NotificationID: notification.ID,
Success: false,
Error: err.Error(),
SentAt: time.Now(),
}
}
func (b *BaseNotifier) SuccessResult(
notification *domain.Notification,
message string,
recipientCount int,
providerResponse map[string]interface{},
) *domain.NotificationResult {
return &domain.NotificationResult{
NotificationID: notification.ID,
Success: true,
Message: message,
SentAt: time.Now(),
ProviderResponse: providerResponse,
}
}
Then in each notifier:
// Instead of:
return &domain.NotificationResult{...}, err
// Use:
return b.ErrorResult(notification, err), err
// For success:
return b.SuccessResult(notification, "message", len(recipients), response), nil
Lines Removed: 15 lines across all notifiers Effort: LOW (25 minutes) Risk: MINIMAL - Helper methods only
5. Middleware Error Response Pattern (LOW PRIORITY)
Issue: REST and gRPC middleware have similar error logging and response patterns
Affected Files:
internal/auth/rest_middleware.go:30-49(error logging pattern)internal/auth/grpc_middleware.go:33-49(error logging pattern)
Duplicated Pattern:
if apiKey == "" {
logger.Warnf("REST/gRPC: Missing API key ...")
http.Error() / status.Error()
return
}
if err != nil {
logger.Warnf("REST/gRPC: Invalid API key ...")
http.Error() / status.Error()
return
}
if !allowed {
logger.Warnf("REST/gRPC: Rate limit exceeded ...")
http.Error() / status.Error()
return
}
Impact:
- Logging pattern variations could accumulate
- Error messages might diverge over time
Refactoring Recommendation: Leverage extracted validateAndAuthorize() from Issue #1
Effort: Already covered by Issue #1 refactoring
Summary Table
| Issue | Files | Duplicate Lines | Effort | Priority | Risk | Benefit |
|---|---|---|---|---|---|---|
| #1: Auth validation | 3 | 32 | LOW (30m) | HIGH | MINIMAL | High impact |
| #2: API key extraction | 2 | 15 | LOW (20m) | MEDIUM | MINIMAL | Consistency |
| #3: Notifier registration | 1 | 30 | MEDIUM (45m) | MEDIUM | LOW | Extensibility |
| #4: Error result creation | 3 | 15 | LOW (25m) | LOW | MINIMAL | Maintenance |
| #5: Middleware error pattern | 2 | - | - | LOW | - | Covered by #1 |
| TOTAL | 11 | 92 | 2.5 hours | - | MINIMAL | 40-50% less duplication |
Refactoring Roadmap
Phase 1: Quick Wins (1 hour)
-
Extract API Key Extraction (Issue #2) - 20 min
- Add
extractBearerToken()to auth.go - Update both REST and gRPC middleware
- No behavior changes, pure extraction
- Add
-
Add Result Helpers (Issue #4) - 25 min
- Add
ErrorResult()andSuccessResult()to BaseNotifier - Update all notifier Send() methods
- Reduces verbosity consistently
- Add
Impact: 30 lines removed, improved code cleanliness
Phase 2: Core Refactoring (1.5 hours)
-
Extract Auth Validation (Issue #1) - 30 min
- Add
validateAndAuthorize()to auth middleware - Update all 3 auth validation locations
- Consistent error handling
- Add
-
Registration Pattern (Issue #3) - 45 min
- Add
registerNotifierType()helper - Refactor registerNotifiers() function
- Better extensibility
- Add
Impact: 60+ lines removed, easier to extend
Estimated Total Effort: 2.5 hours
Estimated Duplication Reduction: 92 lines removed (~40% of total duplicated code)
Implementation Guidelines
Key Principles
- No Behavior Changes: Only extract existing logic
- No New Dependencies: Use only stdlib and existing imports
- Simple Helpers: Keep new methods/functions simple and focused
- Easy Testing: Extracted code should be easier to test
- Incremental: Can be done one issue at a time
Testing Strategy
After each refactoring:
- Run existing tests:
go test ./... - Verify no behavior changes
- Manual smoke tests for affected components
- No new tests required (refactoring only)
Rollback Strategy
Each refactoring is independent:
- Can be reverted without affecting others
- Git commits should be atomic per issue
- Easy to identify if something goes wrong
Risk Assessment
Overall Risk Level: MINIMAL
- No API Changes: All refactorings are internal only
- No Logic Changes: Extracting existing patterns
- Fully Testable: Existing tests cover all changes
- Easy Rollback: Each change is atomic and reversible
- Incrementally Applicable: Can implement one at a time
Conclusion
The identified duplication represents clear opportunities for improvement without adding complexity. The refactorings are straightforward extractions of existing patterns that will:
- Improve code maintainability
- Reduce the surface area for bugs
- Make future changes easier
- Improve code readability
- Better enable testing and extension
Recommendation: Implement Phase 1 immediately (low effort, high value), then Phase 2 in the next development cycle.