The problem
Several business modules needed one configurable multi-level approval flow with per-level deadlines, configured differently per tenant. But if the engine reads a module's tables, or a module imports engine internals, they are welded together within six months.
Three further problems: configuration keeps changing under in-flight requests; two approvers can click at the same instant; and a manager approves a hundred requests in one action.
The approach
One public gateway class is the engine's entire surface. The services behind it are not exported, and lint rules block deep imports.
The return path is inverted: approval emits domain events and business modules
subscribe. Modules register a log source at boot so their history merges into the
approval timeline without the engine knowing their tables. Cross-module relations
use indexed entity_type + entity_id — never foreign keys across modules.
On submit, the whole workflow tree including per-level deadlines is snapshotted, so later configuration edits cannot rewrite history.
Approve and reject carry an optimistic lock returning 409 on a stale write,
and approving a level marks peers ignored and opens the next level in the same
transaction.
Bulk is a separate path: one transaction, statements grouped per table in a fixed order rather than interleaved row locks; invalid items are skipped with their error code instead of failing the batch.
Per-level SLA uses two delayed jobs as pure clocks, not the source of truth: the worker re-reads the database when it fires and does nothing if the level was already handled — so no job ever needs cancelling. Job IDs are deterministic, so rescheduling cannot duplicate mail. Redis being down loses reminders but never blocks approving.
Where that failure domain stops is worth drawing:
The result
50 TypeScript files behind a single gateway class, 11 documented error codes, 7 flow diagrams, and 18 user stories written back from delivered code.
Notification ownership is explicit: the engine owns every mail belonging to the approval flow and returns the exact recipient lists so callers never guess. All dispatch happens after commit and is fail-soft — a mail failure can never undo an approval.