88 lines
4.3 KiB
Markdown
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.
|