ADR-016: Event-Driven convert_report — 202 Accepted with Eventual Consistency
On this page
Status
Mooted (2026-04-22) — decision no longer required.
The question this ADR was framed to answer — "should the cross-service convert_report flow be synchronous HTTP or event-driven?" — assumed two stateful services (craig-intake and craig-cases) that each hold part of a report’s lifecycle and must coordinate transitions between them. During review, we challenged that assumption and concluded that intake does not, in fact, need to hold report state at all.
With stateless intake (ADR-017), the flow stops being a cross-service coordination problem:
-
The report is written to craig-cases on submission (intake validates at the edge and forwards).
-
Screening, disposition (screened-in / screened-out / screened-out-and-referred / I&R), and conversion to referral all become internal state transitions within craig-cases — single-service, atomic, no distributed transaction surface.
-
There is no "intake row vs cases row" to keep in sync, no
convertingintermediate state, no DLQ for the convert flow specifically, and no polling contract for callers.
The sync-vs-event design space this ADR enumerated is therefore void for this use case. Events continue to play their existing role in CRAIG — post-persistence notifications on the craig.events topic exchange (ADR-003) — and the convert_report transition, now internal to cases, publishes case.report_converted the same way other cases transitions already publish their notifications.
Why this ADR is kept in the tree
Mooted ADRs are preserved, not deleted. The question this ADR asked was a legitimate one given the information we had at the time, and the analysis walked through the trade-offs (atomicity, availability coupling, JWT forwarding, backpressure) that led the reviewers to interrogate the deeper assumption. That chain of reasoning is valuable context for anyone re-encountering the same surface question in the future.
The original Proposed content is preserved below for historical reference.
Original (historical — Proposed 2026-04-22, mooted same day)
|
The content below reflects the ADR as originally drafted. It is retained unchanged for historical reference. The direction it proposes was not adopted — see ADR-017 for the accepted decision. |
Context (original)
POST /api/internal/reports/{id}/convert in services/craig-intake/src/api/internal.rs promotes a screened-in intake report to a referral in craig-cases. The 96-line handler runs a synchronous cross-service HTTP POST to craig-cases, parses the response, updates intake’s own DB row, and publishes a fire-and-forget intake.report_converted event. The design had four structural problems as drafted:
-
Distributed transaction without a coordinator. Intake row update and cases referral insert are in separate services with no atomicity.
-
Tight coupling. Intake availability is gated on craig-cases availability.
-
JWT forwarding. The caseworker’s bearer token is extracted from request headers and forwarded to craig-cases, which works for interactive HTTP but breaks for non-HTTP initiators (queued batch conversions, SDK callers using signing, CLI flows with short-lived credentials).
-
No backpressure. A burst of convert calls generates parallel synchronous craig-cases writes with no queueing, retry, or rate shaping.
Decision (original — not adopted)
Replace the synchronous HTTP call with an event-driven flow over the existing craig.events topic exchange. New intake.report_convert_requested event published by intake; craig-cases subscribes and creates the referral; craig-intake subscribes to case.referral_created filtered by intake_report_id and closes the loop by updating its own row. API contract changes from 200 OK with the updated report to 202 Accepted with a Location header, requiring clients (SDKs, CLI, craig-web BFF, E2E tests) to poll or subscribe for the terminal state.
Why this was not adopted
Review surfaced a deeper question: does intake need to hold report state in its own database at all? On examination, every use case previously cited as justification (preventing double-conversion, intake-side dashboards, audit trail, AFCARS reporting) can be served from cases as the single source of truth. Intake’s terminal states screened_in / converted / etc. only exist to mirror cases-side decisions; they are not independently meaningful.
Further feedback from practitioners reinforced this: the work is a chain (intakes → referrals → investigations → cases, plus cross-chain family relationships), and staff need to see the chain in one place. Georgia-specific workflow elements (CICC-to-county disposition override) and the four-path disposition model (screened-in, screened-out, screened-out-and-referred, I&R, with NCANDS retention across all paths) all involve ongoing caseworker action regardless of disposition — which means none of these paths can live in a separate store from the rest of case-management work.
Once that premise changes, the sync-vs-event design space this ADR enumerated becomes moot: there is no longer a cross-service coordination problem to design. ADR-017 captures the adopted direction.