# Module Interaction Model

This document defines the rules governing how the 20 modules in `04-domain-modules.md` are allowed to communicate. These rules are what makes "modular monolith" a real architectural discipline rather than a label on a ball of mud — and what preserves the option to extract a module into a separately-deployed service later without a rewrite (see `03-reference-architecture.md`).

## The two allowed communication patterns

A module may interact with another module in exactly two ways. Nothing else is permitted.

1. **Synchronous in-process call to another module's public interface.**
   Used when the caller needs an answer *now*, on the request path — e.g., Orders calling Tax to calculate the tax owed during checkout, or Orders calling Inventory to reserve stock before confirming a purchase. The call goes through the target module's published service interface only. It never reaches past that interface into the target module's internal logic or storage.

2. **Asynchronous domain event via the Postgres-backed event/job mechanism.**
   Used when the source module needs to announce that something happened, and doesn't need or want to wait for consumers to react — e.g., Orders publishing "Order Placed," which Inventory, Notification Service, Automation Engine, and Analytics each independently pick up and act on, on their own schedule. This is the default pattern for anything that isn't strictly required to complete before the triggering action can be considered done.

**Rule of thumb:** if the checkout (or any Core synchronous flow) cannot proceed without the answer, it's pattern 1. If the action can happen "eventually" without blocking anything the merchant or customer is waiting on, it's pattern 2. When in doubt, prefer pattern 2 — it's what keeps INTEL and SUPPORT modules from becoming availability dependencies of CORE modules.

## What "owns its data" means in practice

Each module owns its own tables/schema namespace. Concretely:

- **No cross-module joins in application code.** If Orders needs product information, it asks Catalog through Catalog's public interface — it does not query Catalog's tables directly, even though they live in the same physical database.
- **No shared tables.** Two modules never write to the same table. If two modules seem to need the same data, that's a signal the data belongs to one of them and the other should be consuming it via a call or an event, not a signal to share storage.
- **Snapshots, not live references, across a transaction boundary.** An Order Line Item stores a snapshot of the product's name/price/SKU at time of purchase — it does not live-join against Catalog's current product record. This is why "Product" means something different in Catalog (current merchandising data), Inventory (current stock data), and Orders (a frozen historical fact). Collapsing these into one shared "Product" model is the single most common modeling mistake in commerce systems, and it's exactly what this rule prevents.

This is documentation-only discipline in Phase 1 — see `06-open-questions.md` for the flagged risk that Phase 3 needs to pick an actual enforcement mechanism (schema-per-module plus a lint/dependency-boundary checker) before implementation begins in earnest.

## Worked example: Order Placed

A single checkout completion touches most of the system. Tracing it shows both patterns in use and shows why the CORE/INTEL/SUPPORT tiering (`04-domain-modules.md`) matters operationally:

1. **Customer completes checkout.** Commerce Core orchestrates the flow, calling Tax (pattern 1, synchronous — checkout can't complete without a tax figure), Discount Engine (pattern 1 — the discount must be validated and applied correctly before the total is final), Payments (pattern 1 — authorization must succeed before the order is confirmed), and Inventory (pattern 1 — stock must be reserved/decremented as part of the same transactional boundary).
2. **Order is confirmed.** Orders persists the Order and its immutable Line Item snapshots, then publishes an **Order Placed** event (pattern 2).
3. **Subscribers react independently, asynchronously:**
   - **Inventory** finalizes the stock decrement tied to the order (if not already finalized in step 1) and checks whether any affected product crosses a low-stock threshold.
   - **Notification Service** sends the order confirmation email/SMS to the customer.
   - **Automation Engine** evaluates the order for cross-sell/upsell learning signal and checks whether it resolves an abandoned-cart recovery that was in flight.
   - **Analytics** records the transaction for metric computation (revenue, conversion funnel).
4. **Downstream of Analytics:** **Reporting** picks up computed metrics on its own schedule to build merchant-facing reports. **Merchant Health** incorporates the new order data, along with Inventory's low-stock signal from step 3, into the next Store Health Score recalculation.

Notice what never happens: Notification Service never queries Orders' tables directly to figure out what to send — it reacts to the event payload. Merchant Health never reaches into Inventory's stock tables — it consumes Inventory's published low-stock event (relayed through or alongside Automation Engine, per `04-domain-modules.md`'s dependency list) and Analytics' computed metrics. Every cross-module fact flows through a public interface or an event, never through a shared table.

## Why this is worth the discipline

Nothing in CommerceDex's Phase 1 architecture deploys these modules separately today — that would violate the cost/deployment constraint in `03-reference-architecture.md` for no current benefit. But because every cross-module interaction already goes through an interface or an event instead of a shared table, extracting any one module into its own process later is a matter of swapping an in-process call for a network call and an in-process event bus for the same Postgres-backed mechanism (or a real broker, if ever justified) — not a redesign of how the system thinks about ownership and boundaries.
