# OpenSpec Change: enterprise-service-discovery - **Change ID**: enterprise-service-discovery - **Package**: `@suaveplan/service-discovery` - **Category**: universal - **Target path**: `universal/network/service-discovery` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/service-discovery.md` --- ## Summary Deliver `@suaveplan/service-discovery` on the genesis branch from scratch at enterprise quality. The package provides a service registry and discovery system: `ServiceRegistry` (register/deregister/lookup services by name and tags), health-check scheduler (HTTP and TCP probes, injectable), TTL-based expiry, `ServiceResolver` with round-robin and weighted load balancing, in-memory and pluggable backends, and event-bus change notifications. Pure TypeScript, no platform hard deps. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The main-branch package had 97.76% coverage. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `ServiceDiscoveryError`; `SERVICE_DISCOVERY.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `StorageLike` | | `@suaveplan/events` (`workspace:^`) | Event bus for registry-change notifications | | `@suaveplan/network` (`workspace:^`) | `HttpClient` for HTTP health probes | | `@suaveplan/retry` (`workspace:^`) | Retry on health-check probes | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API ### Core types ```ts class ServiceDiscoveryError extends SuaveplanError type ServiceStatus = 'healthy' | 'unhealthy' | 'unknown' type HealthCheckType = 'http' | 'tcp' | 'custom' type LoadBalancingStrategy = 'round-robin' | 'weighted' | 'random' | 'first-healthy' interface ServiceEndpoint { host: string port: number protocol?: string // default: 'http' path?: string // default: '/' } interface HealthCheck { type: HealthCheckType intervalMs?: number // default: 30000 timeoutMs?: number // default: 5000 failureThreshold?: number // default: 3; mark unhealthy after N consecutive failures successThreshold?: number // default: 1; mark healthy after N consecutive successes customCheck?: (endpoint: ServiceEndpoint) => Promise } interface ServiceRegistration { id: string // unique per instance name: string // service name (logical) version?: string endpoint: ServiceEndpoint tags?: string[] metadata?: Record ttlMs?: number // auto-expire if not refreshed; 0 = no expiry healthCheck?: HealthCheck weight?: number // for weighted load balancing; default: 1 } interface ServiceInstance extends ServiceRegistration { status: ServiceStatus registeredAt: number // unix epoch ms lastSeenAt: number lastCheckedAt?: number failureCount: number successCount: number } interface RegistryBackend { register(instance: ServiceRegistration): Promise deregister(id: string): Promise get(id: string): Promise list(name?: string, tags?: string[]): Promise refresh(id: string): Promise // reset TTL updateStatus(id: string, status: ServiceStatus): Promise } interface ServiceRegistryOptions { backend?: RegistryBackend // default: MemoryRegistryBackend events?: EventBusLike // for change notifications clock?: Clock logger?: LoggerLike } class ServiceRegistry { constructor(options?: ServiceRegistryOptions) register(registration: ServiceRegistration): Promise deregister(id: string): Promise refresh(id: string): Promise get(id: string): Promise list(name?: string, tags?: string[]): Promise listHealthy(name: string, tags?: string[]): Promise startHealthChecks(client?: HttpClient): void stopHealthChecks(): void } interface ServiceResolverOptions { registry: ServiceRegistry strategy?: LoadBalancingStrategy // default: 'round-robin' clock?: Clock logger?: LoggerLike } class ServiceResolver { constructor(options: ServiceResolverOptions) resolve(name: string, tags?: string[]): Promise resolveAll(name: string, tags?: string[]): Promise } function createServiceRegistry(options?: ServiceRegistryOptions): ServiceRegistry function createServiceResolver(options: ServiceResolverOptions): ServiceResolver // In-memory backend (default) class MemoryRegistryBackend implements RegistryBackend { ... } ``` ### Error codes ```ts type ServiceDiscoveryErrorCode = | 'SERVICE_DISCOVERY_NOT_FOUND' | 'SERVICE_DISCOVERY_ALREADY_REGISTERED' | 'SERVICE_DISCOVERY_HEALTH_CHECK_FAILED' | 'SERVICE_DISCOVERY_NO_HEALTHY_INSTANCES' | 'SERVICE_DISCOVERY_TTL_EXPIRED' | 'SERVICE_DISCOVERY_INVALID_REGISTRATION' const SERVICE_DISCOVERY_ERROR_METADATA: Readonly> function registerServiceDiscoveryErrorCodes(): void ``` ### Zod schemas ```ts ServiceEndpointSchema HealthCheckSchema ServiceRegistrationSchema ServiceInstanceSchema ServiceRegistryOptionsSchema ServiceResolverOptionsSchema ``` --- ## New additions for genesis 1. **Pluggable `RegistryBackend`** — `MemoryRegistryBackend` ships by default; callers can swap to Redis, SurrealDB, or any other store implementing the interface. 2. **Health-check scheduler** — HTTP probes use `@suaveplan/network` `HttpClient`; TCP probes use injected `net.connect`-compatible function. All timers use injectable `Clock`. 3. **TTL expiry** — background sweeper removes expired registrations; sweep interval = min(TTL) / 2. 4. **Event notifications** — `'service:registered'`, `'service:deregistered'`, `'service:status-changed'` events emitted on `EventBusLike`. 5. **Load balancing** — four strategies: round-robin (stateful cursor), weighted (WRR), random (injected RNG), first-healthy (deterministic). 6. **`AbortSignal` propagation** — `resolve` accepts optional signal; throws `ServiceDiscoveryError(SERVICE_DISCOVERY_NO_HEALTHY_INSTANCES)` on abort. 7. **OTel instrumentation** — counter `service_discovery.register_total`, counter `service_discovery.resolve_total`, histogram `service_discovery.resolve_duration_ms`, counter `service_discovery.health_check_total` (attr `service_discovery.result`). 8. **Property-based tests** — round-robin resolves to all registered healthy instances in N calls (no skips), weighted strategy probability distribution converges, register+deregister+list invariants. 9. **Benchmark suite** — `register` 100 services, `resolve` hot path (round-robin, 1000 instances), health check scheduler overhead. --- ## File layout ```text universal/network/service-discovery/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts types/ common.ts common.md core/ service-registry/ service-registry.ts service-registry.md service-registry.types.ts service-registry.test.ts index.ts service-resolver/ service-resolver.ts service-resolver.md service-resolver.types.ts service-resolver.test.ts index.ts backends/ memory-backend/ memory-backend.ts memory-backend.md memory-backend.test.ts index.ts health/ http-probe/ http-probe.ts http-probe.md http-probe.test.ts index.ts tcp-probe/ tcp-probe.ts tcp-probe.md tcp-probe.test.ts index.ts health-scheduler/ health-scheduler.ts health-scheduler.md health-scheduler.test.ts index.ts balancing/ round-robin/ round-robin.ts round-robin.md round-robin.test.ts index.ts weighted/ weighted.ts weighted.md weighted.test.ts index.ts __benchmarks__/ service-discovery.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream**: any microservice or multi-peer architecture on genesis; `@suaveplan/api-gateway` consumers. **Breaking changes**: none — fresh build. **Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/retry` published to Verdaccio.