docs(plans): amend S04 Step 3 to the background-migration model

What

Amend Step 3 of the database composition-root plan so it specifies the design the step builds: a background migration runner gated through /-/readiness, a constructor-injection dependency seam, and a tier-orchestrated composition root. The amendment is confined to Step 3 (its Scope, Approach, Test seam, Files, and Acceptance entries) and the research bullets it rests on.

Why

The merged Step 3 plan specifies two designs that are unsafe or awkward to build against:

  • Synchronous, boot-aborting migrations. Running migrations as a critical component before the HTTP and probe listeners bind crash-loops a rolling deploy whenever a migration is slow or lock-contended: the startup probe is refused past its grace window and Kubernetes kills the pod before the migration can finish.
  • A type-assertion OCI seam. Gating the OCI mount on a runtime *postgres.Client type assertion couples the format tier to the concrete client type and hides the DB-less unit-test path behind an implicit contract.

The plan is the approval-gate artifact for the implementation MR. This amendment brings it in line with the S04 spec and the buildable design so the two no longer contradict.

What the amended Step 3 specifies

  • Background migration + readiness gating. The runner is wrapped in an asyncMigrationRunner whose Start returns immediately. /-/liveness stays up; /-/readiness reports the migrations check at 503 until the pass completes, then 200. A failed pass cancels the app context (context.WithCancelCause) and exits non-zero, so Kubernetes restarts and retries.
  • Constructor-injection seam. assembleApp builds defaultBootDeps() itself and accepts variadic bootOverride mutators; production passes withFatal(...), tests pass stubs. fatal is a generic process-abort hook that defaults to a no-op.
  • Client-returning tier orchestration. newAppDatabase surfaces the concrete *postgres.Client as a distinct return value (nil under the stub). wireDatabase returns it; a sibling wireServices mounts the OCI tier on it only when non-nil — no type assertion.
  • File layout. The async component lives in migration_runner.go and the injection seam plus wireServices / wireDatabase in wire_database.go, keeping cmd/artifact-registry/main.go scoped to the composition root and CLI action.
  • Scope. Only this hookup's own two checks (migrations, database) are readiness-gated; the broader dependency-aware readiness model (#58) stays out of scope.

The S04 spec is amended in lockstep in the implementation MR.

🤖 Generated with Claude Code

Edited by Suleimi Ahmed

Merge request reports

Loading
Loading