Service Discovery Patterns

Learn the fundamental patterns and best practices for implementing service discovery in distributed microservices architectures.

Overview

Service discovery is a critical component in microservices architecture that enables services to find and communicate with each other dynamically. ScoutQuest implements several proven patterns to make service discovery reliable, scalable, and easy to manage.

Client-Side Discovery Pattern

In client-side discovery, the service client is responsible for determining the location of available service instances and load balancing requests across them.

How it works

  1. Service instances register themselves with the service registry (ScoutQuest server)
  2. Clients query the service registry to get available service instances
  3. Clients handle load balancing and failover logic

JavaScript/TypeScript Example

import { ScoutQuestClient } from '@scoutquest/sdk';

const client = new ScoutQuestClient('http://localhost:8080');

// Discover available instances
const instances = await client.discoverService('user-service');

// Client-side load balancing
const selectedInstance = instances[Math.floor(Math.random() * instances.length)];
const response = await fetch(`${selectedInstance.getUrl()}/users`);

Rust Example

use scoutquest_rust::ServiceDiscoveryClient;

let client = ServiceDiscoveryClient::new("http://localhost:8080", None)?;

// Discover and automatically load balance
let response: Vec<User> = client.get("user-service", "/users").await?;

Advantages

  • Simple architecture: No additional infrastructure components
  • Performance: Direct communication between services
  • Control: Clients have full control over load balancing strategies

Disadvantages

  • Coupling: Clients are coupled to the service registry
  • Logic duplication: Load balancing logic must be implemented in each client
  • Complexity: Each client must handle service discovery logic

Server-Side Discovery Pattern

In server-side discovery, a load balancer or API gateway is responsible for service discovery and load balancing.

How it works

  1. Services register with the service registry
  2. Load balancer queries the registry for available instances
  3. Clients make requests to the load balancer
  4. Load balancer routes requests to appropriate service instances

Example with API Gateway

# API Gateway configuration
routes:
  - path: /api/users/*
    service: user-service
    discovery:
      registry: scoutquest
      strategy: round-robin

  - path: /api/orders/*
    service: order-service
    discovery:
      registry: scoutquest
      strategy: least-connections

Advantages

  • Decoupling: Clients are decoupled from service discovery
  • Centralized logic: Load balancing logic is centralized
  • Simplified clients: Clients just make standard HTTP requests

Disadvantages

  • Additional component: Requires load balancer or API gateway
  • Single point of failure: Load balancer must be highly available
  • Extra network hop: Additional latency

Service Registry Pattern

The service registry is a database of available service instances. ScoutQuest implements this pattern with additional features for health checking and metadata.

Registration Strategies

Self-Registration

Services register themselves when they start up:

// Service startup
const client = new ScoutQuestClient('http://localhost:8080');

await client.registerService({
  serviceName: 'user-service',
  host: 'localhost',
  port: 3000,
  metadata: {
    version: '1.2.0',
    environment: 'production'
  },
  tags: ['api', 'users'],
  healthCheck: {
    path: '/health',
    interval: 30
  }
});

Third-Party Registration

External service registrar handles registration:

# Using Docker with automatic registration
docker run -d \
  --name user-service \
  --label "scoutquest.service=user-service" \
  --label "scoutquest.port=3000" \
  --label "scoutquest.health=/health" \
  my-user-service:latest

Health Checking Patterns

Health checking ensures that only healthy service instances receive traffic. ScoutQuest supports multiple health checking patterns.

Active Health Checks

ScoutQuest actively polls service health endpoints:

{
  "serviceName": "user-service",
  "healthCheck": {
    "path": "/health",
    "interval": 30,
    "timeout": 5,
    "retries": 3
  }
}

Passive Health Checks

Services send heartbeat signals:

// Send heartbeat every 30 seconds
setInterval(async () => {
  await client.heartbeat('user-service-instance-1');
}, 30000);

Circuit Breaker Pattern

Implement circuit breakers to handle failing services gracefully:

class CircuitBreaker {
  private failures = 0;
  private state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN

  async call(serviceName: string, path: string) {
    if (this.state === 'OPEN') {
      throw new Error('Circuit breaker is OPEN');
    }

    try {
      const result = await this.client.get(serviceName, path);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
}

Load Balancing Strategies

Different load balancing strategies suit different use cases. ScoutQuest supports multiple strategies.

Round Robin

Distributes requests evenly across all instances:

const client = new ScoutQuestClient('http://localhost:8080', {
  loadBalancing: 'round-robin'
});

// Automatically uses round-robin for all requests
const users = await client.get('user-service', '/users');

Least Connections

Routes to the instance with the fewest active connections.

Weighted Round Robin

Distributes requests based on instance weights:

await client.registerService({
  serviceName: 'user-service',
  host: 'localhost',
  port: 3000,
  metadata: {
    weight: 10 // Higher weight = more traffic
  }
});

Random

Selects instances randomly, useful for simple scenarios.

Service Mesh Integration

ScoutQuest can integrate with service mesh solutions for advanced traffic management.

Istio Integration Example

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: user-service
spec:
  hosts:
  - user-service
  http:
  - route:
    - destination:
        host: user-service
        subset: v1
      weight: 90
    - destination:
        host: user-service
        subset: v2
      weight: 10

Best Practices

1. Graceful Shutdown

Always deregister services during shutdown:

process.on('SIGTERM', async () => {
  await client.deregister('user-service-instance-1');
  process.exit(0);
});

2. Service Versioning

Use metadata for service versioning:

await client.registerService({
  serviceName: 'user-service',
  metadata: {
    version: '2.1.0',
    api_version: 'v2'
  },
  tags: ['v2', 'stable']
});

3. Environment Isolation

Use different service names or tags for environments:

const serviceName = `user-service-${process.env.ENVIRONMENT}`;
// Results in: user-service-staging, user-service-production

4. Monitoring and Alerting

Monitor service discovery health:

  • Service registration/deregistration rates
  • Health check success rates
  • Service discovery query latency
  • Instance availability metrics

5. Security Considerations

  • Use TLS for all service registry communication
  • Implement authentication for service registration
  • Network segmentation for service discovery traffic
  • Regular security audits of registered services

Common Patterns Summary

Pattern Use Case Complexity Performance
Client-Side Discovery Small to medium microservices Medium High
Server-Side Discovery Large microservices, polyglot environments Low (for clients) Medium
Service Mesh Complex distributed systems High Medium

Next Steps

Now that you understand service discovery patterns, explore these related topics: