---
name: service-discovery
description: Service Discovery và Service Registry patterns. Use for dynamic service location, health checking, và load balancing trong microservices.
compatibility: ".NET 10+, Consul, Kubernetes, DNS"
metadata:
  author: Velik Ho
  version: "1.0"
---

# Service Discovery / Service Discovery Pattern

Patterns cho service discovery và dynamic service location trong microservices.

## When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:
- Services need to find each other dynamically / Services cần tìm nhau động
- Deploying to Kubernetes / Triển khai trên Kubernetes
- Implementing client-side load balancing / Triển khai load balancing phía client
- Health checking service instances / Kiểm tra health của service instances

## Core Concepts / Khái Niệm Cốt Lõi

### Service Discovery Patterns

```
┌─────────────────────────────────────────────────────────────┐
│                 SERVICE DISCOVERY PATTERNS                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              CLIENT-SIDE DISCOVERY                    │  │
│  │                                                       │  │
│  │  Client ──Query──▶ Registry ──▶ Service List         │  │
│  │    │                                                  │  │
│  │    └──Direct Call──▶ Service Instance                │  │
│  │                                                       │  │
│  │  Pros: Simpler, fewer hops                           │  │
│  │  Cons: Client needs discovery logic                  │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              SERVER-SIDE DISCOVERY                    │  │
│  │                                                       │  │
│  │  Client ──▶ Load Balancer ──Query──▶ Registry        │  │
│  │                  │                                    │  │
│  │                  └──▶ Service Instance                │  │
│  │                                                       │  │
│  │  Pros: Client stays simple                           │  │
│  │  Cons: Extra network hop                             │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### Service Registry / Registry dịch vụ

```
┌─────────────────────────────────────────────────────────────┐
│                    SERVICE REGISTRY                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ Service: user-service                                │  │
│   │ Instances:                                           │  │
│   │   - host: 10.0.1.10, port: 5001, health: ✅         │  │
│   │   - host: 10.0.1.11, port: 5001, health: ✅         │  │
│   │   - host: 10.0.1.12, port: 5001, health: ❌         │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │ Service: order-service                               │  │
│   │ Instances:                                           │  │
│   │   - host: 10.0.2.10, port: 5002, health: ✅         │  │
│   │   - host: 10.0.2.11, port: 5002, health: ✅         │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### Discovery Options Comparison

| Option | Type | Complexity | Best For |
|--------|------|------------|----------|
| **Kubernetes DNS** | Server-side | Low | K8s deployments |
| **Consul** | Client-side | Medium | Multi-platform |
| **Eureka** | Client-side | Medium | Spring ecosystem |
| **DNS + Load Balancer** | Server-side | Low | Simple setups |

## Key Patterns / Mẫu Chính

### Kubernetes Service Discovery

```yaml
# EN: Kubernetes Service for internal discovery
# VI: Kubernetes Service cho discovery nội bộ

apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: goodgo
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 5001
  type: ClusterIP

---
# EN: Deployment with health probes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: goodgo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: goodgo/user-service:latest
          ports:
            - containerPort: 5001
          livenessProbe:
            httpGet:
              path: /health/live
              port: 5001
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 5001
            initialDelaySeconds: 5
            periodSeconds: 5
```

### Using Kubernetes DNS in .NET

```csharp
/// <summary>
/// EN: HTTP client using Kubernetes DNS.
/// VI: HTTP client sử dụng Kubernetes DNS.
/// </summary>

// Program.cs
builder.Services.AddHttpClient<IUserServiceClient, UserServiceClient>(client =>
{
    // EN: Use Kubernetes service DNS name
    // VI: Sử dụng Kubernetes service DNS name
    client.BaseAddress = new Uri("http://user-service.goodgo.svc.cluster.local");
})
.AddStandardResilienceHandler();

// appsettings.json for different environments
{
    "Services": {
        "UserService": {
            "BaseUrl": "http://user-service.goodgo.svc.cluster.local"  // K8s
            // "BaseUrl": "http://localhost:5001"  // Local dev
        }
    }
}
```

### Consul Service Registration

