rfc-00002/document.md
2026-07-21 01:33:20 +00:00

16 KiB

[RFC] Product Catalog API

This RFC proposes a Product Catalog REST API for the shopping cart backend that supports creating, reading, updating, deleting, and listing products. The API is exposed under the existing /api route namespace and is implemented with Gin, GORM, PostgreSQL, and goose migrations.

The design defines a stable product resource schema, request validation behavior, error contracts, and implementation responsibilities across handler, service, and repository layers. The goal is to deliver predictable CRUDL behavior aligned with the PRD and acceptance criteria while keeping scope to a single product-catalog module.

Background

The backend currently has no canonical product catalog surface that supports full product lifecycle operations. This limits API consumers to ad hoc product data handling and prevents implementation of downstream shopping-cart flows that depend on consistent product identity, pricing, and discoverability.

The PRD requires a v1 product catalog with exact-match filtering by department and brand, strict validation for required fields, and deterministic error handling. The existing service conventions in this repository (Gin router, modular internal packages, GORM repositories, and goose-managed schema changes) make a dedicated Product Catalog module the most direct way to deliver this feature while preserving project consistency.

Proposal

Introduce a Product Catalog module under internal/product with a single products table and five REST endpoints:

  1. POST /api/products
  2. GET /api/products
  3. GET /api/products/:id
  4. PUT /api/products/:id
  5. DELETE /api/products/:id

The module will:

  1. Persist products in PostgreSQL with UUID primary keys and unique product names.
  2. Enforce validation in service and database layers.
  3. Return JSON-only responses with a stable product shape and a shared error shape.
  4. Map domain and persistence errors to deterministic HTTP status codes.

AC Traceability

This RFC implements the Phase 1 product-catalog scope defined in the PRD and covers the following acceptance-criteria ranges:

  1. AC-1.1 through AC-1.8 for admin product lifecycle management.
  2. AC-2.1 through AC-2.8 for customer product discovery and browsing.
  3. AC-3.1 through AC-3.7 for consistent response behavior and data integrity.

Deferred PRD scope remains out of this RFC:

  1. Phase 2 authentication and authorization requirements.
  2. Phase 3 pagination, advanced browsing, and scale-oriented enhancements.

Trade-offs

  1. Exact-match department and brand filters are chosen over search or fuzzy matching to keep the Phase 1 query surface deterministic and easy to validate, at the cost of less flexible discovery behavior.
  2. Application-generated UUIDv4 identifiers are chosen over database-generated UUIDs to keep the module portable across environments, at the cost of pushing identifier generation responsibility into the model layer.
  3. Hard deletes are chosen over soft deletes to align with the Phase 1 PRD and keep repository behavior simple, at the cost of losing built-in recovery and audit history.
  4. Deterministic default ordering by created_at DESC is included for the list endpoint to satisfy frontend and QA stability requirements, at the cost of making newest-first ordering the fixed Phase 1 behavior until explicit sorting is introduced later.

Success Criteria

  1. All CRUDL endpoints are implemented and registered under /api.
  2. Validation and conflict behavior match RFC and AC expectations.
  3. Migration is idempotent and creates required constraints.
  4. Automated tests cover happy paths and key error conditions.

Out of Scope

  1. Pagination, client-selectable sorting, and full-text search.
  2. Product inventory, media, and category hierarchy.
  3. Phase 2 authentication and authorization policy changes.
  4. Soft deletion and audit/event streams.

Data Model

products Table

Column Type Constraints
id UUID Primary key, generated in app layer (UUIDv4)
name VARCHAR(255) NOT NULL, UNIQUE
description TEXT Nullable
department VARCHAR(255) NOT NULL
brand VARCHAR(255) NOT NULL
price NUMERIC(10,2) NOT NULL, CHECK (price >= 0)
created_at TIMESTAMP WITH TIME ZONE NOT NULL, set on insert
updated_at TIMESTAMP WITH TIME ZONE NOT NULL, managed by GORM

Notes:

  1. UUID is generated before insert using a model hook to avoid DB-specific UUID functions.
  2. Name uniqueness is enforced by unique index and surfaced as HTTP 409.
  3. Deletions are hard deletes.

API Contract

All routes are prefixed with /api.

Method Path Success Key Error Cases
POST /api/products 201 Created 400 Bad Request, 409 Conflict
GET /api/products 200 OK 400 Bad Request (invalid query shape only)
GET /api/products/:id 200 OK 400 Bad Request, 404 Not Found
PUT /api/products/:id 200 OK 400 Bad Request, 404 Not Found, 409 Conflict
DELETE /api/products/:id 204 No Content 400 Bad Request, 404 Not Found

Create Product Request

{
  "name": "Wireless Mouse",
  "description": "Ergonomic wireless mouse",
  "department": "Electronics",
  "brand": "LogiTech",
  "price": 29.99
}

Successful create returns 201 Created with the full product response shape.

Representative validation and conflict responses:

