# Module Interfaces

This document makes `../architecture/05-module-interaction-model.md`'s "synchronous in-process call to a public interface" pattern concrete: how a module exposes itself to others, and what's off-limits.

## One public entry point per module

Each module (`modules/<module_name>/` in the codebase, mirroring the schema names in `01-database-design-principles.md`) exposes exactly one public interface — a single file or namespace (e.g., `modules/inventory/public.ts`) that re-exports only the functions other modules are allowed to call. Everything else inside the module directory — internal services, repository/query code, the module's own schema access — is private by convention and enforced by the dependency-boundary linter described in `01-database-design-principles.md`. Another module may only ever `import` from `modules/<module_name>/public`, never reach into `modules/<module_name>/internal/...`.

## Two kinds of public methods

- **Commands** — verbs that change state (`reserveStock`, `calculateTax`, `applyDiscount`, `capturePayment`). Commands run inside the caller's transaction where atomicity matters (e.g., Orders reserving stock as part of checkout must succeed or fail together with the order write) — this means a command's implementation must be transaction-participant-safe, not spawn its own independent transaction.
- **Queries** — read-only lookups (`getProductSummary`, `getCustomerSegments`, `getShippingRates`). Queries return **read models (DTOs)**, purpose-built flat shapes for the caller's need — never the module's internal entity/ORM object. This is what stops a schema change inside one module from breaking every caller: as long as the DTO shape is honored, the underlying table structure is free to change.

## Error handling: expected vs unexpected

- **Expected business outcomes** (insufficient stock, invalid discount code, tax calculation unavailable) are returned as a typed result (e.g., `{ ok: false, reason: 'INSUFFICIENT_STOCK' }`), not thrown as exceptions. The calling module is expected to handle these as normal control flow — Orders checking `reserveStock`'s result and surfacing "out of stock" to the checkout flow is business logic, not error handling.
- **Unexpected system failures** (database connection lost, a bug) throw/propagate normally and are logged — these are not business outcomes a caller should be written to anticipate.

This distinction keeps calling code readable: a checkout flow reads as a sequence of business decisions, not a try/catch pyramid.

## Canonical example: Tax module interface

Resolving `../architecture/06-open-questions.md` #3 concretely — the interface Commerce Core/Orders calls during checkout:

```
calculateTax(orderContext: {
  lineItems, shippingAddress, customerExemptions
}) -> TaxResult: {
  ok: true, totalTaxAmount, breakdown: [...]
} | { ok: false, reason: 'JURISDICTION_UNSUPPORTED' | ... }
```

Nothing in this signature implies the rate tables in `tax.tax_rate` (`02-module-schemas.md`) are the only possible implementation. A future swap to an external tax API changes only the internals behind `calculateTax` — every caller, and the checkout flow that depends on it, is unaffected. This is the general pattern every module's interface should follow: **the signature describes the business capability, never the storage mechanism behind it.**

## Cross-module read example: Orders displaying a product name

Orders needs a product's current name/image to render an order detail page (distinct from the frozen `name_snapshot` on the line item, which reflects the name *at time of purchase* — see `02-module-schemas.md`). It calls `catalog.getProductSummary(productId) -> { name, imageUrl, status }`, a query — read-only, no transaction participation needed, safe to cache briefly. Orders never queries `catalog.product` directly, even for something this simple, because the boundary is enforced identically regardless of how trivial the read looks (the database-level grants in `01-database-design-principles.md` wouldn't allow it anyway).

## What this buys, concretely

Because every cross-module interaction is one of these two typed, narrow surfaces, a module's internal implementation — its table structure, its business logic, even its language/framework choice in a hypothetical future extraction — can change freely as long as its public interface's behavior is preserved. Integration tests between modules are written against the public interface, not against internal state, which is what makes the extraction path described in `../architecture/03-reference-architecture.md`'s scaling story realistic rather than theoretical.