```csharp
/// <summary>
/// EN: Register service with Consul.
/// VI: Đăng ký service với Consul.
/// </summary>
public static class ConsulServiceExtensions
{
    public static IServiceCollection AddConsulServiceDiscovery(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddSingleton<IConsulClient, ConsulClient>(sp =>
        {
            var consulAddress = configuration["Consul:Address"];
            return new ConsulClient(cfg =>
            {
                cfg.Address = new Uri(consulAddress!);
            });
        });

        services.AddHostedService<ConsulRegistrationService>();
        
        return services;
    }
}

/// <summary>
/// EN: Background service for Consul registration.
/// VI: Background service cho Consul registration.
/// </summary>
public class ConsulRegistrationService : IHostedService
{
    private readonly IConsulClient _consulClient;
    private readonly IConfiguration _configuration;
    private readonly ILogger<ConsulRegistrationService> _logger;
    private string? _registrationId;

    public ConsulRegistrationService(
        IConsulClient consulClient,
        IConfiguration configuration,
        ILogger<ConsulRegistrationService> logger)
    {
        _consulClient = consulClient;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken ct)
    {
        var serviceName = _configuration["Service:Name"]!;
        var serviceHost = _configuration["Service:Host"]!;
        var servicePort = int.Parse(_configuration["Service:Port"]!);

        _registrationId = $"{serviceName}-{Guid.NewGuid():N}";

        var registration = new AgentServiceRegistration
        {
            ID = _registrationId,
            Name = serviceName,
            Address = serviceHost,
            Port = servicePort,
            Tags = new[] { "api", "v1" },
            Check = new AgentServiceCheck
            {
                HTTP = $"http://{serviceHost}:{servicePort}/health",
                Interval = TimeSpan.FromSeconds(10),
                Timeout = TimeSpan.FromSeconds(5),
                DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
            }
        };

        await _consulClient.Agent.ServiceRegister(registration, ct);
        
        _logger.LogInformation(
            "Registered service {ServiceName} with Consul as {RegistrationId}",
            serviceName, _registrationId);
    }

    public async Task StopAsync(CancellationToken ct)
    {
        if (_registrationId != null)
        {
            await _consulClient.Agent.ServiceDeregister(_registrationId, ct);
            _logger.LogInformation("Deregistered service {RegistrationId}", _registrationId);
        }
    }
}
```

### Client-Side Load Balancing with Consul

```csharp
/// <summary>
/// EN: HTTP client with Consul-based service discovery.
/// VI: HTTP client với service discovery dựa trên Consul.
/// </summary>
public class ConsulServiceDiscoveryClient
{
    private readonly IConsulClient _consulClient;
    private readonly ILogger<ConsulServiceDiscoveryClient> _logger;

    public ConsulServiceDiscoveryClient(
        IConsulClient consulClient,
        ILogger<ConsulServiceDiscoveryClient> logger)
    {
        _consulClient = consulClient;
        _logger = logger;
    }

    public async Task<Uri?> GetServiceUriAsync(
        string serviceName,
        CancellationToken ct = default)
    {
        var services = await _consulClient.Health.Service(
            serviceName, 
            tag: null, 
            passingOnly: true, 
            ct);

        if (services.Response.Length == 0)
        {
            _logger.LogWarning("No healthy instances found for {ServiceName}", serviceName);
            return null;
        }

        // EN: Simple round-robin load balancing
        // VI: Load balancing round-robin đơn giản
        var service = services.Response[Random.Shared.Next(services.Response.Length)];
        var uri = new Uri($"http://{service.Service.Address}:{service.Service.Port}");

        _logger.LogDebug(
            "Resolved {ServiceName} to {Uri}",
            serviceName, uri);

        return uri;
    }
}

/// <summary>
/// EN: HTTP message handler with service discovery.
/// VI: HTTP message handler với service discovery.
/// </summary>
public class ServiceDiscoveryHandler : DelegatingHandler
{
    private readonly ConsulServiceDiscoveryClient _discovery;
    private readonly string _serviceName;

    public ServiceDiscoveryHandler(
        ConsulServiceDiscoveryClient discovery,
        string serviceName)
    {
        _discovery = discovery;
        _serviceName = serviceName;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken ct)
    {
        var serviceUri = await _discovery.GetServiceUriAsync(_serviceName, ct);
        
        if (serviceUri == null)
            throw new ServiceNotFoundException(_serviceName);

        // EN: Replace the request URI with discovered service
        // VI: Thay thế URI request với service được discover
        var builder = new UriBuilder(request.RequestUri!)
        {
            Scheme = serviceUri.Scheme,
            Host = serviceUri.Host,
            Port = serviceUri.Port
        };
        request.RequestUri = builder.Uri;

        return await base.SendAsync(request, ct);
    }
}
```