{
  "error": "name is required"
}
{
  "error": "product name already exists"
}

Update Product Request

{
  "price": 24.99,
  "description": "Ergonomic wireless mouse with silent click"
}

Update requests support partial field replacement for mutable business fields only. If id, created_at, or updated_at are supplied, they are ignored. Successful update returns 200 OK with the full updated product response shape.

Representative not-found response:

{
  "error": "product not found"
}

Product Response Shape

{
  "id": "uuid",
  "name": "string",
  "description": "string|null",
  "department": "string",
  "brand": "string",
  "price": 0.00,
  "created_at": "ISO8601",
  "updated_at": "ISO8601"
}

Error Response Shape

{
  "error": "human-readable message"
}

List Filtering

GET /api/products supports optional exact-match query parameters:

  1. department
  2. brand

When both are provided, filters are combined with logical AND.

The default list order is created_at DESC so identical requests return a deterministic newest-first result set until explicit sorting is introduced in a later phase.

Invalid UUID path parameters for :id requests return 400 Bad Request. Unsupported query parameters are ignored unless they violate the request binding shape.

Validation and Field Semantics

Field Rule
name Required, max 255 chars, globally unique
department Required
brand Required
price Required, numeric, >= 0
description Optional

PUT semantics:

  1. The request is treated as replace-for-provided-fields behavior.
  2. id, created_at, and updated_at are ignored if included in request payload.
  3. Name conflicts with a different product return 409.

Implementation

Package and Responsibility Boundaries

  1. internal/product/model.go
  • GORM model definitions
  • request/response DTOs
  • UUID generation hook
  1. internal/product/handler.go
  • Gin bindings for path, query, and body
  • request validation handoff and response marshaling
  • HTTP status mapping for domain errors
  1. internal/product/service.go
  • business validation and invariants
  • orchestration of repository calls
  • conflict and not-found decision logic
  1. internal/product/db.go
  • repository functions for create/get/list/update/delete
  • filter query construction for department/brand
  • default list ordering by created_at descending
  1. internal/router/router.go
  • registration of product routes under /api

Migration

A single goose migration creates products with required constraints and unique index on name. The migration service in docker-compose remains the execution path for local and CI schema setup.

Error Mapping

  1. Validation failure -> 400
  2. Unique name violation -> 409
  3. Missing resource -> 404
  4. Successful delete with no body -> 204

Rollout Plan

  1. Apply migration in development and CI.
  2. Implement repository and service logic.
  3. Add handlers and route wiring.
  4. Execute unit and integration tests.
  5. Run end-to-end demo script and verify example API flows.

Rollback strategy:

  1. Revert route registration and deployment if runtime issues occur.
  2. Preserve backward compatibility by keeping existing paths untouched (new feature only).
  3. If schema rollback is required, apply down migration in controlled environment.

Testing Strategy

  1. Unit tests in internal/product for validation, uniqueness handling, and update semantics.
  2. Integration tests in internal/test for endpoint contracts and DB behavior.
  3. Coverage for:
  • successful CRUDL operations
  • duplicate name conflicts
  • missing resource paths
  • invalid payloads and negative price
  • filter combinations for list endpoint

Performance Test Check

  1. Run a brief concurrency check against the Phase 1 list and update endpoints as part of pre-demo validation.
  2. Verify list requests meet the PRD target of p95 <= 200ms at 100 concurrent reads and update requests meet p95 <= 300ms at 50 concurrent writes in the development environment.
  3. Record the result in the demo validation notes or CI evidence used for the alpha milestone.

Monitoring and Observability

Metrics

The following metrics should be instrumented for the Product Catalog API:

Request Metrics:

  • product_api_requests_total (counter) - Total API requests by endpoint, method, and status code
  • product_api_request_duration_seconds (histogram) - Request latency distribution by endpoint
  • product_api_request_size_bytes (histogram) - Request payload size distribution
  • product_api_response_size_bytes (histogram) - Response payload size distribution

Business Metrics:

  • product_catalog_total (gauge) - Total number of products in catalog
  • product_catalog_by_department (gauge) - Product count by department
  • product_catalog_by_brand (gauge) - Product count by brand
  • product_operations_total (counter) - Product operations by type (create, update, delete)

Database Metrics:

  • product_db_query_duration_seconds (histogram) - Database query latency by operation type
  • product_db_connection_errors_total (counter) - Database connection failures
  • product_db_constraint_violations_total (counter) - Constraint violations by type (unique, check)

Error Metrics:

  • product_api_errors_total (counter) - Errors by type (validation, conflict, not_found, internal)
  • product_validation_failures_total (counter) - Validation failures by field

Logging

Structured Logging Requirements:

All logs should be emitted in JSON format with the following standard fields:

  • timestamp - ISO8601 UTC timestamp
  • level - Log level (debug, info, warn, error)
  • service - "product-catalog"
  • request_id - Unique request identifier for tracing
  • endpoint - API endpoint path
  • method - HTTP method

Log Levels by Event:

INFO Level:

  • Successful product creation with product ID
  • Successful product updates with product ID and changed fields
  • Successful product deletion with product ID
  • List queries with filter parameters and result count

WARN Level:

  • Validation failures with field details
  • Duplicate name conflicts with attempted name
  • Resource not found with requested ID
  • Invalid query parameters

ERROR Level:

  • Database connection failures
  • Unexpected database errors with query context
  • Internal service errors with stack traces
  • Migration failures

Example Log Entry:

{
  "timestamp": "2026-06-07T00:18:43Z",
  "level": "info",
  "service": "product-catalog",
  "request_id": "req-abc123",
  "endpoint": "/api/products",
  "method": "POST",
  "message": "Product created successfully",
  "product_id": "550e8400-e29b-41d4-a716-446655440000",
  "product_name": "Wireless Mouse",
  "department": "Electronics",
  "duration_ms": 45
}

Health Checks

Endpoint: GET /health/products

Response Contract:

{
  "status": "healthy|degraded|unhealthy",
  "checks": {
    "database": {
      "status": "healthy|unhealthy",
      "latency_ms": 5,
      "message": "Connection successful"
    },
    "catalog_size": {
      "status": "healthy|degraded",
      "product_count": 1250,
      "message": "Catalog within normal range"
    }
  },
  "timestamp": "2026-06-07T00:18:43Z"
}

Health Check Logic:

  • Database check: Execute simple SELECT 1 query with 2-second timeout
  • Catalog size check: Warn if product count exceeds expected threshold (configurable)
  • Overall status: Unhealthy if database fails, degraded if catalog size warning

Alerting Thresholds

Critical Alerts:

  • Database connection failure rate > 5% over 5 minutes
  • API error rate > 10% over 5 minutes
  • P99 request latency > 2 seconds over 5 minutes
  • Health check failures > 3 consecutive checks

Warning Alerts:

  • API error rate > 5% over 10 minutes
  • P95 request latency > 1 second over 10 minutes
  • Database query latency P95 > 500ms over 10 minutes
  • Constraint violation rate increase > 50% over baseline

Tracing

Distributed Tracing Requirements:

Each request should generate a trace with the following spans:

  1. http.request - Overall HTTP request span
  2. product.validate - Input validation span
  3. product.service.{operation} - Service layer operation span
  4. product.db.{query} - Database query span

Trace Context Propagation:

  • Accept X-Request-ID header from upstream services
  • Generate new request ID if not provided
  • Propagate request ID through all layers
  • Include request ID in all logs and error responses

Dashboard Requirements

Operational Dashboard:

  • Request rate and error rate by endpoint (last 1h, 24h, 7d)
  • P50, P95, P99 latency by endpoint
  • Database query performance distribution
  • Active product count and growth trend
  • Top departments and brands by product count

Error Dashboard:

  • Error breakdown by type and endpoint
  • Validation failure trends by field
  • Conflict rate (duplicate names)
  • Not found rate by endpoint
  • Database error trends

Performance Baselines

Expected Performance Characteristics:

  • P50 latency: < 50ms for GET operations, < 100ms for write operations
  • P95 latency: < 200ms for GET operations, < 300ms for write operations
  • P99 latency: < 500ms for all operations
  • Throughput: Support 100 requests/second per instance
  • Database query time: < 50ms P95 for indexed queries

Capacity Planning Metrics:

  • Products per department distribution
  • Average product payload size
  • Query filter usage patterns (department vs brand vs combined)
  • Peak request times and seasonal patterns

Security Implications

  1. No new auth mechanism is introduced; this RFC assumes existing service access controls.
  2. Input is constrained via structured JSON binding and explicit validation.
  3. GORM parameterization is used for SQL safety; raw SQL should be avoided in repository methods.

Residual risk:

  1. Without authz in scope, API misuse remains possible if exposed broadly. Mitigation is deferred to platform-level controls.

Dependencies and Cross-Team Requirements

  1. PostgreSQL availability and connectivity via environment variables.
  2. goose migration execution path in local/CI environments.
  3. No external team dependency is required for v1 implementation.

Abandoned Ideas

  1. Database-generated UUIDs
  • Rejected to avoid PostgreSQL-specific UUID generation dependency and keep app-layer portability.
  1. Soft delete with deleted_at
  • Rejected because PRD requires hard-delete semantics for removed products.
  1. Pagination in v1 list endpoint
  • Rejected to keep scope aligned with PRD v1.0; can be introduced in a follow-up RFC.
  1. Case-insensitive uniqueness for name
  • Deferred. Current scope uses DB default uniqueness; normalization/collation policy needs dedicated follow-up discussion.

Open Questions

  1. Should product name uniqueness be case-sensitive or case-insensitive in future versions?
  2. Should future versions return machine-readable error codes in addition to error messages?

Approvals

Role Owner Status Date
Tech Lead Steven Rice Pending TBD
Architect John Suarez Pending TBD