Skip to content

feat!(examples): each example does what its transport is for - #33

Merged
btravers merged 7 commits into
mainfrom
feat/example-reshape
Aug 13, 2026
Merged

feat!(examples): each example does what its transport is for#33
btravers merged 7 commits into
mainfrom
feat/example-reshape

Conversation

@btravers

Copy link
Copy Markdown
Contributor

Per discussion: no start-core-only examples, and each worker example should teach its transport's real strength.

Structure

  • Removed examples/order-worker — the in-memory queue example existed to demonstrate the bare kernel; that job is done better by the adapter-backed examples.
  • Renamed order-amqporder-amqp-worker and order-temporalorder-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).
  • An outbox relay layered onto start-amqp's runtime sweeps pending rows in commit order onto the orders exchange, marks what the broker confirmed, and is deliberately at-least-once. It starts after the consumer and stops before it; drain stays the consumer's.
  • The contract is now a broadcast: order.placed (a fact, past tense), one subscriber queue (order-notifications) with the DLX/retry story intact.
  • Proven end to end against a real RabbitMQ: place → outbox → relay → exchange → consumer, commit order preserved, outbox drained — including delivery to a foreign queue the contract never declared, which is the broadcast claim itself.

order-temporal-worker: orchestration, as a fulfillment saga

  • fulfillOrder orchestrates placereserveStockarrangeShipping and compensates in reverse (releaseStock, then cancelPlacement) when a later step answers a permanent no.
  • Triage rule: declared errors compensate and surface typed at the client; Temporal's machinery failures propagate uncompensated (un-deciding unknown state is a second bug). Compensation activities declare no errors — cancelPlacement absorbs OrderNotFound so a re-run answers the same.
  • Stubs (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.
  • One subtlety documented in the README: AsyncResults are eager, so saga steps must be built inside the previous step's flatMap or the sequence runs as a race.

Foundation

New Outbox/StockService/ShippingService ports in order-application, OutOfStock/ShippingUnavailable in order-domain, outbox table + transactional save + remove in order-infrastructure.

Verification

turbo run build typecheck test green across all workspaces (the one failure on my machine is start-core's pre-existing binds 9000 test, 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

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>
Copilot AI lite review requested due to automatic review settings August 13, 2026 21:41
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-worker and renames/restructures the Temporal and AMQP examples into order-temporal-worker (fulfillment saga) and order-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-worker as 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.

Comment thread examples/order-amqp-worker/src/outbox-relay.ts
Comment thread examples/order-temporal-worker/src/temporal-runtime.spec.ts Outdated
Comment thread examples/order-config/README.md Outdated
Comment thread CLAUDE.md Outdated
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>
Comment thread examples/order-amqp-worker/src/main.ts Outdated
Comment thread examples/order-amqp-worker/src/outbox-relay.ts Outdated
Comment thread examples/order-infrastructure/src/database.ts Outdated
Benoit Travers and others added 2 commits August 14, 2026 00:01
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>
Comment thread examples/order-infrastructure/src/prisma-order-repository.ts Outdated
Comment thread examples/order-infrastructure/src/prisma-order-repository.ts
Benoit Travers and others added 2 commits August 14, 2026 00:17
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>
@btravers
btravers merged commit bc11702 into main Aug 13, 2026
13 checks passed
@btravers
btravers deleted the feat/example-reshape branch August 13, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants