Files
tech-log-backend/docs/superpowers/specs/2026-07-25-application-outbox-failure-reporting-harness-free-design.md

88 lines
4.3 KiB
Markdown

# Application Outbox Failure Reporting — Harness-Free Design
## Context
`application-core` currently carries Spring Boot and SLF4J only because
`PublishPendingOutboxEventsUseCase` renders relay failures itself. That reverses the diagnostic
dependency direction and also permits a duplicate WARN in `OutboxMessagePublishAdapter`.
This change is harness-free: `src/config/architecture/modules.json`, Gradle, ArchUnit, and focused
module tests are the policy and evidence authorities. No `.harness` files or public paths change.
## Boundary
`application-core` owns a specific `OutboxRelayFailureReportPort` and an immutable
`OutboxRelayFailureReport`. The report is an allowlist containing only:
- `OperationalError code`
- event, aggregate, and correlation identifiers
- event type, attempt count, optional next-attempt time
- the originating `RuntimeException`
It never carries the payload, idempotency key, message template, severity, arbitrary fields, or the
whole `OutboxEvent`. Factories and record invariants admit only retryable
`OUTBOX_PUBLISH_FAILED` reports with a next-attempt time and terminal `OUTBOX_DEAD_LETTER` reports
without one.
`adapter:outbound:messaging` owns `Slf4jOutboxRelayFailureReportAdapter`. It maps the typed report to
one canonical SLF4J 2 fluent ERROR with fixed key names and runbook links. Bootstrap only wires the
port.
## Ordering and Failure Semantics
The persisted FAILED or DEAD transition is authoritative:
1. broker publication fails;
2. the application calculates the transition;
3. the store transition succeeds inside `TransactionPort`;
4. only then is the typed report emitted.
A transition failure propagates and emits no report. A reporter `RuntimeException` is contained by
both the adapter and the use case, so it cannot change the relay outcome or prevent later events
from running. Successful publication and `markPublished` failures emit no failure report.
There is no production no-op reporter. `MessagingConfig` always contributes exactly one reporter
bean, using the configured broker name or `disabled` when blank. `OutboxMessagePublishAdapter`
becomes mapping/send-only: runtime failures propagate, checked failures are wrapped with their
cause, and it emits no success or failure log. The general `OutboundMessagePublisher` retains its
existing fail-open dependency logging.
## Structured ERROR Contract
Every confirmed transition produces one ERROR with the common fields:
`error.code`, `error.category`, `dependency_name`, `dependency_type=messaging`, `outcome`,
`event_id`, `event_type`, `aggregate_id`, `correlation_id`, `attempt_count`, and `runbook_link`.
Retryable failures additionally carry `next_attempt_at`. Mappings are:
| Code | Outcome | Runbook |
| --- | --- | --- |
| `OUTBOX_PUBLISH_FAILED` | `FAILED` | `runbook://outbox/publish-failed` |
| `OUTBOX_DEAD_LETTER` | `DEAD` | `runbook://outbox/dead-letter` |
The originating exception is attached as the throwable. Payload, idempotency key, envelope data,
message templates derived from the exception, and arbitrary exception fields are forbidden.
The adapter's fail-open boundary also applies to invalid direct calls: `report(null)` must never
throw. The focused structured-adapter test pins this behavior.
## Enforcement and Tests
- Value tests enforce invariants and reflectively pin the exact record component allowlist.
- Relay tests pin transition-before-report ordering, no-report paths, exact cardinality, and
reporter containment.
- Messaging tests capture Logback events and pin level, fields, throwable, and unsafe-data absence.
- `verifyApplicationCoreDependencyPurity` rejects non-project production declarations and forbidden
Spring/logging/metrics groups on resolved application classpaths.
- `APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORK` bans SLF4J, JUL, Logback, Log4j, and Micrometer from
the exact `dev.caskeleton.application..` scope. Its dedicated violation fixture also resides
inside that scope, under `dev.caskeleton.application.architecture.violations`, proving the rule
is non-vacuous.
- `application-core` test dependencies are reduced to JUnit Jupiter and AssertJ; all other leaves
keep the shared Spring Boot test baseline.
## Scope
No public path, CI workflow, module-registry edge, payload shape, outbox persistence schema, or
general publisher logging behavior changes. Agents do not stage, commit, amend, or push.