Uh oh!
There was an error while loading. Please reload this page.
feat!(examples): each example does what its transport is for - #33
Conversation
Remove order-worker (a start-core-only example), rename order-amqp -> order-amqp-worker and order-temporal -> order-temporal-worker, and give each surviving worker its transport's real story: - order-amqp-worker: event broadcast via a transactional outbox. OrderRepository.save writes the order row and its outbox row in one transaction; a relay unit layered onto start-amqp's runtime sweeps the outbox onto the orders exchange; the contract's consumer is one subscriber among any. Proven end to end against a real RabbitMQ, including delivery to a queue the contract never declared. - order-temporal-worker: a fulfillment saga. fulfillOrder orchestrates place -> reserveStock -> arrangeShipping and compensates in reverse (releaseStock, cancelPlacement) when a step answers a permanent no — declared errors compensate and surface typed at the client; machinery failures propagate uncompensated. Proven against the time-skipping test environment, both compensation paths included. Foundation: Outbox/StockService/ShippingService ports in order-application, OutOfStock/ShippingUnavailable in order-domain, the outbox table + transactional save + remove (compensation's persistence arm) in order-infrastructure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Refactors the examples/ teaching surface to remove the start-core-only worker example and reframe the remaining example deployments around what each transport is best at (HTTP answering, Temporal orchestration, AMQP broadcast), adding the necessary domain/application/infrastructure support (fulfillment saga + transactional outbox).
Changes:
- Removes
examples/order-workerand renames/restructures the Temporal and AMQP examples intoorder-temporal-worker(fulfillment saga) andorder-amqp-worker(transactional outbox broadcast). - Adds fulfillment ports/errors and a compensation-capable Temporal workflow + activities, with updated Temporal contract.
- Implements a transactional outbox in the Prisma repository plus an outbox relay layered onto the AMQP runtime/contract.
Reviewed changes
Copilot reviewed 77 out of 92 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates repo-level docs to describe 10 examples and 3 transport-focused runtimes. |
| pnpm-workspace.yaml | Adds @amqp-contract/client to the catalog. |
| pnpm-lock.yaml | Locks the new @amqp-contract/client dependency and workspace renames. |
| knip.json | Removes deleted example entries and updates Temporal worker entry. |
| examples/order-worker/vitest.config.ts | Removed (order-worker example deleted). |
| examples/order-worker/tsconfig.test-d.json | Removed (order-worker example deleted). |
| examples/order-worker/tsconfig.json | Removed (order-worker example deleted). |
| examples/order-worker/src/vitest.d.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/test-fixtures.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/queue.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/queue-runtime.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/queue-runtime.spec.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/needs-gate.test-d.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/module.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/main.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/index.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/env.ts | Removed (order-worker example deleted). |
| examples/order-worker/src/env.spec.ts | Removed (order-worker example deleted). |
| examples/order-worker/README.md | Removed (order-worker example deleted). |
| examples/order-worker/package.json | Removed (order-worker example deleted). |
| examples/order-temporal/src/workflows.ts | Removed (renamed/replaced by order-temporal-worker). |
| examples/order-temporal/src/temporal-runtime.spec.ts | Removed (renamed/replaced by order-temporal-worker). |
| examples/order-temporal/src/module.ts | Removed (renamed/replaced by order-temporal-worker). |
| examples/order-temporal/README.md | Removed (renamed/replaced by order-temporal-worker). |
| examples/order-temporal-worker/vitest.config.ts | Added Temporal worker vitest config with cold-cache timeouts. |
| examples/order-temporal-worker/tsconfig.test-d.json | Added Temporal worker type-test tsconfig. |
| examples/order-temporal-worker/tsconfig.json | Added Temporal worker tsconfig. |
| examples/order-temporal-worker/src/workflows.ts | Adds fulfillOrder saga orchestration workflow. |
| examples/order-temporal-worker/src/vitest.d.ts | Adds unthrown vitest matcher types. |
| examples/order-temporal-worker/src/test-fixtures.ts | Reworks fixtures for saga services + DB assertions. |
| examples/order-temporal-worker/src/temporal-runtime.ts | Expands runtime activities to support saga + compensations. |
| examples/order-temporal-worker/src/temporal-runtime.spec.ts | Adds saga e2e specs (fulfill + compensation paths). |
| examples/order-temporal-worker/src/needs-gate.test-d.ts | Updates runtime needs-gate description/references. |
| examples/order-temporal-worker/src/module.ts | Adds new composition root exporting saga-needed ports. |
| examples/order-temporal-worker/src/main.ts | Updates runtime description wording after rename. |
| examples/order-temporal-worker/src/index.ts | Adds new public entry exports for the example workspace. |
| examples/order-temporal-worker/src/fulfillment.ts | Adds stand-in fulfillment services module for the saga. |
| examples/order-temporal-worker/src/env.ts | Adds Temporal worker env schema (address/namespace + probes). |
| examples/order-temporal-worker/src/env.spec.ts | Adds Temporal worker env specs. |
| examples/order-temporal-worker/README.md | New README explaining the saga and its contract/error model. |
| examples/order-temporal-worker/package.json | Renames package to ...-order-temporal-worker. |
| examples/order-temporal-contract/src/test-fixtures.ts | Updates fixtures to validate fulfillOrder input schema. |
| examples/order-temporal-contract/src/layering.test-d.ts | Updates layering guard to renamed worker package. |
| examples/order-temporal-contract/src/contract.ts | Replaces single-workflow contract with saga workflow + 5 activities. |
| examples/order-temporal-contract/README.md | Updates contract README for saga + compensation semantics. |
| examples/order-infrastructure/src/test-fixtures.ts | Adds Outbox fixture alongside repository fixture. |
| examples/order-infrastructure/src/prisma-outbox.ts | Adds Prisma adapter implementing the outbox read/mark port. |
| examples/order-infrastructure/src/prisma-outbox.spec.ts | Adds specs for transactional outbox behavior. |
| examples/order-infrastructure/src/prisma-order-repository.ts | Writes outbox row in the same transaction as order save; adds remove. |
| examples/order-infrastructure/src/module.ts | Exports Outbox from PersistenceModule and provides outbox adapter. |
| examples/order-infrastructure/src/index.ts | Re-exports prismaOutbox. |
| examples/order-infrastructure/src/database.ts | Adds outbox table DDL for in-memory SQLite test DB. |
| examples/order-infrastructure/prisma/schema.prisma | Adds OutboxMessage model. |
| examples/order-domain/src/index.ts | Exports new fulfillment domain errors. |
| examples/order-domain/src/fulfillment.ts | Adds OutOfStock and ShippingUnavailable tagged domain errors. |
| examples/order-config/README.md | Updates config package docs for renamed example workspaces. |
| examples/order-application/src/test-fixtures.ts | Updates stub repository to include remove. |
| examples/order-application/src/ports.ts | Adds Outbox/StockService/ShippingService ports + repository remove. |
| examples/order-application/src/needs-gate.test-d.ts | Updates wiring type-test to include repository remove. |
| examples/order-application/src/index.ts | Re-exports new ports and event type. |
| examples/order-api/README.md | Updates cross-links from removed order-worker to Temporal/AMQP examples. |
| examples/order-amqp/src/test-fixtures.ts | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp/src/needs-gate.test-d.ts | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp/src/module.ts | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp/src/amqp-runtime.ts | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp/src/amqp-runtime.spec.ts | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp/README.md | Removed (renamed/replaced by order-amqp-worker). |
| examples/order-amqp-worker/vitest.config.ts | Adds AMQP worker vitest config with RabbitMQ global setup. |
| examples/order-amqp-worker/tsconfig.test-d.json | Adds AMQP worker type-test tsconfig. |
| examples/order-amqp-worker/tsconfig.json | Adds AMQP worker tsconfig. |
| examples/order-amqp-worker/src/vitest.d.ts | Adds unthrown vitest matcher types. |
| examples/order-amqp-worker/src/test-fixtures.ts | Adds fixtures for running the broadcast runtime and tapping services. |
| examples/order-amqp-worker/src/outbox-relay.ts | Adds outbox relay loop using TypedAmqpClient. |
| examples/order-amqp-worker/src/needs-gate.test-d.ts | Adds runtime needs-gate type test for relay + logger needs. |
| examples/order-amqp-worker/src/module.ts | Adds broadcast deployment composition root exporting Outbox/Logger (+ writer port). |
| examples/order-amqp-worker/src/main.ts | Updates main to include relay poll interval from env. |
| examples/order-amqp-worker/src/index.ts | Adds entry exports for the renamed AMQP worker example. |
| examples/order-amqp-worker/src/env.ts | Adds OUTBOX_POLL_MS validation alongside broker/probes env. |
| examples/order-amqp-worker/src/env.spec.ts | Adds env tests including rejecting OUTBOX_POLL_MS=0. |
| examples/order-amqp-worker/src/amqp-runtime.ts | Replaces command-consumer with broadcast consumer + relay layering. |
| examples/order-amqp-worker/src/amqp-runtime.spec.ts | Adds end-to-end broadcast/outbox/commit-order tests. |
| examples/order-amqp-worker/README.md | New README focused on transactional outbox broadcast. |
| examples/order-amqp-worker/package.json | Renames package and adds @amqp-contract/client dep. |
| examples/order-amqp-contract/src/test-fixtures.ts | Updates schema validator fixture to the broadcast payload. |
| examples/order-amqp-contract/src/layering.test-d.ts | Updates layering guard to renamed worker package. |
| examples/order-amqp-contract/src/contract.ts | Replaces command message with order.placed broadcast event + subscriber queue. |
| examples/order-amqp-contract/src/contract.spec.ts | Updates contract assertions for broadcast queue and routing key. |
| examples/order-amqp-contract/README.md | Updates README to reflect broadcast event contract. |
| CLAUDE.md | Updates authoritative spec references to example count/names (still has stale order-worker mentions). |
| .gitignore | Updates Temporal cache comment link to new worker path. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
CLAUDE.md:64
- This paragraph still uses
examples/order-workeras one of the three “one runtime per process” demonstrations and describes a dead-letter outcome that no longer exists now that the in-memory queue example was removed. The referenced example list and the outcome comparison should be updated to match the current three examples (API, AMQP broadcast, Temporal orchestration).
`examples/order-api`, `examples/order-worker` and `examples/order-temporal-worker`
make this testable rather than asserted: the same `ApplicationModule` +
`PersistenceModule` composition under three runtimes, with the same
`DuplicateOrder` arriving as a typed `CONFLICT` on the first, a dead-letter
on the second and a `nonRetryable` typed contract error on the third — and
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The relay's idle sleep clears its timer on early wake and unrefs it (a stray timeout could pin the event loop past stop() for up to pollMs); the shipping-refusal spec pins the typed ShippingUnavailable at the client instead of any-error; two stale order-worker references dropped from order-config's README and CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Every caller passed the same orderContract constant, so the parameter was ceremony; order-temporal-worker keeps its own because its specs pass a genuinely different value (withTaskQueue). Documents why the relay's client is created rather than injected, and why the relay is not a di provider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-written DDL and schema.prisma are two sources of truth for one shape; a model added to the schema alone compiles and fails only at runtime. The new spec reads the schema and fails if any model has no table. Documents why a real migration step has nothing to occupy here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
remove now writes a tombstone — an event with a null payload — in the same transaction as the delete, so a subscriber that learned an order exists learns it is gone. The outbox row becomes the event envelope (kind, subjectId, occurredAt, payload) and the wire carries it under one routing key, because a reader compacting by id needs a subject's create and its tombstone in one ordered stream. A remove that finds nothing writes nothing: the delete fails inside the transaction and the tombstone rolls back with it, so a compensation that runs twice cannot tell the world twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DDL array is gone. prisma/migrations/ is generated from the schema by prisma migrate dev and committed, a db:migrate script runs prisma migrate deploy, and turbo's dev task depends on ^db:migrate so the app cannot start against an unmigrated database — the deploy step owns migrations, never the process at boot. This example's own database is in-memory, so no external command can reach it: openDatabase applies the same committed SQL itself, which means the specs run the exact statements a deployment runs instead of a hand-kept copy that can drift. Prisma 7 removed url from the schema datasource, so the migration connection lives in prisma.config.ts — the CLI's alone, since the application passes a driver adapter rather than a URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Per discussion: no start-core-only examples, and each worker example should teach its transport's real strength.
Structure
examples/order-worker— the in-memory queue example existed to demonstrate the bare kernel; that job is done better by the adapter-backed examples.order-amqp→order-amqp-workerandorder-temporal→order-temporal-worker.order-amqp-worker: event broadcast, via a transactional outbox
OrderRepository.save(order-infrastructure) writes the order row and its outbox row in one$tryTransaction— the write and the fact of the write commit or roll back together (spec: a rolled-back duplicate leaves no event behind).start-amqp's runtime sweeps pending rows in commit order onto theordersexchange, marks what the broker confirmed, and is deliberately at-least-once. It starts after the consumer and stops before it;drainstays the consumer's.order.placed(a fact, past tense), one subscriber queue (order-notifications) with the DLX/retry story intact.order-temporal-worker: orchestration, as a fulfillment saga
fulfillOrderorchestratesplace→reserveStock→arrangeShippingand compensates in reverse (releaseStock, thencancelPlacement) when a later step answers a permanent no.cancelPlacementabsorbsOrderNotFoundso a re-run answers the same.FulfillmentModule) always say yes; the specs swap in refusing providers and verify both compensation paths against the real database — after a refusal, the placement is gone.AsyncResults are eager, so saga steps must be built inside the previous step'sflatMapor the sequence runs as a race.Foundation
New
Outbox/StockService/ShippingServiceports in order-application,OutOfStock/ShippingUnavailablein order-domain, outbox table + transactional save +removein order-infrastructure.Verification
turbo run build typecheck testgreen across all workspaces (the one failure on my machine isstart-core's pre-existingbinds 9000test, blocked locally by ZscalerTunnel squatting on 127.0.0.1:9000 — identical on untouched main). Lint, format, knip clean. Example spec count: 79 (docs updated from 93). Docs swept: examples/README (three-deployment framing), both worker READMEs rewritten around their patterns, contract READMEs, CLAUDE.md counts, knip entry.🤖 Generated with Claude Code