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

CopilotAI 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
FileDescription
README.mdUpdates repo-level docs to describe 10 examples and 3 transport-focused runtimes.
pnpm-workspace.yamlAdds @amqp-contract/client to the catalog.
pnpm-lock.yamlLocks the new @amqp-contract/client dependency and workspace renames.
knip.jsonRemoves deleted example entries and updates Temporal worker entry.
examples/order-worker/vitest.config.tsRemoved (order-worker example deleted).
examples/order-worker/tsconfig.test-d.jsonRemoved (order-worker example deleted).
examples/order-worker/tsconfig.jsonRemoved (order-worker example deleted).
examples/order-worker/src/vitest.d.tsRemoved (order-worker example deleted).
examples/order-worker/src/test-fixtures.tsRemoved (order-worker example deleted).
examples/order-worker/src/queue.tsRemoved (order-worker example deleted).
examples/order-worker/src/queue-runtime.tsRemoved (order-worker example deleted).
examples/order-worker/src/queue-runtime.spec.tsRemoved (order-worker example deleted).
examples/order-worker/src/needs-gate.test-d.tsRemoved (order-worker example deleted).
examples/order-worker/src/module.tsRemoved (order-worker example deleted).
examples/order-worker/src/main.tsRemoved (order-worker example deleted).
examples/order-worker/src/index.tsRemoved (order-worker example deleted).
examples/order-worker/src/env.tsRemoved (order-worker example deleted).
examples/order-worker/src/env.spec.tsRemoved (order-worker example deleted).
examples/order-worker/README.mdRemoved (order-worker example deleted).
examples/order-worker/package.jsonRemoved (order-worker example deleted).
examples/order-temporal/src/workflows.tsRemoved (renamed/replaced by order-temporal-worker).
examples/order-temporal/src/temporal-runtime.spec.tsRemoved (renamed/replaced by order-temporal-worker).
examples/order-temporal/src/module.tsRemoved (renamed/replaced by order-temporal-worker).
examples/order-temporal/README.mdRemoved (renamed/replaced by order-temporal-worker).
examples/order-temporal-worker/vitest.config.tsAdded Temporal worker vitest config with cold-cache timeouts.
examples/order-temporal-worker/tsconfig.test-d.jsonAdded Temporal worker type-test tsconfig.
examples/order-temporal-worker/tsconfig.jsonAdded Temporal worker tsconfig.
examples/order-temporal-worker/src/workflows.tsAdds fulfillOrder saga orchestration workflow.
examples/order-temporal-worker/src/vitest.d.tsAdds unthrown vitest matcher types.
examples/order-temporal-worker/src/test-fixtures.tsReworks fixtures for saga services + DB assertions.
examples/order-temporal-worker/src/temporal-runtime.tsExpands runtime activities to support saga + compensations.
examples/order-temporal-worker/src/temporal-runtime.spec.tsAdds saga e2e specs (fulfill + compensation paths).
examples/order-temporal-worker/src/needs-gate.test-d.tsUpdates runtime needs-gate description/references.
examples/order-temporal-worker/src/module.tsAdds new composition root exporting saga-needed ports.
examples/order-temporal-worker/src/main.tsUpdates runtime description wording after rename.
examples/order-temporal-worker/src/index.tsAdds new public entry exports for the example workspace.
examples/order-temporal-worker/src/fulfillment.tsAdds stand-in fulfillment services module for the saga.
examples/order-temporal-worker/src/env.tsAdds Temporal worker env schema (address/namespace + probes).
examples/order-temporal-worker/src/env.spec.tsAdds Temporal worker env specs.
examples/order-temporal-worker/README.mdNew README explaining the saga and its contract/error model.
examples/order-temporal-worker/package.jsonRenames package to ...-order-temporal-worker.
examples/order-temporal-contract/src/test-fixtures.tsUpdates fixtures to validate fulfillOrder input schema.
examples/order-temporal-contract/src/layering.test-d.tsUpdates layering guard to renamed worker package.
examples/order-temporal-contract/src/contract.tsReplaces single-workflow contract with saga workflow + 5 activities.
examples/order-temporal-contract/README.mdUpdates contract README for saga + compensation semantics.
examples/order-infrastructure/src/test-fixtures.tsAdds Outbox fixture alongside repository fixture.
examples/order-infrastructure/src/prisma-outbox.tsAdds Prisma adapter implementing the outbox read/mark port.
examples/order-infrastructure/src/prisma-outbox.spec.tsAdds specs for transactional outbox behavior.
examples/order-infrastructure/src/prisma-order-repository.tsWrites outbox row in the same transaction as order save; adds remove.
examples/order-infrastructure/src/module.tsExports Outbox from PersistenceModule and provides outbox adapter.
examples/order-infrastructure/src/index.tsRe-exports prismaOutbox.
examples/order-infrastructure/src/database.tsAdds outbox table DDL for in-memory SQLite test DB.
examples/order-infrastructure/prisma/schema.prismaAdds OutboxMessage model.
examples/order-domain/src/index.tsExports new fulfillment domain errors.
examples/order-domain/src/fulfillment.tsAdds OutOfStock and ShippingUnavailable tagged domain errors.
examples/order-config/README.mdUpdates config package docs for renamed example workspaces.
examples/order-application/src/test-fixtures.tsUpdates stub repository to include remove.
examples/order-application/src/ports.tsAdds Outbox/StockService/ShippingService ports + repository remove.
examples/order-application/src/needs-gate.test-d.tsUpdates wiring type-test to include repository remove.
examples/order-application/src/index.tsRe-exports new ports and event type.
examples/order-api/README.mdUpdates cross-links from removed order-worker to Temporal/AMQP examples.
examples/order-amqp/src/test-fixtures.tsRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp/src/needs-gate.test-d.tsRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp/src/module.tsRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp/src/amqp-runtime.tsRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp/src/amqp-runtime.spec.tsRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp/README.mdRemoved (renamed/replaced by order-amqp-worker).
examples/order-amqp-worker/vitest.config.tsAdds AMQP worker vitest config with RabbitMQ global setup.
examples/order-amqp-worker/tsconfig.test-d.jsonAdds AMQP worker type-test tsconfig.
examples/order-amqp-worker/tsconfig.jsonAdds AMQP worker tsconfig.
examples/order-amqp-worker/src/vitest.d.tsAdds unthrown vitest matcher types.
examples/order-amqp-worker/src/test-fixtures.tsAdds fixtures for running the broadcast runtime and tapping services.
examples/order-amqp-worker/src/outbox-relay.tsAdds outbox relay loop using TypedAmqpClient.
examples/order-amqp-worker/src/needs-gate.test-d.tsAdds runtime needs-gate type test for relay + logger needs.
examples/order-amqp-worker/src/module.tsAdds broadcast deployment composition root exporting Outbox/Logger (+ writer port).
examples/order-amqp-worker/src/main.tsUpdates main to include relay poll interval from env.
examples/order-amqp-worker/src/index.tsAdds entry exports for the renamed AMQP worker example.
examples/order-amqp-worker/src/env.tsAdds OUTBOX_POLL_MS validation alongside broker/probes env.
examples/order-amqp-worker/src/env.spec.tsAdds env tests including rejecting OUTBOX_POLL_MS=0.
examples/order-amqp-worker/src/amqp-runtime.tsReplaces command-consumer with broadcast consumer + relay layering.
examples/order-amqp-worker/src/amqp-runtime.spec.tsAdds end-to-end broadcast/outbox/commit-order tests.
examples/order-amqp-worker/README.mdNew README focused on transactional outbox broadcast.
examples/order-amqp-worker/package.jsonRenames package and adds @amqp-contract/client dep.
examples/order-amqp-contract/src/test-fixtures.tsUpdates schema validator fixture to the broadcast payload.
examples/order-amqp-contract/src/layering.test-d.tsUpdates layering guard to renamed worker package.
examples/order-amqp-contract/src/contract.tsReplaces command message with order.placed broadcast event + subscriber queue.
examples/order-amqp-contract/src/contract.spec.tsUpdates contract assertions for broadcast queue and routing key.
examples/order-amqp-contract/README.mdUpdates README to reflect broadcast event contract.
CLAUDE.mdUpdates authoritative spec references to example count/names (still has stale order-worker mentions).
.gitignoreUpdates 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 threadexamples/order-amqp-worker/src/outbox-relay.ts
Comment threadexamples/order-temporal-worker/src/temporal-runtime.spec.ts Outdated
Comment threadexamples/order-config/README.md Outdated
Comment threadCLAUDE.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 threadexamples/order-amqp-worker/src/main.ts Outdated
Comment threadexamples/order-amqp-worker/src/outbox-relay.ts Outdated
Comment threadexamples/order-infrastructure/src/database.ts Outdated
Benoit Traversand 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 threadexamples/order-infrastructure/src/prisma-order-repository.ts Outdated
Comment threadexamples/order-infrastructure/src/prisma-order-repository.ts
Benoit Traversand 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 mainAug 13, 2026
13 checks passed
@btravers
btravers deleted the feat/example-reshape branch August 13, 2026 22:25
Sign up for freeto 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

@btravers