4.3 KiB
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:
- broker publication fails;
- the application calculates the transition;
- the store transition succeeds inside
TransactionPort; - 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.
verifyApplicationCoreDependencyPurityrejects non-project production declarations and forbidden Spring/logging/metrics groups on resolved application classpaths.APPLICATION_HAS_NO_DIAGNOSTIC_FRAMEWORKbans SLF4J, JUL, Logback, Log4j, and Micrometer from the exactdev.caskeleton.application..scope. Its dedicated violation fixture also resides inside that scope, underdev.caskeleton.application.architecture.violations, proving the rule is non-vacuous.application-coretest 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.