Microservices and Patterns¶
PackyTrace maps the bounded contexts in the domain model to independently deployed services. The API Gateway is an architectural edge rather than a bounded context. Measurement & Anonymization is a bounded context implemented by measurement-pipeline; it owns the privacy-preserving consumer-to-brand transformation.
Decomposition¶
Solid arrows below are synchronous calls made when the caller needs an immediate answer. Dashed arrows are asynchronous Kafka facts. Every data-bearing service accesses only its own schema.
The domain-to-service relationship is direct:
Verdict stays inside Personalization & Verdict. The measurement pipeline stays separate from Brand Analytics because raw consumer facts must be aggregated before crossing the privacy wall. Fridge, Shopping List and Identity facts are published but are not currently projected into brand metrics.
Data ownership¶
Services exchange IDs, APIs and versioned events; they never read another service's tables.
| Owner | Private data | Important records |
|---|---|---|
| passport-service | passport schema + producer memory registry | Scan records, product requests, Open Food Facts cache, in-memory Brand products and lot traces |
| personalization-service | personalization schema | Health profiles and rule sets |
| fridge-service | fridge schema | Event streams, current items, freshness and waste projections |
| shopping-list-service | shopping_list schema | Event streams and current-items projection |
| identity-service | identity schema + Keycloak | Visitor identities, accounts, consent ledger, Brands and Brand Users |
| measurement-pipeline | measurement schema | Processed-event ledger and windowed metric counters |
| brand-analytics-service | brand_analytics schema | Minimum-group-size, tenant-scoped metric read model |
One PostgreSQL instance hosts the seven schemas. Each service connects through its own GRANT-restricted role, so ownership is enforced by the database rather than convention. Cross-service Brand references use stable IDs because schema isolation rules out cross-schema foreign keys.
Development fixture IDs belong to the seed and simulator configuration, not the architecture. Four deployments of mock-brand-service act as independent external producer APIs. Passport periodically loads their synthetic product and lot snapshots into memory. Open Food Facts remains the public community-data fallback behind its anti-corruption adapter, cache and circuit breaker.
Microservice patterns¶
The patterns below are described through the problem they solve, the concrete PackyTrace mechanism, verification evidence and trade-offs. Event Sourcing has its own page, Event Sourcing, which also covers the event contracts and the event-driven measurement pipeline.
API Gateway¶

Diagram source: services/api-gateway/architecture.drawio. Current implementation view, verified against the gateway source on 2026-08-01.
Problem. Clients must not know eight service addresses or be trusted to supply account and Brand identity headers. Authentication, CORS, visitor identity and route ownership need one public enforcement point.
Implementation. services/api-gateway/internal/gateway/router.go builds a chi router whose middleware order is required by the edge policy: request metadata and recovery, Prometheus instrumentation, CORS, visitor identity and Keycloak token verification all run before proxying. Route files mount each public surface on its owning upstream. '/api/v1/me/' requires an authenticated account, while '/api/v1/brand/' requires a verified brand_id claim. Client-supplied X-Account-Id and X-Brand-Id headers are deleted; only server-derived values are forwarded. httputil.ReverseProxy returns a stable 502 upstream_unavailable response when an upstream is unreachable.
Evidence. router_test.go covers CORS, Visitor ID precedence, authentication, account gates and Brand-scope gates. The container journey test sends scans only through the gateway.
Trade-off. The gateway is a critical entry dependency and must remain thin. It owns edge policy and routing, not domain orchestration or another service's business rules.
CQRS¶

Diagram source: docs/development/architecture/cqrs.drawio. Current implementation view, verified against the pipeline and Brand Analytics sources on 2026-08-01.
Problem. The Brand dashboard needs tenant-scoped, dashboard-shaped reads, but raw consumer facts must never be queried by or copied into Brand Analytics. The service that owns a command-side fact need not also own the dashboard-shaped query model.
Implementation. Passport and Personalization are the current command-side fact producers. The Measurement Pipeline consumes those allowed facts, builds a privacy-gated projection and publishes only BrandMetricBatchPublished aggregates that satisfy the minimum group size. Brand Analytics consumes those batches and upserts its own brand_metrics read model keyed by Brand, period, metric and canonical dimensions. Its repository includes WHERE brand_id = ? on every read. The dashboard calls GET /api/v1/brand/metrics through the gateway's verified Brand gate.
Brand Analytics is a query-only service: nothing a client sends can change its data, and its read model is kept current by events rather than by a shared table.
Evidence. BrandMetricConsumer decodes only the aggregate event; ingestBrandMetric writes the read model; queryBrandMetrics reads it. Tests cover decoding, ingestion, replacement and tenant-scoped responses.
Trade-off. The separation costs a second datastore and the code that keeps it current, and the read model lags the consumer side by the open window plus the flush interval. Analytics tolerates a view that is minutes old, and a scan never waits for it.
The pipeline stages that feed this read model are described in Event Sourcing.
Circuit Breaker¶
Problem. Open Food Facts is outside PackyTrace's control. Repeatedly waiting for its timeout during an outage would consume resources and make every scan slow.
Implementation. offapi/openfoodfacts.go wraps the source with a mutex-protected breaker. The default threshold is three consecutive failures and the cooldown is 30 seconds. While open, requests fail fast with errCircuitOpen; after cooldown, a probe is allowed. Success closes and resets the breaker. Stale cached entries are preferred over a full scan failure, and optional passport-section failures remain section-local.
Evidence. openfoodfacts_test.go verifies that the upstream is hit three times and then bypassed. resolve_scan_test.go proves that stale cache and optional-source failures do not fail the complete scan.
Trade-off. The breaker may temporarily reject a recovered source until its cooldown permits a probe. This bounded delay is preferable to multiplying latency and load during an outage.
Pattern interaction¶
The API Gateway centralizes edge policy before requests reach the owning service. Kafka transports selected facts from the consumer side to the aggregation write side, and CQRS exposes only the privacy-safe read model to Brands. The Circuit Breaker keeps external-source failures from consuming the scan path. Fridge and Shopping List facts originate from their event-sourced aggregates; Passport, Personalization and Identity publish integration facts from ordinary transactional state. Operational visibility into these patterns belongs to the observability patterns, which are covered on their own page.