The commerce platform is split into services by responsibility and trust zone
Context
"Commerce" spans very different concerns: a stateful transactional core (merchants, orders, checkout, the money orchestration), a background-job workload (webhooks, emails, recurring billing), a payment-processing control plane, and the one component that ever touches a cleartext card. These have different failure modes, different scaling and latency needs, different languages that fit them, and — critically — very different PCI trust levels. Putting them in one codebase would drag the whole application into the card-data blast radius and couple unrelated release cycles.
Decision
Split the platform into separate services by responsibility and by trust zone, each its own repo:
commerce-api(Elixir/Phoenix) — the transactional core and the public/merchant API: merchants, orders, checkout, discounts, and the orchestration of the money paths. It owns and migrates the MAIN schema (ADR 0016). Never sees a cleartext PAN.commerce-worker(Elixir) — the background-job runtime (Oban): outbound webhooks, emails, subscriptions/billing, appointments. It shares the MAIN database and owns the JOBS database (ADR 0017); commerce-api enqueues into it over HTTP (ADR 0014). Isolating jobs keeps their load and failures off the request path.commerce-payments(TypeScript/Hono) — the PCI control plane: provider adapters (ADR 0008), the append-only ledger and the PanToken vault. Handles PanTokens only, never a cleartext PAN (ADR 0006). Low PCI scope.commerce-payments-edge(TypeScript/Cloudflare) — the CDE: the only place a cleartext PAN exists, and it retains nothing (ADR 0006). Its own repo precisely because it is the highest trust zone — a small, bounded surface tightens change-control and least-privilege for the code that PCI cares about most.commerce-adapters(TypeScript) — the legacy adapter service being superseded bycommerce-payments(ADR 0009); it exists during the migration and will be retired.
The organising principle: language fits the job (Elixir for the stateful core and its job runtime; TS/Hono and a Cloudflare Worker for the fast, isolated payment edge), and trust zone dictates the boundary — the sensitive card-handling code is quarantined in the smallest repo that can hold it.
Consequences
- Positive: the PCI CDE is a tiny, independently change-controlled repo; the rest of the platform is out of the card-data blast radius. Each service deploys and scales on its own cadence.
- Positive: background-job churn and failures are isolated from the request-serving core; the payment control plane can evolve (and be swapped in behind the compat shim) without touching the transactional API.
- Neutral / to revisit: the split has real seams to maintain — the shared MAIN database couples api and worker (ADR 0017), the HTTP enqueue contract couples them too (ADR 0014), and the signed checkout-intent canonical couples commerce-api to the edge (ADR 0012). Cross-service contracts must be versioned and kept in step.
- Neutral / to revisit: two payment services (
commerce-paymentsandcommerce-adapters) run in parallel until the migration completes (ADR 0009).
Alternatives considered
- A single commerce monolith: rejected — it would pull the entire application into PCI scope and the card-data blast radius, and couple unrelated release cycles (a job change forcing an API redeploy, etc.).
- One payments repo for both the control plane and the CDE: rejected — mixing the PAN-handling edge with the PAN-free control plane enlarges the tightly-controlled surface; keeping the CDE alone in its own repo is the whole point of the trust-zone boundary (ADR 0006/0007).
- Merging api and worker into one Elixir app: rejected — background-job load and failures would share the request path's runtime; a separate worker isolates the job runtime and lets it be operated and scaled independently.