Backend Modular Monolith Design
Relevant source files
The following files were used as context for generating this wiki page:
The ProductCompare backend is designed as a modular monolith built with the Phoenix framework on Elixir. This architecture prioritizes strict boundary enforcement between functional domains—such as Catalog, Taxonomy, and Accounts—while maintaining them within a single deployment unit. The primary goal is to provide a typed product comparison engine that supports complex workflows like claim lifecycle management, pricing history, and attribute-based filtering.
The system utilizes PostgreSQL 18 as its sole durable datastore and exposes its functionality primarily through a Relay-first GraphQL API. This design ensures that business logic is decoupled from transport layers, allowing the core domain logic to remain stable regardless of whether it is accessed via GraphQL, Phoenix Channels, or internal background jobs.
Sources: README.md:3-5, backend_starting_guide.md:5-7, 2026-03-03-product-compare-backend-mvp-plan.md:7-11
Architectural Principles and Stack
The backend adheres to "non-negotiable conventions" to ensure maintainability and technical consistency across all modules.
Core Technology Stack
| Category | Technology | Version / Specification |
|---|---|---|
| Runtime | Elixir / Erlang/OTP | Elixir 1.19.x / OTP 28 |
| Framework | Phoenix | ~> 1.8.3 |
| Database | PostgreSQL | 18+ (native uuidv7 support) |
| GraphQL | Absinthe | ~> 1.9 (Relay-first) |
| Analysis | Dialyzer / Boundary | Strict typespecs and boundary enforcement |
Sources: README.md:7-14, backend_starting_guide.md:12-30
Boundary Model
The application is structured into five top-level boundaries. This separation ensures that schemas remain lean (data-only) while context modules handle the orchestration of business workflows.
The diagram shows the high-level dependency flow where transport layers (Web/GQL) depend on the Context API, which in turn utilizes Schemas and Infrastructure.
Sources: backend_starting_guide.md:46-64, 2026-03-03-product-compare-backend-mvp-plan.md:10-11
Context Boundaries
The system is partitioned into several distinct contexts, each responsible for a specific domain of the product comparison engine.
| Context | Responsibility |
|---|---|
| Accounts | User management, authentication, and reputation tracking. |
| Taxonomy | Hierarchical structures (taxons), closure tables for traversal, and use-case tagging. |
| Catalog | Brand and product management. |
| Specs | Attributes, units, claims lifecycle, and normalization of numeric values. |
| Pricing | Merchant management, product listings, and historical price tracking. |
| Affiliate | Networks, programs, affiliate links, and coupons. |
| Discussions | Threads, posts, and product reviews. |
Sources: README.md:24-32, 2026-03-03-product-compare-backend-mvp-plan.md:83-113
Data Modeling and Persistence
The backend enforces strict database rules to ensure performance and data integrity, particularly for the high-volume filtering required by product comparisons.
Primary Keys and Identifiers
- Relational Tables: Use
bigintprimary keys (id) for internal foreign key targets. - Global Identifiers: Every relational table includes an
entropy_idusinguuidv7, which is indexed for external-facing lookups. - Timestamps: All timestamps use
:utc_datetime_usecfor microsecond precision.
Sources: backend_starting_guide.md:7-11, 2026-03-03-product-compare-backend-mvp-plan.md:118-120
Attribute Claim Lifecycle
To maintain a "Single Source of Truth" for product specifications while allowing community contributions, the system uses an append-only claim model.
Workflow for transitioning a product specification from a proposal to the active "current" value used in filtering.
Sources: 2026-03-03-product-compare-backend-mvp-plan.md:167-183
Authentication and Security
Authentication is handled via Phoenix-backed session cookies, though all browser flows are routed through GraphQL mutations.
Token Handling Contract
- Storage: Raw secrets are never persisted; they are hashed using
SHA3-256. - Format: Transport tokens are serialized as
"<uuid>.<base64url(raw_secret)>". - Validation: Performed using
Plug.Crypto.secure_compare/2. - Rotation: Single-use refresh rotation is enforced; consuming an old refresh token issues a new pair.
Sources: backend_starting_guide.md:87-105, AGENTS.md:23-28
Authorization Precedence
- Bearer Token:
Authorization: Bearer ...headers are checked first. - Session Fallback: Only used if the bearer header is entirely absent.
- Strictness: Malformed bearer tokens result in an error rather than falling back to the session.
Sources: backend_starting_guide.md:118-125
GraphQL and Relay Conventions
The API is "Relay-first," strictly adhering to the Global Object Identification and Connection specifications.
- Node Interface: The root query must include a
node(id: ID!)field for refetching. - Global IDs: Internal numeric IDs are never exposed; they are encoded into global Relay IDs.
- Connections: All list surfaces use standard connections (edges, cursors) with stable, deterministic ordering for pagination.
- Mutations: Mutations use specific
inputandoutputpayload blocks and return structured error objects rather than simple booleans.
Sources: backend_starting_guide.md:127-147, ARCHITECTURE.md:46-50
Implementation Standards
To maintain high code quality, the project uses a set of automated "CI Gates."
# Example of the strict precommit requirements
defmodule YourApp.MixProject do
# ...
defp aliases do
[
"check.typespecs": ["dialyzer", "check.typespecs --strict"],
precommit: ["compile --warnings-as-errors", "format", "test", "check.typespecs"]
]
end
end
Conceptual representation of the mix aliases used to enforce architectural standards.
Sources: backend_starting_guide.md:160-170
Summary of Requirements
- Typespecs: Every public function must have a
@spec. - Schema Modules: Must be schema-only; no repo orchestration or business logic.
- Contexts: Act as the stable entry point for the domain logic.
Sources: backend_starting_guide.md:58-62, backend_starting_guide.md:154-158