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:
- Microservices API Gateway: Use service discovery for routing requests
- Background Workers: Discover task processing services
- 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: