# Client Application Development Recommendations This guide provides best practices for building applications that integrate with the Notifier service. ## Architecture & Design ### 1. Credential Injection Pattern Use dependency injection to pass the API key to your notification client: ```go type NotificationService struct { client *NotifierClient apiKey string // Injected at initialization logger Logger } func NewNotificationService(addr, apiKey string, logger Logger) (*NotificationService, error) { client, err := NewNotifierClient(addr, apiKey) if err != nil { return nil, err } return &NotificationService{ client: client, apiKey: apiKey, logger: logger, }, nil } ``` ### 2. Configuration Management **Structure your config to externalize credentials:** ```go type Config struct { Notifier NotifierConfig `yaml:"notifier"` // ... } type NotifierConfig struct { Address string `yaml:"address"` // e.g., "localhost:50051" APIKey string `yaml:"api_key"` // Load from env var } func (c *Config) LoadFromEnv() { if key := os.Getenv("NOTIFIER_API_KEY"); key != "" { c.Notifier.APIKey = key } } ``` ### 3. Rate Limiting & Retry Logic Implement exponential backoff for rate limit errors: ```go func (s *NotificationService) SendWithRetry(ctx context.Context, req *SendRequest) error { var lastErr error maxRetries := 3 baseDelay := 100 * time.Millisecond for attempt := 0; attempt < maxRetries; attempt++ { err := s.Send(ctx, req) // Check if it's a rate limit error if err != nil && isRateLimitError(err) { // Exponential backoff: 100ms, 200ms, 400ms delay := baseDelay * time.Duration(math.Pow(2, float64(attempt))) time.Sleep(delay) lastErr = err continue } if err != nil { return err // Don't retry non-rate-limit errors } return nil // Success } return fmt.Errorf("rate limit exceeded after %d retries: %w", maxRetries, lastErr) } ``` ### 4. Error Handling Strategy Define clear error handling for each scenario: ```go type NotificationError struct { Code string // "auth_failed", "rate_limited", "invalid_request", "server_error" Message string Retryable bool } func isRetryable(err error) bool { // Retryable: rate limits, temporary network errors, 503 // Non-retryable: auth errors, validation errors, 404 // ... } ``` ## Security Best Practices ### 1. Secret Management Hierarchy ``` Priority 1: Environment Variables Priority 2: Configuration Files (restricted permissions) Priority 3: Secrets Manager (Vault, AWS Secrets Manager) Priority 4: Kubernetes Secrets (if using K8s) ``` **Example:** ```bash # Load from highest priority available if [ -n "$NOTIFIER_API_KEY" ]; then # Use env var API_KEY="$NOTIFIER_API_KEY" elif [ -f /etc/notifier-secret ]; then # Use secret file (only readable by app user) API_KEY=$(cat /etc/notifier-secret) else # Fail - no credential found exit 1 fi ``` ### 2. Key Rotation Strategy Implement zero-downtime key rotation: ```go type NotifierClient struct { primaryKey string secondaryKey string // For rotation period } func (c *NotifierClient) Authenticate(ctx context.Context) error { // Try primary key first if err := c.tryAuthenticate(ctx, c.primaryKey); err == nil { return nil } // Fall back to secondary key if err := c.tryAuthenticate(ctx, c.secondaryKey); err == nil { return nil } return errors.New("authentication failed with all keys") } // During rotation: // 1. Create new key // 2. Deploy code with new key as primary // 3. After deploy completes, disable old key in Notifier service // 4. Remove old key from config ``` ### 3. Preventing Credential Leaks ```go // DON'T: Log credentials logger.Infof("Using API key: %s", apiKey) // WRONG! // DO: Log masked credentials maskedKey := apiKey[:10] + "..." + apiKey[len(apiKey)-4:] logger.Infof("Using API key: %s", maskedKey) // CORRECT // DO: Implement SafeString for sensitive values type SafeString string func (s SafeString) String() string { str := string(s) if len(str) < 10 { return "***" } return str[:4] + "***" + str[len(str)-4:] } // DO: Clear sensitive data from memory after use func (c *NotifierClient) Close() error { if c.apiKey != "" { // Clear from memory (best-effort) for i := 0; i < len(c.apiKey); i++ { c.apiKey[i] = 0 } } return c.conn.Close() } ``` ## Performance Optimization ### 1. Connection Pooling For gRPC: ```go // Reuse single connection for multiple calls conn, _ := grpc.Dial(address, grpc.WithDefaultCallOptions( grpc.MaxCallRecvMsgSize(4*1024*1024), ), ) defer conn.Close() client := pb.NewNotifierServiceClient(conn) // Reuse for multiple calls for _, notif := range notifications { client.SendNotification(ctx, notif) } ``` For REST: ```go // Use http.Client with connection pooling httpClient := &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, MaxConnsPerHost: 100, }, } // Reuse for multiple requests resp, _ := httpClient.Do(req) ``` ### 2. Batch Operations Group notifications to reduce API calls: ```go type BatchNotifier struct { client *NotifierClient batchSize int ticker *time.Ticker queue []*SendRequest } func (bn *BatchNotifier) Queue(req *SendRequest) { bn.queue = append(bn.queue, req) // Flush when batch is full if len(bn.queue) >= bn.batchSize { bn.Flush() } } func (bn *BatchNotifier) Flush() { if len(bn.queue) == 0 { return } // Send batch bn.client.SendBatch(context.Background(), bn.queue) bn.queue = nil } ``` ### 3. Caching & Memoization Cache notifier metadata to reduce API calls: ```go type CachedNotifierClient struct { client *NotifierClient notifiersMu sync.RWMutex notifiers *pb.NotifiersResponse notifiersAge time.Time cacheTTL time.Duration } func (cnc *CachedNotifierClient) GetNotifiers(ctx context.Context) (*pb.NotifiersResponse, error) { cnc.notifiersMu.RLock() if time.Since(cnc.notifiersAge) < cnc.cacheTTL && cnc.notifiers != nil { defer cnc.notifiersMu.RUnlock() return cnc.notifiers, nil } cnc.notifiersMu.RUnlock() // Fetch from server notifiers, err := cnc.client.GetNotifiers(ctx) if err != nil { return nil, err } // Cache result cnc.notifiersMu.Lock() cnc.notifiers = notifiers cnc.notifiersAge = time.Now() cnc.notifiersMu.Unlock() return notifiers, nil } ``` ## Monitoring & Observability ### 1. Instrumentation Instrument your notification client: ```go import "go.opentelemetry.io/otel" type InstrumentedNotifierClient struct { client *NotifierClient tracer trace.Tracer } func (inc *InstrumentedNotifierClient) SendNotification(ctx context.Context, req *SendRequest) error { ctx, span := inc.tracer.Start(ctx, "send_notification") defer span.End() span.SetAttributes( attribute.String("notification.type", string(req.Type)), attribute.Int("notification.recipients", len(req.Recipients)), ) err := inc.client.SendNotification(ctx, req) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } return err } ``` ### 2. Metrics Collection Track key metrics: ```go type MetricsCollector struct { sendAttempts prometheus.Counter sendSuccesses prometheus.Counter sendFailures prometheus.Counter sendDuration prometheus.Histogram rateLimitErrors prometheus.Counter } func (mc *MetricsCollector) Record(result *SendResult) { mc.sendAttempts.Inc() if result.Error != nil { mc.sendFailures.Inc() if isRateLimitError(result.Error) { mc.rateLimitErrors.Inc() } } else { mc.sendSuccesses.Inc() } mc.sendDuration.Observe(result.Duration.Seconds()) } ``` ### 3. Health Checks Periodically verify connectivity: ```go func (s *NotificationService) HealthCheck(ctx context.Context) error { deadline, _ := context.WithTimeout(ctx, 5*time.Second) _, err := s.client.HealthCheck(deadline) return err } // In your main loop ticker := time.NewTicker(30 * time.Second) go func() { for range ticker.C { if err := s.HealthCheck(context.Background()); err != nil { logger.Errorf("Health check failed: %v", err) // Maybe trigger alerts or circuit breaker } } }() ``` ## Testing ### 1. Mock the Notifier Client ```go type MockNotifierClient struct { SendNotificationFunc func(context.Context, *SendRequest) error } func (m *MockNotifierClient) SendNotification(ctx context.Context, req *SendRequest) error { if m.SendNotificationFunc != nil { return m.SendNotificationFunc(ctx, req) } return nil } // In tests func TestNotificationService(t *testing.T) { mock := &MockNotifierClient{ SendNotificationFunc: func(ctx context.Context, req *SendRequest) error { assert.Equal(t, "email", string(req.Type)) return nil }, } svc := NewNotificationService(mock) err := svc.Notify("test@example.com", "Hello") assert.NoError(t, err) } ``` ### 2. Test Rate Limiting ```go func TestRateLimitHandling(t *testing.T) { responses := []error{ status.Error(codes.ResourceExhausted, "rate limit"), status.Error(codes.ResourceExhausted, "rate limit"), nil, // Success on third try } callCount := 0 mock := &MockNotifierClient{ SendNotificationFunc: func(ctx context.Context, req *SendRequest) error { err := responses[callCount] callCount++ return err }, } svc := NewNotificationService(mock) err := svc.SendWithRetry(context.Background(), &SendRequest{...}) assert.NoError(t, err) assert.Equal(t, 3, callCount) } ``` ## Deployment Considerations ### 1. Environment Variables Checklist ```bash # Production checklist NOTIFIER_API_KEY=nk_... # From secure secrets manager NOTIFIER_ADDRESS=notifier:50051 # Use internal DNS NOTIFIER_TIMEOUT=30s # Reasonable timeout APP_LOG_LEVEL=info # Not debug (sensitive logs) ``` ### 2. Kubernetes Secrets ```yaml apiVersion: v1 kind: Secret metadata: name: notifier-credentials type: Opaque stringData: api-key: nk_... --- apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: template: spec: containers: - name: my-app env: - name: NOTIFIER_API_KEY valueFrom: secretKeyRef: name: notifier-credentials key: api-key - name: NOTIFIER_ADDRESS value: notifier:50051 ``` ### 3. Docker Best Practices ```dockerfile # DON'T embed credentials ARG API_KEY=default ENV NOTIFIER_API_KEY=$API_KEY # DO mount secrets # docker run -v /run/secrets/notifier_api_key:/etc/notifier-secret ... # DO use multi-stage builds to exclude dev dependencies FROM golang:1.21-alpine AS builder WORKDIR /build COPY . . RUN go build -o app . FROM alpine:latest COPY --from=builder /build/app . # Credentials provided at runtime only CMD ["./app"] ``` ## Versioning & Compatibility ### 1. API Versioning Your client should handle API changes gracefully: ```go type APIVersion struct { Major int Minor int Patch int } func (s *NotificationService) CheckCompatibility(version APIVersion) error { if version.Major != 1 { return fmt.Errorf("incompatible API version: %d", version.Major) } return nil } ``` ### 2. Feature Detection Detect available features instead of hardcoding versions: ```go func (s *NotificationService) SupportsHTMLEmail() bool { notifiers, _ := s.GetNotifiers(context.Background()) for _, n := range notifiers.Notifiers { if n.Type == TypeEmail { return true // Assume HTML support in email notifiers } } return false } ``` ## Troubleshooting Checklist - [ ] API key format is correct: `nk_<32-hex>` - [ ] API key hasn't expired - [ ] Client has required roles for the notifier - [ ] Rate limit hasn't been exceeded - [ ] Notifier service is accessible (network, firewall) - [ ] Request payload is valid JSON/protobuf - [ ] Notifier account exists in service config - [ ] Credentials are being loaded from environment (not hardcoded) - [ ] Connection is using correct protocol (HTTP/2 for gRPC) - [ ] Logs are not leaking sensitive data ## Summary 1. **Externalize credentials** - Use env vars or secrets managers 2. **Implement retries** - Handle rate limits gracefully 3. **Cache when possible** - Reduce API calls 4. **Monitor health** - Regular health checks 5. **Instrument code** - Add tracing and metrics 6. **Test thoroughly** - Mock clients and test error cases 7. **Secure deployment** - Mount secrets at runtime, not build time 8. **Log carefully** - Never log API keys or sensitive data