# Code Duplication and Refactoring Analysis - Notifier Codebase ## Executive Summary The notifier codebase shows several areas of code duplication and repeated patterns that can be refactored to improve maintainability without increasing complexity. Most duplication issues are low-to-medium complexity to address and would benefit from extraction into utility functions or helper methods. --- ## 1. REPEATED VALIDATION AND INITIALIZATION PATTERNS IN NOTIFIER IMPLEMENTATIONS ### Issue 1.1: Identical Validation Pattern in All Notifier Send Methods **File Locations:** - `/Users/igodwin/Workspace/notifier/internal/notifier/smtp.go`: Lines 63-70 - `/Users/igodwin/Workspace/notifier/internal/notifier/slack.go`: Lines 78-85 - `/Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go`: Lines 185-192 - `/Users/igodwin/Workspace/notifier/internal/notifier/stdout.go`: Lines 26-33 **Description:** Every notifier implementation repeats the same validation pattern at the start of the Send method: ```go if err := ValidateContext(ctx); err != nil { return nil, err } if err := s.Validate(notification); err != nil { return nil, err } ``` **Impact:** - Code duplication (4 instances of identical pattern) - Maintenance burden if validation requirements change - Inconsistency risk if one is updated and others aren't **Refactoring Recommendation:** Create an exported wrapper function in the `notifier` package that encapsulates this validation pattern. Could be implemented as a helper that all Send implementations call first. ```go // Example helper function func ValidateNotification(ctx context.Context, notif *domain.Notification, validator domain.Notifier) error { if err := ValidateContext(ctx); err != nil { return err } return validator.Validate(notif) } ``` **Estimated Effort:** Low (30 minutes) - Extract into 1 utility function - Update 4 notifier files - No behavioral changes needed --- ## 2. REPEATED HTTP REQUEST PATTERNS IN EXTERNAL NOTIFIERS ### Issue 2.1: Identical HTTP Request/Response Handling in Slack and Ntfy Notifiers **File Locations:** - `/Users/igodwin/Workspace/notifier/internal/notifier/slack.go`: Lines 182-211 (sendToSlack method) - `/Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go`: Lines 290-323 (sendToTopic method) **Description:** Both Slack and Ntfy notifiers have near-identical HTTP request handling: ```go jsonData, err := json.Marshal(msg/req) if err != nil { return fmt.Errorf("failed to marshal: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") // Add auth header resp, err := s.httpClient.Do(httpReq) if err != nil { return fmt.Errorf("failed to send: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("API returned status: %d", resp.StatusCode) } ``` **Impact:** - 25+ lines of duplicated code across 2 files - Difficult to maintain and update HTTP handling logic - Risk of inconsistent error handling between implementations - Makes future HTTP-related improvements (retries, timeouts, etc.) harder **Refactoring Recommendation:** Create a shared HTTP utility function for JSON POST requests with authentication support. ```go // Helper function in notifier package func sendJSONPostRequest(ctx context.Context, client *http.Client, url string, payload interface{}, headers map[string]string) error { jsonData, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") for k, v := range headers { req.Header.Set(k, v) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("API returned status: %d", resp.StatusCode) } return nil } ``` **Estimated Effort:** Low-Medium (45 minutes) - Create 1 utility function in notifier package - Refactor 2 send methods to use utility - Comprehensive testing needed --- ## 3. REPEATED FILTER MATCHING LOGIC ### Issue 3.1: Identical List Membership Check Pattern in matchesFilter **File Location:** - `/Users/igodwin/Workspace/notifier/internal/service/service.go`: Lines 481-540 **Description:** The `matchesFilter` method checks if a notification matches filters using the same pattern repeated 4 times: ```go if len(filter.IDs) > 0 { found := false for _, id := range filter.IDs { if notification.ID == id { found = true break } } if !found { return false } } // ... repeated for Types, Statuses, Recipients with nearly identical logic ``` This pattern is repeated for: - IDs (lines 481-492) - Types (lines 495-506) - Statuses (lines 509-520) - Recipients (with nested loop, lines 523-539) **Impact:** - 60+ lines with highly repetitive matching logic - Hard to maintain and extend if matching logic needs changes - Error-prone for adding new filter fields - Readability suffers due to repetitive structure **Refactoring Recommendation:** Create a generic "contains" helper function and use it for simpler cases. For recipients, create a dedicated function. ```go // Helper function func containsValue[T comparable](haystack []T, needle T) bool { for _, h := range haystack { if h == needle { return true } } return false } // In matchesFilter: if len(filter.IDs) > 0 && !containsValue(filter.IDs, notification.ID) { return false } if len(filter.Types) > 0 && !containsValue(filter.Types, notification.Type) { return false } if len(filter.Statuses) > 0 && !containsValue(filter.Statuses, notification.Status) { return false } // For recipients (more complex): if len(filter.Recipients) > 0 && !notificationHasRecipient(notification, filter.Recipients) { return false } func notificationHasRecipient(notif *domain.Notification, recipients []string) bool { for _, fr := range recipients { for _, nr := range notif.Recipients { if fr == nr { return true } } } return false } ``` **Estimated Effort:** Low (40 minutes) - Create 2 helper functions - Refactor matchesFilter method - Unit tests for helpers - Reduces 60+ lines to ~30 lines --- ## 4. REPEATED AUTHENTICATION VALIDATION LOGIC ### Issue 4.1: Duplicated API Key Validation in REST and gRPC Middleware **File Locations:** - `/Users/igodwin/Workspace/notifier/internal/auth/rest_middleware.go`: Lines 35-50 - `/Users/igodwin/Workspace/notifier/internal/auth/grpc_middleware.go`: Lines 38-50 (Unary), Lines 82-94 (Stream) **Description:** Both REST and gRPC middleware repeat the same validation sequence: ```go // Validate API key key, err := m.store.ValidateKey(apiKey) if err != nil { // log error // return auth error } // Check rate limit allowed, err := m.store.CheckRateLimit(apiKey) if err != nil || !allowed { // log error // return rate limit error } // Update last used if err := m.store.UpdateLastUsed(apiKey); err != nil { // log error } // Create auth context authCtx := &AuthContext{...} ``` This pattern appears 3 times (REST middleware once, gRPC unary once, gRPC stream once). **Impact:** - 30+ lines of duplicated validation logic across 2 files - Changes to validation logic must be made in 3 places - Inconsistency risk between REST and gRPC authentication - Testing burden increased **Refactoring Recommendation:** Extract validation into a reusable helper method in APIKeyStore. ```go type ValidatedKey struct { APIKey *APIKey ClientID string Roles []string } // In APIKeyStore: func (s *APIKeyStore) ValidateAndAuthorize(keyStr string) (*ValidatedKey, error) { // Validate key key, err := s.ValidateKey(keyStr) if err != nil { return nil, err } // Check rate limit allowed, err := s.CheckRateLimit(keyStr) if err != nil || !allowed { return nil, fmt.Errorf("rate limit exceeded") } // Update last used if err := s.UpdateLastUsed(keyStr); err != nil { // Log but don't fail return nil, err } return &ValidatedKey{ APIKey: key, ClientID: key.ClientID, Roles: key.Roles, }, nil } ``` Then in both middlewares: ```go validatedKey, err := m.store.ValidateAndAuthorize(apiKey) if err != nil { // return auth error } authCtx := &AuthContext{ APIKey: validatedKey.APIKey, ClientID: validatedKey.ClientID, Roles: validatedKey.Roles, } ``` **Estimated Effort:** Low-Medium (50 minutes) - Add helper method to APIKeyStore - Refactor 3 middleware methods - Update error handling to match - Better error semantics (different errors for different failures) --- ## 5. REPEATED NOTIFIER REGISTRATION PATTERN ### Issue 5.1: Identical Notifier Registration Logging in Main **File Location:** - `/Users/igodwin/Workspace/notifier/cmd/server/main.go`: Lines 193-241 **Description:** The `registerNotifiers` function repeats the same registration pattern 3 times (for SMTP, Slack, Ntfy): ```go for accountName, smtpConfig := range cfg.Notifiers.SMTP { notifier, err := notifier.NewSMTPNotifier(smtpConfig) if err != nil { logger.Warnf("Failed to create ... notifier for account '%s': %v", accountName, err) } else { if err := factory.RegisterNotifier(domain.TypeEmail, accountName, notifier); err != nil { logger.Fatalf("Failed to register ... notifier for account '%s': %v", accountName, err) } defaultStr := "" if smtpConfig.Default { defaultStr = " (default)" } logger.Infof("Registered ... notifier for account '%s'%s", accountName, defaultStr) } } // ... repeat for Slack ... // ... repeat for Ntfy ... ``` **Impact:** - 50+ lines with highly repetitive registration logic - Maintenance burden - any changes must be made in 3 places - Hard to add new notifier types - Inconsistent error handling between types requires careful duplication **Refactoring Recommendation:** Create a generic registration helper function. ```go type NotifierConfig interface { Default() bool } func registerNotifierType[T NotifierConfig]( cfg map[string]T, notifierType domain.NotificationType, factory *notifier.Factory, logger *logging.Logger, creator func(T) (domain.Notifier, error), creatorName string) { for accountName, config := range cfg { notif, err := creator(config) if err != nil { logger.Warnf("Failed to create %s notifier for account '%s': %v", creatorName, accountName, err) } else { if err := factory.RegisterNotifier(notifierType, accountName, notif); err != nil { logger.Fatalf("Failed to register %s notifier for account '%s': %v", creatorName, accountName, err) } defaultStr := "" if config.Default() { defaultStr = " (default)" } logger.Infof("Registered %s notifier for account '%s'%s", creatorName, accountName, defaultStr) } } } // In registerNotifiers: registerNotifierType(cfg.Notifiers.SMTP, domain.TypeEmail, factory, logger, func(c *notifier.SMTPConfig) (domain.Notifier, error) { return notifier.NewSMTPNotifier(c) }, "SMTP") ``` **Estimated Effort:** Medium (1 hour) - Refactor config structs to implement interface - Create generic registration function - Update 3 registration calls - Add type-specific naming --- ## 6. REPEATED DEFAULT ACCOUNT RESOLUTION PATTERN ### Issue 6.1: Identical Default Account Search in Config **File Location:** - `/Users/igodwin/Workspace/notifier/internal/config/config.go`: Lines 346-380 **Description:** The `GetDefaultAccount` method repeats the same search pattern 3 times: ```go switch notifierType { case domain.TypeEmail: for name, cfg := range c.Notifiers.SMTP { if cfg.Default { return name } } // Return first account if no default is set for name := range c.Notifiers.SMTP { return name } // ... repeat for TypeSlack ... // ... repeat for TypeNtfy ... } ``` **Impact:** - 35+ lines with repetitive logic - Hard to maintain - changes needed in 3 places - Error-prone for adding new notifier types - The "get default or first" pattern is repeated 3 times **Refactoring Recommendation:** Create a generic helper function for finding default in a config map. ```go // Helper function func getDefaultOrFirst[T interface{ Default() bool }](configs map[string]T) string { // First pass: find explicitly marked default for name, cfg := range configs { if cfg.Default() { return name } } // Second pass: return first if no default for name := range configs { return name } return "" } // Update config structs to implement interface: func (s *notifier.SMTPConfig) Default() bool { return s.Default } // In GetDefaultAccount: switch notifierType { case domain.TypeEmail: return getDefaultOrFirst(c.Notifiers.SMTP) case domain.TypeSlack: return getDefaultOrFirst(c.Notifiers.Slack) case domain.TypeNtfy: return getDefaultOrFirst(c.Notifiers.Ntfy) } ``` **Estimated Effort:** Low-Medium (45 minutes) - Create generic helper function - Refactor config structs to expose Default() method - Update GetDefaultAccount to use helper - Reduces 35+ lines to ~15 lines --- ## 7. REPEATED NOTIFICATIONRESULT ERROR CREATION ### Issue 7.1: Duplicated NotificationResult Creation with Error **File Locations:** - `/Users/igodwin/Workspace/notifier/internal/notifier/smtp.go`: Lines 80-86, Lines 100-105 - `/Users/igodwin/Workspace/notifier/internal/notifier/slack.go`: Lines 93-98 - `/Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go`: Lines 268-273 **Description:** All notifier implementations repeat a pattern for creating error results: ```go return &domain.NotificationResult{ NotificationID: notification.ID, Success: false, Error: err.Error(), SentAt: time.Now(), }, fmt.Errorf("...") ``` This pattern (with variations) appears 4+ times across different notifier files. **Impact:** - Duplicated error result creation - Inconsistent handling of SentAt timestamp - Risk of incomplete error results **Refactoring Recommendation:** Create a helper in the BaseNotifier for error results. ```go // In BaseNotifier: func (b *BaseNotifier) ErrorResult(notificationID, errorMsg string, err error) *domain.NotificationResult { return &domain.NotificationResult{ NotificationID: notificationID, Success: false, Error: errorMsg, SentAt: time.Now(), } } // Usage in notifiers: return b.ErrorResult(notification.ID, err.Error(), err), err ``` **Estimated Effort:** Low (30 minutes) - Add helper method to BaseNotifier - Update 4 error creation sites --- ## 8. REPEATED LOCK/UNLOCK PATTERNS IN API KEY STORE ### Issue 8.2: Similar RWMutex Lock Patterns **File Location:** - `/Users/igodwin/Workspace/notifier/internal/auth/auth.go`: Multiple methods **Description:** Multiple methods in APIKeyStore follow the same pattern: ```go s.mu.RLock() defer s.mu.RUnlock() // read-only operations ``` And for write operations: ```go s.mu.Lock() defer s.mu.Unlock() // write operations ``` While this is acceptable Go practice, it's a repeated pattern that could be standardized. **Impact:** - Not a critical issue - proper defer usage is correct - Consistent approach across codebase - Could be improved by adding higher-level thread-safe methods **Refactoring Recommendation:** This is acceptable as-is. The repetition is appropriate for the pattern. **Estimated Effort:** N/A - Not recommended for change --- ## 9. REPEATED TYPE CONVERSION FUNCTIONS IN GRPC HANDLER ### Issue 9.1: Multiple Similar Type Conversion Functions **File Location:** - `/Users/igodwin/Workspace/notifier/api/grpc/handler.go`: Lines 279-449 **Description:** Multiple conversion functions follow the same switch-case pattern: - `convertProtoTypeToDomain` (lines 279-292) - `convertDomainTypeToProto` (lines 294-307) - `convertDomainToProtoType` (lines 331-344) - **DUPLICATE of above** - `convertProtoContentTypeToDomain` (lines 309-318) - `convertDomainContentTypeToProto` (lines 320-329) - `convertDomainToProtoStatus` (lines 346-363) Lines 331-344 are a duplicate of lines 294-307! **Impact:** - Exact duplication: `convertDomainTypeToProto` and `convertDomainToProtoType` do the same thing - Code maintenance burden - Risk of inconsistency between duplicate functions - Takes up 30+ unnecessary lines **Refactoring Recommendation:** Remove the duplicate `convertDomainToProtoType` function and use `convertDomainTypeToProto` instead. Search all files for usages: - `convertDomainToProtoType` is used at line 368 in `convertDomainToProtoNotification` - Replace this single usage with `convertDomainTypeToProto` - Delete lines 331-344 ```go // REMOVE THIS (duplicate): func convertDomainToProtoType(domainType domain.NotificationType) pb.NotificationType { // ... identical to convertDomainTypeToProto ... } // USE THIS INSTEAD: func convertDomainTypeToProto(domainType domain.NotificationType) pb.NotificationType { // existing implementation } // Update line 368: - Type: convertDomainToProtoType(notif.Type), + Type: convertDomainTypeToProto(notif.Type), ``` **Estimated Effort:** Low (15 minutes) - Remove duplicate function (14 lines) - Update 1 call site - Simple find-and-replace --- ## 10. REPEATED REST RESPONSE HELPER PATTERN ### Issue 10.1: REST Response Helpers as Only Response Utility **File Location:** - `/Users/igodwin/Workspace/notifier/api/rest/handlers.go`: Lines 264-284 **Description:** The REST handler has two response helper functions that format responses: ```go func respondJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func respondError(w http.ResponseWriter, status int, message string, err error) { errMsg := message if err != nil { errMsg = message + ": " + err.Error() } respondJSON(w, status, map[string]interface{}{ "error": message, "details": errMsg, }) } ``` These are well-factored helpers already, but they're not shared with gRPC handler if it needs similar utilities. **Impact:** - REST-specific utilities (proper placement) - gRPC has different response patterns (also proper) - No duplication issue identified **Refactoring Recommendation:** Keep as-is. These are properly scoped to REST handler. **Estimated Effort:** N/A - Not recommended for change --- ## SUMMARY TABLE OF REFACTORING OPPORTUNITIES | Issue | Location | Type | Lines | Effort | Priority | Impact | |-------|----------|------|-------|--------|----------|--------| | 1.1 | Notifier Send validation | Repeated pattern | 4x 8 lines | Low | Medium | Maintainability | | 2.1 | HTTP request handling | Code duplication | 25+ lines | Low-Med | High | Maintainability + Testing | | 3.1 | Filter matching logic | Repeated pattern | 60+ lines | Low | High | Readability + Extensibility | | 4.1 | Auth validation | Code duplication | 30+ lines | Low-Med | Medium | Maintainability + Consistency | | 5.1 | Notifier registration | Repeated pattern | 50+ lines | Medium | Medium | Extensibility | | 6.1 | Default account search | Repeated pattern | 35+ lines | Low-Med | Low | Code cleanliness | | 7.1 | Error result creation | Repeated pattern | 4x 5 lines | Low | Low | Consistency | | 9.1 | Type conversion (DUPLICATE) | Exact duplication | 14 lines | Low | High | Code cleanliness | --- ## IMPLEMENTATION PRIORITY ### Phase 1 (High Impact, Low Effort - Do First) 1. **Issue 9.1** - Remove duplicate `convertDomainToProtoType` (15 min) 2. **Issue 1.1** - Extract validation pattern (30 min) 3. **Issue 7.1** - Add error result helper (30 min) ### Phase 2 (Medium Impact, Low-Medium Effort) 4. **Issue 3.1** - Refactor filter matching (40 min) 5. **Issue 4.1** - Extract auth validation (50 min) 6. **Issue 2.1** - Create HTTP helper (45 min) ### Phase 3 (Good to Have, Medium Effort) 7. **Issue 5.1** - Generic registration function (60 min) 8. **Issue 6.1** - Generic default resolution (45 min) --- ## RISK ASSESSMENT All recommended refactorings are **LOW RISK** because: - They extract existing patterns without changing behavior - No API changes or breaking changes - All can be easily tested with existing test suite - Changes are localized to internal utilities - Incremental refactoring possible (Phase 1, 2, 3)