# ADR 0026: Composite Info Endpoint vs Separate Calls **Status:** Implemented (2026-05-05 — PR pending) ## Context The application currently exposes several endpoints that provide system information: - `/api/version` - returns version, commit, build date, Go version (cached 60s) - `/api/health` - returns `{"status":"healthy"}` (simple liveness) - `/api/healthz` - returns rich health info: status, version, uptime_seconds, timestamp - `/api/ready` - returns readiness with connection details Frontend components like `HealthDashboard` currently call `/api/healthz` to display server info. However, there is a need for a **composite endpoint** that aggregates: 1. Version information (from `/api/version`) 2. Build metadata (commit hash, build date) 3. Uptime information (from `/api/healthz`) 4. Cache status (enabled/disabled) 5. Health status This raises an architectural question: **Should we create a new composite `/api/info` endpoint, or should frontend components make multiple separate API calls?** ### The Problem with Separate Calls If the frontend makes individual calls to `/api/version`, `/api/healthz`, and checks cache config separately: 1. **Multiple network requests**: 3-4 HTTP round trips per page load 2. **Inconsistent data**: Responses may come from different moments in time 3. **No caching coordination**: Each endpoint has its own cache key and TTL 4. **Complex frontend logic**: Need to merge data from multiple sources 5. **Poor user experience**: Slower page loads, multiple loading states ### Current State Analysis | Endpoint | Data Provided | Cache TTL | Use Case | |----------|---------------|-----------|----------| | `/api/version` | version, commit, built, go | 60s | Version info | | `/api/healthz` | status, version, uptime_seconds, timestamp | None | K8s probes, health dashboard | | `/api/health` | status: "healthy" | None | Simple liveness | | `/api/ready` | ready, connections, reason | None | Readiness probes | The `/api/healthz` endpoint already combines some data (status + version + uptime + timestamp), but it: - Doesn't include commit_short - Doesn't include build_date separately - Doesn't include cache_enabled - Is not cached - Has Kubernetes-specific field naming (`healthz`) ## Decision Drivers * **Performance**: Minimize network round trips for frontend * **Consistency**: All data should reflect the same point-in-time * **Maintainability**: Single source of truth for system info * **Caching**: Reuse existing cache infrastructure (ADR-0022) * **API Design**: Follow REST principles and existing patterns * **Backward Compatibility**: Existing endpoints must remain unchanged ## Considered Options ### Option 1: Composite `/api/info` Endpoint (Chosen) Create a new endpoint that aggregates all required data in a single call. **Pros:** - ✅ Single network request for frontend - ✅ Consistent point-in-time data - ✅ Can leverage existing cache infrastructure with key `info:json` - ✅ Follows existing pattern of `/api/version` caching - ✅ Clean API design - one endpoint, one purpose - ✅ Reduces frontend complexity - ✅ Better UX - faster page loads - ✅ Aligns with ADR-0022 cache strategy (reusable cache key pattern) **Cons:** - ⚠️ Duplicates some data from `/api/healthz` and `/api/version` - ⚠️ Requires new endpoint implementation - ⚠️ Need to maintain consistency if source endpoints change ### Option 2: Frontend Aggregation with Multiple Calls Frontend makes separate calls to `/api/version`, `/api/healthz`, and introspects config. **Pros:** - ✅ No backend changes required - ✅ Uses existing endpoints **Cons:** - ❌ Multiple network requests (3-4 round trips) - ❌ Inconsistent data timing - ❌ Complex error handling in frontend - ❌ Poor UX - multiple loading states, slower - ❌ Each endpoint has different caching behavior - ❌ Violates DRY - same data fetched multiple times ### Option 3: Extend `/api/healthz` Endpoint Add `commit_short`, `build_date`, and `cache_enabled` fields to existing `/api/healthz`. **Pros:** - ✅ Reuses existing endpoint - ✅ Single request **Cons:** - ❌ Breaks backward compatibility (response schema change) - ❌ `/api/healthz` is Kubernetes-focused (naming convention) - ❌ Not cached currently - ❌ Mixes health probe concerns with version info - ❌ Violates single responsibility ### Option 4: GraphQL / Query Parameters Allow clients to specify which fields they want via query parameters. **Pros:** - ✅ Flexible - clients get exactly what they need - ✅ Single endpoint **Cons:** - ❌ Overkill for this use case - ❌ Not consistent with existing REST API design - ❌ Complex implementation - ❌ Not aligned with project architecture (Chi router, REST style) ## Decision Outcome **Chosen: Option 1 - Composite `/api/info` Endpoint** We will implement a new `GET /api/info` endpoint that returns a JSON object with all required fields in a single call. This endpoint will: 1. Aggregate data from existing sources (`version` package, `config`, server uptime) 2. Be cached using the existing cache service with key `info:json` 3. Use TTL from `config.cache.default_ttl_seconds` (consistent with ADR-0022) 4. Return `X-Cache: HIT/MISS` headers for debugging 5. Follow existing Go handler patterns from `pkg/server/server.go` ### Response Schema ```json { "version": "1.4.0", "commit_short": "a3f7b2c1", "build_date": "2026-05-04T08:00:00Z", "uptime_seconds": 1234, "cache_enabled": true, "healthz_status": "healthy" } ``` ### Rationale 1. **Performance**: Single HTTP request instead of 3-4 separate calls 2. **Consistency**: All data reflects the same moment in time 3. **Caching**: Leverages existing cache infrastructure (ADR-0022) with predictable key pattern 4. **API Design**: Clean, RESTful endpoint with single responsibility 5. **Maintainability**: Clear separation of concerns - info aggregation is a distinct use case 6. **Backward Compatibility**: Existing endpoints remain unchanged 7. **Frontend Simplicity**: Reduces complexity and improves UX ### Cache Strategy Following ADR-0022 pattern: - Cache key: `info:json` (consistent with `version:format` pattern) - TTL: `config.cache.default_ttl_seconds` (default 300 seconds) - Cache service: `pkg/cache/cache.go` InMemoryService - Headers: `X-Cache: HIT` or `X-Cache: MISS` This allows the endpoint to be fast even under load, while maintaining data freshness. ## Consequences ### Positive 1. **Improved frontend performance**: Single request instead of multiple 2. **Better UX**: Faster page loads, simpler loading states 3. **Consistent data**: All fields reflect the same point-in-time 4. **Cache efficiency**: Reuses existing cache infrastructure 5. **Clean separation**: Info endpoint handles aggregation, source endpoints unchanged 6. **Easy to test**: Single endpoint with predictable response ### Negative 1. **Data duplication**: Some fields appear in multiple endpoints 2. **Maintenance burden**: If source data changes, endpoint must be updated 3. **New endpoint**: Increases API surface area (though minimal) ### Mitigation 1. Data duplication is acceptable - it's read-only system info 2. Source the data from the same packages/functions used by other endpoints 3. The new endpoint has a clear, focused purpose ## Links - [ADR-0002: Chi Router](adr/0002-chi-router.md) - Routing foundation - [ADR-0022: Rate Limiting Cache Strategy](adr/0022-rate-limiting-cache-strategy.md) - Cache pattern reference - [pkg/server/server.go](pkg/server/server.go) - Handler patterns - [pkg/cache/cache.go](pkg/cache/cache.go) - Cache service - [pkg/version/version.go](pkg/version/version.go) - Version data source