From 69c445de26840625f983830ac60aa234035f6176 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 17:54:11 +0200 Subject: [PATCH] docs: refresh explainers + adding-a-feature with full binder signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit di-explainer and data-flow-explainer narratives still showed the pre-ADR-014 1-arg bindProductionBlog(config) form. Updated both explainers to (config, tracer, logger, bus, queue), and adjusted the narrative arc so the production-swap step mentions the resolveEventsAndJobs* preamble. adding-a-feature's Step 16 sketch was the same 1-arg shape; replaced with the canonical 5-arg signature and a short note that the bus/queue params are accept-and-forward until gen event consume / gen job generators inject usage at the anchors. Pointer to a real feature's bind-production.ts for the complete reference. Final sweep is clean — no stale (container, config) or 1-arg bindProduction signatures remain in docs/. --- docs/architecture/data-flow-explainer.html | 6 ++--- docs/architecture/di-explainer.html | 6 ++--- docs/guides/adding-a-feature.md | 28 ++++++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/architecture/data-flow-explainer.html b/docs/architecture/data-flow-explainer.html index faca525..7b9691d 100644 --- a/docs/architecture/data-flow-explainer.html +++ b/docs/architecture/data-flow-explainer.html @@ -1664,7 +1664,7 @@ footer .colophon {

Two binding modes, one symbol.

-

The BlogModule binds IArticlesRepository to MockArticlesRepository by default — useful at dev/test time. At app boot, bindProductionBlog(config) unbinds the symbol and rebinds it to new ArticlesRepository(config). Use cases and controllers don't notice — they get whatever the symbol currently resolves to.

+

The BlogModule binds IArticlesRepository to MockArticlesRepository by default — useful at dev/test time. At app boot, bindProductionBlog(config, tracer, logger, bus, queue) unbinds the symbol and rebinds it to new ArticlesRepository(config, tracer, logger). Use cases and controllers don't notice — they get whatever the symbol currently resolves to.

This is also why the boundary stays clean: features don't import core-cms; the app passes the Payload config in.

@@ -1773,7 +1773,7 @@ footer .colophon {

The mock is reached from two directions. Both are legitimate, neither is "the test version":

    -
  1. By the DI container at runtime. BlogModule binds IArticlesRepository to MockArticlesRepository at module-load time. Anything resolving that symbol — use cases, controllers, tRPC procedures, the dev server — gets the mock until bindProductionBlog(config) swaps it for the real Payload-backed one. See §03.
  2. +
  3. By the DI container at runtime. BlogModule binds IArticlesRepository to MockArticlesRepository at module-load time. Anything resolving that symbol — use cases, controllers, tRPC procedures, the dev server — gets the mock until bindProductionBlog(config, tracer, logger, bus, queue) swaps it for the real Payload-backed one. See §03.
  4. By tests, via direct construction. Unit tests skip the container entirely. They construct the mock with new MockArticlesRepository() and pass it directly into the use-case factory function. Same class, different consumer — just a closure with a fake repo.
@@ -1786,7 +1786,7 @@ footer .colophon {
show: the mock as DI binding (in module.ts)
-

This is from packages/blog/src/di/module.ts — the very first binding in the module is the mock. Everything downstream (use cases, controllers) resolves through this default. bindProductionBlog(config) later replaces only this one line at app boot — use case + controller bindings stay put.

+

This is from packages/blog/src/di/module.ts — the very first binding in the module is the mock. Everything downstream (use cases, controllers) resolves through this default. bindProductionBlog(config, tracer, logger, bus, queue) later replaces only this one line at app boot — use case + controller bindings stay put.

export const BlogModule = new ContainerModule((bind) => {
   // 1) Mock is the DEFAULT binding for the repo symbol.
   //    Dev server, unit tests, storybook all resolve to this.
diff --git a/docs/architecture/di-explainer.html b/docs/architecture/di-explainer.html
index 438438a..d6e0294 100644
--- a/docs/architecture/di-explainer.html
+++ b/docs/architecture/di-explainer.html
@@ -756,7 +756,7 @@ footer .colophon {
       
file 04 · prod swap

bind-production.ts

Replaces the mock repository binding with a real Payload-backed one at app boot.

-

Exports bindProductionBlog(config: SanitizedConfig). Function body: blogContainer.unbind(symbol) if already bound, then .bind(symbol).toConstantValue(new ArticlesRepository(config)). Use cases and controllers stay bound to their factory bindings — they will fetch the new repo through the container automatically because their factories call ctx.container.get(...) at every resolution.

+

Exports bindProductionBlog(config, tracer, logger, bus, queue). Function body: blogContainer.unbind(symbol) if already bound, then .bind(symbol).toConstantValue(new ArticlesRepository(config, tracer, logger)). Use cases and controllers are wrapped via withSpan(withCapture(factory(deps))) at bind time so they inherit instrumentation without changing their factory bodies. The bus and queue params come from the app's resolveEventsAndJobs* step (ADR-015) and feed the // <gen:event-handlers> / // <gen:jobs> injection sites.

When it runs: called from app boot (apps/web-next/src/server/bind-production.ts) when USE_DEV_SEED ≠ "true" AND Payload config is resolvable.
@@ -1302,8 +1302,8 @@ const MODES = { scenarioTag: 'when this happens', title: 'Production binder ran at app boot', narrative: [ - "App's bindAll() did not see USE_DEV_SEED=true, so it dispatched to bindAllProduction(config). That awaited the resolved Payload config and called bindProductionBlog(config).", - "The binder unbound IArticlesRepository and rebound it with .toConstantValue(new ArticlesRepository(config)) — the real Payload-backed implementation that will hit Postgres on every method call.", + "App's bindAll() did not see USE_DEV_SEED=true, so it dispatched to bindAllProduction(). That resolved instrumentation (Rule 0), resolved a Payload-backed bus + queue (ADR-015), awaited the Payload config, and called bindProductionBlog(config, tracer, logger, bus, queue).", + "The binder unbound IArticlesRepository and rebound it with .toConstantValue(new ArticlesRepository(config, tracer, logger)) — the real Payload-backed implementation that will hit Postgres on every method call.", "Use cases and controllers still resolve through their .toDynamicValue closures, but the closures now fetch the real repo. Callers cannot tell the difference — same interface, different storage." ], bindings: [ diff --git a/docs/guides/adding-a-feature.md b/docs/guides/adding-a-feature.md index 07be271..3316083 100644 --- a/docs/guides/adding-a-feature.md +++ b/docs/guides/adding-a-feature.md @@ -926,20 +926,44 @@ describe("CommentsRepository", () => { ### Step 16: `bind-production.ts` +The full canonical shape includes instrumentation (ADR-014) and the +event-bus + job-queue parameters (ADR-015) — see any feature's +`bind-production.ts` for a complete reference. Minimal sketch: + ```typescript // src/di/bind-production.ts import type { SanitizedConfig } from "payload"; +import { + withSpan, + withCapture, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; +import type { IEventBus } from "@repo/core-events"; +import type { IJobQueue } from "@repo/core-shared/jobs"; import { commentsContainer } from "./container"; import { COMMENTS_SYMBOLS } from "./symbols"; import { CommentsRepository } from "../infrastructure/repositories/comments.repository"; -export function bindProductionComments(config: SanitizedConfig): void { +export function bindProductionComments( + config: SanitizedConfig, + tracer: ITracer, + logger: ILogger, + bus: IEventBus, + queue: IJobQueue, +): void { if (commentsContainer.isBound(COMMENTS_SYMBOLS.ICommentsRepository)) { commentsContainer.unbind(COMMENTS_SYMBOLS.ICommentsRepository); } + const repo = new CommentsRepository(config, tracer, logger); commentsContainer .bind(COMMENTS_SYMBOLS.ICommentsRepository) - .toConstantValue(new CommentsRepository(config)); + .toConstantValue(repo); + // Use cases + controllers are wrapped with withSpan(withCapture(...)) + // at bind time. bus + queue are accept-and-forward until you add an + // event handler or job — at which point the gen event consume / gen job + // generators inject usage at the // / // + // anchors at the end of the function body. } ```