Service Discovery Patterns

Exercise: Service Discovery Patterns

Difficulty - Intermediate

Learning Objectives

  • Apply service discovery concepts from distributed systems
  • Implement load balancing strategies
  • Practice with health checking mechanisms
  • Understand service registry trade-offs

Problem Statement

This exercise builds upon the comprehensive service discovery coverage in Distributed Systems Patterns. Instead of reimplementing the core patterns, you'll apply them to practical scenarios.

📖 Background: For detailed explanations of service discovery concepts, patterns, and production considerations, see the Service Discovery section in the Distributed Systems tutorial.

Exercise Tasks

Task 1: Apply the Service Registry Implementation

Using the service registry implementation from the Distributed Systems article, implement a client that uses it:

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "log"
 7    "time"
 8)
 9
10// Exercise: Build a client that uses the ServiceRegistry from
11// /03-advanced-topics/15-distributed-systems
12
13type ServiceClient struct {
14    registry *ServiceRegistry
15    lb       LoadBalancer
16}
17
18func NewServiceClient(registry *ServiceRegistry) *ServiceClient {
19    return &ServiceClient{
20        registry: registry,
21        lb:       &RoundRobin{},
22    }
23}
24
25// TODO: Implement service discovery with load balancing
26func CallService(ctx context.Context, serviceName string) {
27    // 1. Discover healthy instances using c.registry.Discover()
28    // 2. Select instance using c.lb.Next()
29    // 3. Make HTTP call to selected instance
30    // 4. Handle failures and retry logic
31}
32
33func main() {
34    // Setup registry with sample services
35    registry := NewServiceRegistry(30 * time.Second)
36
37    // TODO: Register some test services
38    // TODO: Test the ServiceClient
39
40    fmt.Println("Service discovery client implementation")
41}

Task 2: Implement Load Balancing Strategies

Extend the load balancer implementations from the main tutorial:

 1// Exercise: Add weighted round-robin based on service health
 2type HealthAwareLoadBalancer struct {
 3    weights map[string]int
 4    counter atomic.Uint64
 5}
 6
 7// TODO: Implement health-based weighting
 8func Next(instances []*ServiceInstance) *ServiceInstance {
 9    // 1. Check health status of each instance
10    // 2. Assign weights based on response times
11    // 3. Select instance using weighted selection
12}

Task 3: Production Scenarios

Apply the patterns to these scenarios:

  1. Microservices API Gateway: Use service discovery for routing requests
  2. Background Workers: Discover task processing services
  3. Database Connection Pooling: Discover healthy database replicas
Hint: Reference Implementation

The core service discovery implementation is available in the Distributed Systems article:

 1// From /03-advanced-topics/15-distributed-systems
 2type ServiceRegistry struct {
 3    mu        sync.RWMutex
 4    services  map[string][]*ServiceInstance
 5    heartbeat time.Duration
 6}
 7
 8func Discover(name string)
 9func Register(instance *ServiceInstance)
10func Heartbeat(name, id string) error

See the main article for complete implementations and production patterns.

Solution Approach

Implementation Guidelines

Task 1 Solution:

 1func CallService(ctx context.Context, serviceName string) {
 2    instances, err := c.registry.Discover(serviceName)
 3    if err != nil {
 4        return "", fmt.Errorf("service discovery failed: %w", err)
 5    }
 6
 7    if len(instances) == 0 {
 8        return "", errors.New("no healthy instances available")
 9    }
10
11    instance := c.lb.Next(instances)
12    if instance == nil {
13        return "", errors.New("load balancer returned nil instance")
14    }
15
16    url := fmt.Sprintf("http://%s:%d/health", instance.Host, instance.Port)
17    resp, err := http.Get(url)
18    if err != nil {
19        return "", fmt.Errorf("service call failed: %w", err)
20    }
21    defer resp.Body.Close()
22
23    return fmt.Sprintf("Called %s at %s:%d", serviceName, instance.Host, instance.Port), nil
24}

Task 2 Solution:

 1func Next(instances []*ServiceInstance) *ServiceInstance {
 2    if len(instances) == 0 {
 3        return nil
 4    }
 5
 6    // Prefer instances with recent successful health checks
 7    now := time.Now()
 8    bestInstance := instances[0]
 9    bestScore := now.Sub(bestInstance.LastSeen)
10
11    for _, instance := range instances[1:] {
12        score := now.Sub(instance.LastSeen)
13        if score < bestScore { // More recent heartbeat is better
14            bestScore = score
15            bestInstance = instance
16        }
17    }
18
19    return bestInstance
20}

Key Takeaways

  • Service discovery enables dynamic microservices communication
  • Load balancing strategies impact performance and reliability
  • Health checking is crucial for system resilience
  • Production patterns require careful consideration of edge cases

Further Learning

For comprehensive coverage of service discovery patterns including:

  • Detailed implementation explanations
  • Production considerations and pitfalls
  • Advanced patterns like consistent hashing
  • Integration with Kubernetes and cloud platforms

See: Distributed Systems Patterns → Service Discovery

For cloud-native service discovery patterns, see: