update: document content
This commit is contained in:
parent
757dac2cb7
commit
38e6469c92
465
document.md
465
document.md
@ -1,2 +1,465 @@
|
|||||||
# PaymanTestRFC
|
|
||||||
|
|
||||||
|
# [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
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "name is required"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "product name already exists"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Product Request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "product not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product Response Shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "string",
|
||||||
|
"description": "string|null",
|
||||||
|
"department": "string",
|
||||||
|
"brand": "string",
|
||||||
|
"price": 0.00,
|
||||||
|
"created_at": "ISO8601",
|
||||||
|
"updated_at": "ISO8601"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response Shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
2. internal/product/handler.go
|
||||||
|
- Gin bindings for path, query, and body
|
||||||
|
- request validation handoff and response marshaling
|
||||||
|
- HTTP status mapping for domain errors
|
||||||
|
3. internal/product/service.go
|
||||||
|
- business validation and invariants
|
||||||
|
- orchestration of repository calls
|
||||||
|
- conflict and not-found decision logic
|
||||||
|
4. 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
|
||||||
|
5. 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:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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.
|
||||||
|
2. Soft delete with deleted_at
|
||||||
|
- Rejected because PRD requires hard-delete semantics for removed products.
|
||||||
|
3. Pagination in v1 list endpoint
|
||||||
|
- Rejected to keep scope aligned with PRD v1.0; can be introduced in a follow-up RFC.
|
||||||
|
4. 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 |
|
||||||
Loading…
Reference in New Issue
Block a user