### Health Check Endpoints

```csharp
/// <summary>
/// EN: Health check endpoints for service discovery.
/// VI: Health check endpoints cho service discovery.
/// </summary>

// Program.cs
builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database")
    .AddRedis(builder.Configuration.GetConnectionString("Redis")!, "redis")
    .AddRabbitMQ(builder.Configuration.GetConnectionString("RabbitMQ")!, "rabbitmq");

var app = builder.Build();

// EN: Liveness probe - is the service running?
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false, // No checks, just confirm the process is running
    ResponseWriter = WriteMinimalResponse
});

// EN: Readiness probe - can the service handle requests?
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = WriteDetailedResponse
});

// EN: Full health check for monitoring
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = WriteDetailedResponse
});

static Task WriteMinimalResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "text/plain";
    return context.Response.WriteAsync(report.Status.ToString());
}

static Task WriteDetailedResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "application/json";
    var response = new
    {
        status = report.Status.ToString(),
        checks = report.Entries.Select(e => new
        {
            name = e.Key,
            status = e.Value.Status.ToString(),
            description = e.Value.Description,
            duration = e.Value.Duration.TotalMilliseconds
        }),
        totalDuration = report.TotalDuration.TotalMilliseconds
    };
    return context.Response.WriteAsJsonAsync(response);
}
```

## Common Mistakes / Lỗi Thường Gặp

### 1. Hardcoded Service URLs

```csharp
// ❌ BAD: Hardcoded URLs
var client = new HttpClient 
{ 
    BaseAddress = new Uri("http://10.0.1.50:5001") // What if IP changes?
};

// ✅ GOOD: Use service discovery
var client = new HttpClient 
{ 
    BaseAddress = new Uri("http://user-service.goodgo.svc.cluster.local")
};

// Or with Consul
var serviceUri = await _discovery.GetServiceUriAsync("user-service");
```

### 2. No Health Checks

```yaml
# ❌ BAD: No health probes
spec:
  containers:
    - name: api
      image: my-api:latest

# ✅ GOOD: With health probes
spec:
  containers:
    - name: api
      image: my-api:latest
      livenessProbe:
        httpGet:
          path: /health/live
          port: 80
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 80
```

### 3. Missing Retry on Discovery Failure

```csharp
// ❌ BAD: Single attempt
var service = await _discovery.GetServiceAsync("order-service");
await _client.GetAsync(service.Uri + "/api/orders");

// ✅ GOOD: With retry and fallback
var services = await _discovery.GetHealthyServicesAsync("order-service");
foreach (var service in services)
{
    try
    {
        return await _client.GetAsync(service.Uri + "/api/orders");
    }
    catch (HttpRequestException)
    {
        continue; // Try next instance
    }
}
throw new ServiceUnavailableException("order-service");
```

## Quick Reference / Tham Chiếu Nhanh

### Kubernetes Service DNS Format

| Type | Format | Example |
|------|--------|---------|
| Same namespace | `<service-name>` | `user-service` |
| Different namespace | `<service>.<namespace>` | `user-service.goodgo` |
| Full FQDN | `<service>.<namespace>.svc.cluster.local` | `user-service.goodgo.svc.cluster.local` |

### Consul Health Check Types

| Type | Use For |
|------|---------|
| HTTP | REST APIs |
| TCP | Databases, caches |
| gRPC | gRPC services |
| Script | Custom checks |

### Health Probe Configuration

| Probe | Purpose | Failure Action |
|-------|---------|----------------|
| Liveness | Is process alive? | Restart container |
| Readiness | Can handle traffic? | Remove from LB |
| Startup | Is starting up? | Wait before checking |

## Resources / Tài Nguyên

- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [Deployment Kubernetes](../deployment-kubernetes/SKILL.md) - K8s patterns
- [Docker Traefik](../docker-traefik/SKILL.md) - Container networking
- [Inter-service Communication](../inter-service-communication/SKILL.md) - HTTP clients
- [Error Handling](../error-handling-patterns/SKILL.md) - Resilience patterns
