Kafka Event Catalog
All asynchronous service-to-service communication in Olly flows through Kafka topics. Each service writes domain events to a transactional outbox table in the same Postgres transaction as its state change; a background outbox worker (packages/go/middleware/outboxpoll) polls the outbox and publishes to Kafka. Kafka runs as a single broker in docker-compose on dev-2 with replication.factor=1 (there is no MSK/Strimzi/GKE cluster today - see the target-state note below).
Event Envelope
Every published message is a uniform camelCase JSON envelope (canonical type: packages/go/domain/event_envelope.go), produced by the outbox workers (services/*/internal/outbox/worker.go) or, for the DB-free services (identity, policy-admin, eligibility, notifications), stamped directly by the producer:
{
"eventId": "uuid",
"eventType": "quote.calculated",
"occurredAt": "2026-08-29T00:50:12Z",
"partyLocator": "PTY-2026-001797",
"correlationId": "ec26dfca…",
"causationId": "…",
"sessionId": "88e482f4-…",
"activityId": "e949fd74-…",
"activityName": "price",
"schemaVersion": 1,
"payload": { },
"state": { "quote": { } }
}eventId- the outbox row UUID; consumers with aprocessed_eventsinbox table (billing, eligibility) dedupe on it durably, the rest opportunistically.eventType- a dot-separated verb string, e.g.quote.calculated,policy.issued. Every type is defined in the event registry.- The lineage tree answers four nested audit questions:
sessionId(which client session - the browser tab / device / smoke walk, carried as W3C baggage from the frontend) ⊃activityId/activityName(which named journey step, the parent-span analogue) ⊃correlationId(which request/trace) ⊃causationId(which exact event caused this one, e.g. the source event id on everynotification.dispatched). payload- what happened: the business delta, field shapes governed per type by the registry schema.state- event-carried state: the complete subject entity (or named entities,{"quote": …, "policy": …}) frozen at emit time. Locators are immutable pointers into mutable rows;stateis what the entity looked like when the event fired. Outbox services persist it in the outbox row'sstatejsonb column, so the DB keeps the audit copy. Snapshots are sanitized (no Stripe client secrets, no OTP credential material, no PDF bytes).schemaVersion- the registry contract version the event was written under; absent means v1 (every pre-versioning event is retroactively v1). Old events are validated against the contract of their own version (kept in the registry'spriorVersionswith an upcast note).- Message key is a business locator, chosen per type (
domain.MessageKeyOf: policy → party → invoice → document, quote locator for quote events); it is documented per type in the registry.
Schema governance (as-built)
The event registry - packages/go/domain/eventregistry, rendered as the Event Registry catalog - defines every event type: payload JSON Schema, required state subjects, lineage contract, versioning, producers and consumers as code refs, and golden examples that are its test cases. It is enforced at three levels:
- CI - a generic test validates every golden (
valid-*must pass,invalid-*must fail) and lints routing/docs. - e2e -
tests/e2e/registry_compliance_test.goprices a real quote and judges every resulting wire event; an unregistered type or contract violation fails the suite. - Runtime - the debugger service validates every event it ingests and attaches a
contractverdict (registered/valid/schemaVersion/problems/warnings), so contract drift is visible on every live smoke walk.
🚧 Target-state - not yet built
IDL codegen (buf over api/proto/olly/*/v1/, generating Go/TS/Python payload types and buf breaking in CI) remains planned (#1766). When it lands, generated schemas feed the same registry layout; the enforcement machinery above stays.
Live Event Catalogue (as-built)
Grounded in the events actually flowing to BigQuery olly_analytics.events (verified 2026-08-12). The per-domain JSON examples further down this page are older/aspirational; this table is the running set. Kept in sync with the CDC coverage tracker (OllyInsurance/olly#1625).
Every event scales with one of a few base units - the "common denominator": a member, a policy, a claim, an invoice, or a care episode. Multiply the base count by the ratio to forecast volume at any scale. Current base counts: ~2,600 members, 1,370 policies, 1,069 claims, 1,117 invoices, 143 care episodes.
The 34-event core semantic contract below now fires end-to-end (proven by the live e2e suite tests/e2e/*_events_test.go, 34/34, 0 skips), alongside the wider onboarding funnel and lifecycle events, for 65+ distinct event types in total. Grouped by domain, with each event's emit site as path:line (the producer call at the real state transition), validated against the as-built code by a 13-service sweep. These are plain references, not links, on purpose: the producers are deployed and firing but their source is currently uncommitted working-tree state on dev-1 (94 dirty service files), not in any git commit, on origin/main, or on origin/dev2-deploy. So there is no commit a GitHub permalink could resolve against yet; open the path:line in the dev-1 /root/olly checkout to verify. Committing this source (and landing it on main) is tracked in the CDC coverage tracker (#1625).
Policy & enrollment (base: policy)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
policy.issued | A new policy went live for a member | 1 per policy | services/enrollment/internal/service/policy.go:632 |
policy.lapsed | A policy ended (missed payments) | ~0.8 per policy over its life | services/enrollment/internal/service/policy.go:726 |
policy.cancelled | A policy was cancelled before term end | rare | services/enrollment/internal/service/policy.go:169 |
policy.endorsed | A policy amended mid-term | rare | services/enrollment/internal/service/policy.go:347 |
policy.renewed | A policy renewed for a new term | 1 per policy per year | services/enrollment/internal/service/policy.go:299 |
policy.reinstated | A lapsed/cancelled policy was brought back | rare | services/enrollment/internal/service/policy.go:216 |
element.added / element.updated / element.removed | A single coverage line changed | rare | services/enrollment/internal/service/transaction.go:259 |
scheme.member_enrolled | A member was enrolled into an employer scheme | 1 per scheme member | services/enrollment/internal/service/policy.go:647 |
Quote & rating (base: quote priced)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
quote.calculated / premium.rated | A quote was priced / the rating engine computed a premium | 1 per pricing | services/enrollment/internal/service/quote.go:191 / services/enrollment/internal/service/quote.go:207 |
quote.accepted / quote.declined | A prospect accepted / declined a quote | 1 per quote outcome | services/enrollment/internal/service/quote.go:506 / services/enrollment/internal/service/quote.go:405 |
Claims & prior-auth (base: claim)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
claim.submitted | A member filed a claim | 1 per claim | services/claims/internal/service/claim.go:146 |
claim.approved / claim.rejected | Adjudication outcome | 1 per adjudicated claim | services/claims/internal/service/claim.go:277 / services/claims/internal/service/claim.go:498 |
claim.review_required | A claim routed to manual review | high-value / flagged subset | services/claims/internal/service/claim.go:277 |
claim.info_requested / claim.closed | Info requested / claim closed | per claim lifecycle | services/claims/internal/service/claim.go:537 / services/claims/internal/service/claim.go:574 |
claim.payment_initiated / claim.paid | Reimbursement initiated / paid | approved subset | services/claims/internal/service/claim.go:631 / services/claims/internal/service/claim.go:640 |
prior_auth.submitted / prior-auth.decided / prior_auth.escalated | Pre-approval requested / decided / escalated to clinical review | ~1 per 10 claims | services/claims/internal/service/priorauth.go:71 / services/claims/internal/service/priorauth.go:136 / services/claims/internal/service/priorauth.go:103 |
Billing & payments (base: invoice / payment)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
invoice.finalised / invoice.paid | A bill was issued / paid in full | ~1 per policy per cycle | services/billing/internal/service/billing.go:119 / services/billing/internal/service/billing.go:223 |
invoice.overdue / invoice.delinquent / invoice.lapsed | Dunning escalation on an unpaid bill | overdue subset | services/billing/internal/job/lapse.go:193 / services/billing/internal/job/delinquency.go:207 / services/billing/internal/job/delinquency.go:177 |
invoice.void | A bill was voided | rare | services/billing/internal/service/billing.go:529 |
payment.received / payment.settled | Money received / a funnel payment settled (Stripe) | 1 per payment | services/billing/internal/service/billing.go:231 / services/billing/internal/service/onboarding.go:273 |
payment.voided / payment.refunded | A payment reversed / refunded | rare | services/billing/internal/service/billing.go:299 / services/billing/internal/service/billing.go:309 |
charge.created / charge.voided | A charge line created / voided | per charge | services/billing/internal/service/onboarding.go:214 / services/billing/internal/service/billing.go:484 |
adjustment.created / adjustment.applied / adjustment.reversed | Bill correction (credit/debit) raised / applied / reversed | rare | services/billing/internal/service/billing.go:583 / services/billing/internal/service/billing.go:373 / services/billing/internal/service/billing.go:436 |
account.suspended | Billing account suspended for non-payment (account.created is a policy-admin event, below) | rare | services/billing/internal/job/delinquency.go:190 |
installment.created / installment.cancelled | A payment schedule was set up / cancelled | per installment plan | services/billing/internal/service/installment.go:116 / services/billing/internal/service/installment.go:162 |
Eligibility (base: care episode / claim) - point-of-care, transient (CDC-invisible)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
eligibility.coverage.verified / eligibility.coverage.not_found | Cover checked at point of care | ~1+ per episode | services/eligibility/internal/handler/check.go:194 |
eligibility.coverage.changed | Coverage transitioned active/inactive | per policy lifecycle | services/eligibility/internal/projection/handlers.go:312 |
eligibility.accumulators.applied / eligibility.accumulators.reset | Deductible/limit consumed / reset at renewal | per claim / per renewal | services/eligibility/internal/handler/internal.go:225 / services/eligibility/internal/projection/handlers.go:562 |
Provider & credentialing (base: member / provider)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
provider.searched | A member searched the provider directory (behavioural) | many per member | services/provider/internal/handler/providers.go:203 |
provider.created / provider.activated / provider.deactivated | Provider registered / (de)activated in the network | 1 per provider transition | services/provider/internal/handler/providers.go:140 / services/provider/internal/handler/providers.go:287 / services/provider/internal/handler/providers.go:311 |
provider.reviewed | A member reviewed a provider | per review | services/provider/internal/handler/reviews.go:111 |
credentialing.submitted / credentialing.approved / credentialing.rejected | Provider-network onboarding lifecycle | 1 per credentialing request | services/provider/internal/handler/credentialing.go:98 / services/provider/internal/handler/credentialing.go:175 / services/provider/internal/handler/credentialing.go:223 |
Employer / group scheme (base: scheme / member)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
scheme.created / scheme.updated | An employer scheme created / amended | 1 per scheme | services/group-scheme-service/internal/service/scheme.go:128 / services/group-scheme-service/internal/service/scheme.go:183 |
member.added / member.removed | A member added to / removed from a scheme | 1 per membership change | services/group-scheme-service/internal/service/scheme.go:215 / services/group-scheme-service/internal/service/scheme.go:240 (emits scheme.member_removed) |
member.dispatched / member.activated | Gift-box dispatched / member activated | per onboarding member | services/group-scheme-service/internal/service/scheme.go:299 |
bulk_enrollment.completed / bulk_enrollment.failed | A bulk-enrol job finished / failed | 1 per bulk job | services/group-scheme-service/internal/service/bulk_enrollment.go:288 |
Broker (base: broker / policy)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
broker.appointed | A broker was appointed on a scheme | 1 per appointment | services/broker-api/internal/service/authority.go:56 |
broker.authority.breached / broker.authority.updated | Policy issued above delegated authority / authority limit changed | rare | services/broker-api/internal/consumer/projector.go:128 (via projector) / services/broker-api/internal/service/authority.go:74 |
commission.earned / commission.paid | Commission accrued / paid out | 1 per commissioned policy | services/broker-api/internal/service/commission.go:117 / services/broker-api/internal/service/commission.go:208 |
Care (base: care episode)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
care.episode.opened / care.episode.closed / care.episode.cancelled | A care journey started / ended / cancelled | 1 per episode | services/care/internal/service/service.go:131 / services/care/internal/service/service.go:206 / services/care/internal/service/service.go:235 |
care.appointment.confirmed / care.appointment.attended / care.appointment.cancelled / care.appointment.no_show | Appointment lifecycle | ~1 per episode | services/care/internal/service/service.go:555 / services/care/internal/service/service.go:592 / services/care/internal/service/service.go:646 / services/care/internal/service/service.go:662 |
care.prescription.issued / care.prescription.filled / care.prescription.voided | Prescription lifecycle | per prescription | services/care/internal/service/service.go:766 / services/care/internal/service/service.go:808 / services/care/internal/service/service.go:826 |
care.referral.created / care.referral.completed / care.referral.cancelled | Diagnostics/secondary-care referral lifecycle | per referral | services/care/internal/service/service.go:866 / services/care/internal/service/service.go:926 / services/care/internal/service/service.go:944 |
Consent (base: member)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
consent.changed / consent.granted / consent.withdrawn | A data consent changed / granted / withdrawn | ~1 per member | services/consent/internal/handler/consent.go:138 / services/consent/internal/handler/consent.go:155 / services/consent/internal/handler/consent.go:166 |
consent.deletion.requested / deletion.completed / deletion.failed | Right-to-erasure request lifecycle | rare | services/consent/internal/handler/deletion.go:69 / services/consent/internal/job/erasure.go:191 / services/consent/internal/job/erasure.go:156 |
Documents (base: invoice / member)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
document.ready / document.reissued | A PDF (invoice/letter/schedule) generated / regenerated | ~0.7 per invoice | services/document-service/internal/service/document.go:188 |
document.generation_failed | Document generation failed | rare | services/document-service/internal/service/document.go:226 |
document.downloaded | A document was downloaded (audit) | per access | services/document-service/internal/service/document.go:280 |
Member & onboarding (base: member)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
member.party_created | A member/party record first created | 1 per member | services/policy-admin/internal/service/party.go:94 |
party.updated | A party record was modified | per edit | services/policy-admin/internal/service/party.go:177 |
product.published / account.created | A product version published / an account opened (policy-admin) | per config change | services/policy-admin/internal/service/product.go:275 / services/policy-admin/internal/service/account.go:70 |
onboarding.* | Onboarding funnel (identity is DB-free, so the event stream IS the record): account_created, otp_sent/verified, magic_link_sent/redeemed, pin_issued/validated, password_set, login_success, plus google/slack import, secondary-email, waitlist and employer-activation branches | ~6+ per onboarding member | services/identity/internal/handler/member_onboarding.go:77 (~25-type funnel) |
Triage (clinical) (base: triage session)
| Event | What it means | Scales with | Emit site (path:line) |
|---|---|---|---|
triage.session.started / triage.session.completed | A triage session opened / reached a terminal state | 1 per session | services/triage/src/triage/v5_narrow.py:785 / services/triage/src/triage/chat_completions.py:716 |
triage.disposition.reached | A clinical outcome (care type + disposition) was determined | 1 per completed session | services/triage/src/triage/chat_completions.py:707 |
triage.red_flag.identified | A clinical red-flag category was raised (safety signal) | per flagged turn | services/triage/src/triage/rf_sidecar.py:701 |
triage.pathway.selected / triage.summary.created | A pathway was committed / a clinician summary written | per session | services/triage/src/triage/v5_narrow.py:832 / services/triage/src/triage/summary_store.py:201 |
fhir.record.created | A clinical FHIR record pushed to the GCP Healthcare store | ~1 per media upload | services/triage/src/triage/v5_media_api.py:575 |
Coverage note
The two-layer model: named semantic events (above, olly_analytics.events) are the business-event contract; the raw log-based CDC layer (olly_cdc) independently captures every row change of every entity as a completeness safety net. The remaining gap is a small tail of empty child tables + low-value operational signals, tracked on the CDC coverage tracker (#1625).
Event surface: needed vs emitted (the gap)
A 13-domain code audit put the complete member-journey event surface at 136 events. Status (2026-08-13, after the semantic-event build-out):
| Status | Count | Meaning |
|---|---|---|
| Firing live | 65+ distinct types | Observed in olly_analytics.events - the whole core contract across claims, policy, billing, eligibility, provider, care, consent, broker, triage and onboarding |
| Built this pass | 48 | The previously-absent named events (14 P0 + 25 P1 + 9 P2), now emitting at their real state transitions |
| Absent | small tail | Mostly empty child tables + low-value operational signals; the P0/P1/P2 core-journey gaps are closed |
The full 34-event core semantic contract now fires end-to-end, proven by a live e2e suite (tests/e2e/*_events_test.go) that drives each real transition and asserts the event lands on its topic - 34/34, 0 skips.
Two layers - be precise about "moved to BigQuery": the raw log-based CDC layer (olly_cdc) moves every state change of every entity to BigQuery (every policy update/renewal, every new policy/product creation, verified live). The named semantic events sit on top - the meaningful business signals (policy.renewed, commission.earned, triage.disposition.reached), not raw diffs.
P0 core-journey events - all now BUILT + firing (were the original gaps):
| Domain | P0 events (now live) |
|---|---|
| Eligibility | eligibility.coverage.verified/not_found/changed, eligibility.accumulators.applied |
| Provider | provider.created/activated, credentialing.submitted/approved/rejected |
| Triage | triage.session.started/completed, triage.disposition.reached, triage.red_flag.identified, fhir.record.created |
| Employer (group scheme) | scheme.created, member.added/activated/dispatched, bulk_enrollment.completed/failed |
| Broker | commission.earned/paid, broker.appointed, broker.authority.breached |
| Member onboarding | member.party_created, onboarding.* (full funnel) |
| Claims / Billing | claim.rejected/paid/closed/review_required, prior_auth.decided/escalated, payment.settled/voided, invoice.overdue/delinquent/lapsed, account.suspended |
The full breakdown (built / firing / remaining tail, per domain, with priorities) is maintained on the CDC coverage tracker (OllyInsurance/olly#1625).
Core Insurance Journeys - the Kafka event contract
Principle. The core insurance journeys are first-class, well-defined Kafka events - a stable semantic contract every service and the warehouse relies on. Raw log-based CDC (olly_cdc) is the completeness safety-net (it captures every row change so nothing is ever lost); it is not the contract for core journeys. If a step is a core-journey milestone, it MUST be an explicit event - never inferred from a CDC row diff.
Each journey is an ordered event chain. Status: [live] fires today (built this pass or already emitting), [build] still absent (no distinct state transition in code yet).
1. Quote → Buyquote.created [build] → quote.calculated [live] → premium.rated [live] → quote.underwritten [build] → quote.accepted [live] → policy.issued [live] → policy.activated [build]
2. Member onboardingmember.registered [live] → member.email_verified [live] → member.party_created [live] → onboarding.* funnel [live] → consent.granted [live]
3. Health & care (the clinical loop)triage.session.started [live] → triage.disposition.reached [live] → eligibility.coverage.verified [live] → care.episode.opened [live] → care.appointment.confirmed [live] → care.appointment.attended [live] → fhir.record.created [live] → care.episode.closed [live]
4. Claim → reimbursementclaim.submitted [live] → prior-auth.decided [live] → claim.approved [live] / claim.rejected [live] → claim.paid [live]
5. Billing & paymentsinvoice.finalised [live] → payment.attempted [build] → payment.received [live] / payment.settled [live] → (dunning) invoice.overdue/invoice.delinquent [live] → policy.lapsed [live]
6. Policy lifecycle & renewaltransaction.created [build] → transaction.priced [build] → transaction.underwritten [build] → policy.renewed [live] / policy.endorsed [live] / policy.cancelled [live] → policy.reinstated [live]
7. Employer onboardingscheme.created [live] → bulk_enrollment.started [build] → bulk_enrollment.completed [live] → member.activated [live]
8. Provider networkcredentialing.submitted [live] → credentialing.approved [live] / credentialing.rejected [live] → provider.activated [live]
9. Brokerbroker.appointed [live] → commission.earned [live] → commission.paid [live]
What remains. The [build] markers above are the small tail: pre-issue quote/transaction lifecycle events (quote.created, quote.underwritten, transaction.*), payment.attempted (decline capture), and a couple of enum-only states (policy.activated, bulk_enrollment.started) with no distinct transition in code yet. All the P0 core-journey milestones (clinical loop, employer onboarding, provider network, broker money, member onboarding) now fire; CDC (olly_cdc) stays as the safety-net underneath for everything.
Topic Naming Convention
Topics are one per service, named <service>.events:
claims.events eligibility.events enrollment.events
billing.events provider.events care.events consent.eventsEvery event type a service emits (e.g. CLAIM_SUBMITTED, INVOICE_GENERATED) is written to that service's single <service>.events topic; the eventType envelope field discriminates them. Topic names use dot-separated lowercase segments only.
Historical note
The 3-segment {domain}.{entity}.{event} subject names (e.g. claims.claim.submitted) used elsewhere on this page are legacy/aspirational and do not match the running topology, which is per-service <service>.events. Treat any 3-segment topic name below as the logical event class, not the physical Kafka topic.
Consumer group naming: groups follow {service}-{domain}-consumer, e.g. billing-claims-consumer, notifications-enrollment-consumer, eligibility-enrollment-consumer. Consumers maintain durable per-partition projection checkpoints (CQRS-lite read models).
Default Topic Configuration
Unless noted otherwise, topics are created with these defaults:
| Parameter | Default | Notes |
|---|---|---|
replication.factor | 1 | Single broker in docker-compose (dev-2). No MSK/multi-broker cluster today. |
retention.ms | 604800000 (7 days) | Sufficient for consumer lag recovery |
compression.type | lz4 | Good ratio/speed balance |
max.message.bytes | 1048576 (1 MB) | |
cleanup.policy | delete | Log compaction not used; events are time-bounded |
partitions | 6 |
🚧 Target-state - not yet built
Multi-broker durability (replication.factor=3, min.insync.replicas=2, acks=all), per-topic partition tuning, and 12-partition/14-day high-volume topics are all planned for a managed-Kafka production target. None are provisioned today (infra/terraform and infra/k8s contain only .gitkeep).
Claims Domain
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
claims.claim.submitted | 12 | 14 days | Claims Service | Billing Service, Notifications Service |
claims.claim.adjudicated | 12 | 14 days | Claims Service | Billing Service, Notifications Service, EDI (Mirth Connect) |
claims.claim.paid | 6 | 7 days | Billing Service (primary), Claims Service (audit mirror) | Notifications Service, OpenSearch |
claims.prior_auth.decision | 6 | 7 days | Claims Service | Notifications Service, Eligibility Service |
claims.appeal.resolved | 6 | 7 days | Claims Service | Notifications Service, Billing Service |
claims.claim.submitted
Event types: CLAIM_SUBMITTED
{
"eventType": "CLAIM_SUBMITTED",
"claimId": "uuid",
"memberId": "uuid",
"providerId": "uuid",
"planId": "uuid",
"claimType": "PROFESSIONAL",
"totalBilledAmount": "250.00",
"serviceDate": "2026-01-15",
"submittedAt": "2026-01-16T10:00:00Z",
"requiresPriorAuth": true,
"priorAuthId": "uuid-or-null"
}claims.claim.paid
Event types: CLAIM_PAID
Producer: Billing Service. Consumers: Notifications Service, OpenSearch.
{
"event_type": "CLAIM_PAID",
"claim_locator": "CLM-789",
"party_locator": "PTY-456",
"policy_locator": "POL-123",
"amount_paid": 2750.50,
"payment_method": "EFT",
"remittance_advice_ref": "edi-835-2026-06-08-00004",
"paid_at": "2026-06-08T15:00:00Z"
}claims.prior_auth.decision
Event types: PRIOR_AUTH_DECISION
Producer: Claims Service. Consumers: Notifications Service, Eligibility Service.
{
"event_type": "PRIOR_AUTH_DECISION",
"prior_auth_locator": "PA-321",
"claim_locator": null,
"party_locator": "PTY-456",
"decision": "APPROVED",
"reason_code": null,
"approved_units": 12,
"expires_at": "2026-07-08T00:00:00Z",
"decided_at": "2026-06-08T13:30:00Z",
"decided_by": "underwriting-rule-engine"
}decision enum: APPROVED / DENIED / NEEDS_INFO. claim_locator is null when the authorisation is pre-claim.
claims.appeal.resolved
Event types: APPEAL_RESOLVED
Producer: Claims Service. Consumers: Notifications Service, Billing Service.
{
"event_type": "APPEAL_RESOLVED",
"appeal_locator": "APL-654",
"claim_locator": "CLM-789",
"party_locator": "PTY-456",
"outcome": "OVERTURNED",
"new_amount_paid": 2950.00,
"reviewer": "claims-appeals-team",
"resolved_at": "2026-06-08T16:00:00Z"
}outcome enum: OVERTURNED / UPHELD / WITHDRAWN. new_amount_paid is null unless outcome is OVERTURNED.
claims.claim.adjudicated
Event types: CLAIM_APPROVED, CLAIM_DENIED
{
"eventType": "CLAIM_APPROVED",
"claimId": "uuid",
"memberId": "uuid",
"providerId": "uuid",
"status": "APPROVED",
"totalBilledAmount": "250.00",
"allowedAmount": "180.00",
"paidAmount": "144.00",
"memberResponsibility": "36.00",
"deductibleApplied": "0.00",
"copayApplied": "36.00",
"coinsuranceApplied": "0.00",
"adjudicatedAt": "2026-01-17T14:30:00Z",
"eobS3Key": "eob/2026/01/uuid.pdf"
}{
"eventType": "CLAIM_DENIED",
"claimId": "uuid",
"memberId": "uuid",
"providerId": "uuid",
"denialReasonCode": "CO-4",
"denialReasonText": "Service requires prior authorization",
"appealDeadline": "2026-02-17",
"adjudicatedAt": "2026-01-17T14:30:00Z"
}Eligibility Domain
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
eligibility.coverage.verified | 6 | 7 days | Eligibility Service | Claims Service |
eligibility.coverage.terminated | 6 | 7 days | Eligibility Service | Claims Service, Notifications Service |
eligibility.coverage.verified
Event types: COVERAGE_VERIFIED, COVERAGE_NOT_FOUND
{
"eventType": "COVERAGE_VERIFIED",
"memberId": "uuid",
"planId": "uuid",
"groupNumber": "GRP-00123",
"memberNumber": "MEM-456789",
"effectiveDate": "2026-01-01",
"terminationDate": null,
"coverageType": "MEDICAL",
"networkTier": "IN_NETWORK",
"deductibleRemaining": "1500.00",
"oopMaxRemaining": "4000.00",
"verifiedAt": "2026-01-16T10:01:00Z"
}COVERAGE_NOT_FOUND variant - emitted when no active policy exists for the member on the requested service date:
{
"event_type": "COVERAGE_NOT_FOUND",
"verified_at": "2026-06-08T13:42:00Z",
"party_locator": "PTY-456",
"service_date": "2026-06-08",
"lookup_reason": "no_active_policy",
"deductible_remaining": null,
"oop_max_remaining": null,
"policy_locator": null
}lookup_reason values: no_active_policy, policy_lapsed, member_not_found.
Enrollment Domain
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
enrollment.enrollment.submitted | 6 | 7 days | Enrollment Service | Eligibility Service, Billing Service, Notifications Service |
enrollment.enrollment.activated | 6 | 7 days | Enrollment Service | Eligibility Service, Billing Service, Notifications Service |
enrollment.enrollment.terminated | 6 | 7 days | Enrollment Service | Eligibility Service, Billing Service, Notifications Service |
enrollment.enrollment.plan_changed | 6 | 7 days | Enrollment Service | Eligibility Service, Billing Service, Notifications Service |
enrollment.cobra.notice_sent | 6 | 7 days | Enrollment Service | Notifications Service |
enrollment.cobra.elected | 6 | 7 days | Enrollment Service | Eligibility Service, Billing Service, Notifications Service |
enrollment.enrollment.submitted
Event types: ENROLLMENT_SUBMITTED
{
"eventType": "ENROLLMENT_SUBMITTED",
"enrollmentId": "uuid",
"memberId": "uuid",
"groupId": "uuid-or-null",
"planId": "uuid",
"coverageType": "MEDICAL",
"electionType": "NEW",
"effectiveDate": "2026-01-01",
"tierCode": "EE+FAM",
"memberPremium": "480.00",
"totalPremium": "1200.00",
"dependentCount": 2,
"submittedAt": "2025-11-15T10:00:00Z"
}enrollment.enrollment.activated
Event types: ENROLLMENT_ACTIVATED
Producer: Enrollment Service. Consumers: Eligibility Service, Billing Service, Notifications Service.
{
"event_type": "ENROLLMENT_ACTIVATED",
"policy_locator": "POL-123",
"policy_term_locator": "PT-001",
"account_locator": "ACC-789",
"party_locators": ["PTY-456", "PTY-457"],
"effective_from": "2026-07-01",
"effective_to": "2027-06-30",
"premium_annual": 4800.00,
"activated_at": "2026-06-08T14:00:00Z"
}enrollment.enrollment.terminated
Event types: ENROLLMENT_TERMINATED
Producer: Enrollment Service. Consumers: Eligibility Service, Billing Service, Notifications Service.
{
"event_type": "ENROLLMENT_TERMINATED",
"policy_locator": "POL-123",
"policy_term_locator": "PT-001",
"party_locators": ["PTY-456"],
"termination_reason": "MEMBER_REQUEST",
"effective_date": "2026-09-01",
"terminated_at": "2026-06-08T16:00:00Z"
}termination_reason enum: MEMBER_REQUEST / NON_PAYMENT / EMPLOYER_TERMINATION / END_OF_TERM.
enrollment.enrollment.plan_changed
Event types: PLAN_CHANGED
Producer: Enrollment Service. Consumers: Eligibility Service, Billing Service, Notifications Service.
{
"event_type": "PLAN_CHANGED",
"policy_locator": "POL-123",
"previous_product_version_locator": "PV-456",
"new_product_version_locator": "PV-457",
"effective_date": "2026-07-01",
"premium_delta_annual": 600.00,
"changed_at": "2026-06-08T16:30:00Z",
"changed_by": "admin-portal-user"
}enrollment.cobra.notice_sent
Event types: COBRA_NOTICE_SENT
Producer: Enrollment Service. Consumer: Notifications Service.
{
"event_type": "COBRA_NOTICE_SENT",
"policy_locator": "POL-123",
"party_locator": "PTY-456",
"qualifying_event": "TERMINATION_OF_EMPLOYMENT",
"election_deadline": "2026-09-01",
"monthly_premium": 480.00,
"notice_sent_at": "2026-06-08T17:00:00Z"
}enrollment.cobra.elected
Event types: COBRA_ELECTED
Producer: Enrollment Service. Consumers: Eligibility Service, Billing Service, Notifications Service.
{
"event_type": "COBRA_ELECTED",
"policy_locator": "POL-123",
"party_locator": "PTY-456",
"cobra_coverage_start": "2026-09-01",
"cobra_coverage_end": "2027-08-31",
"first_premium_due": "2026-09-15",
"elected_at": "2026-06-08T18:00:00Z"
}Billing Domain
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
billing.invoice.generated | 12 | 14 days | Billing Service | Notifications Service |
billing.payment.received | 6 | 7 days | Billing Service | Enrollment Service, Notifications Service |
billing.payment.missed | 6 | 7 days | Billing Service | Enrollment Service, Notifications Service |
billing.payment.completed | 6 | 7 days | Billing Service | Claims Service, Notifications Service |
billing.invoice.generated
Event types: INVOICE_GENERATED
Producer: Billing Service. Consumer: Notifications Service.
{
"event_type": "INVOICE_GENERATED",
"invoice_locator": "INV-2026-000123",
"account_locator": "ACC-789",
"policy_term_locator": "PT-001",
"total_amount": 400.00,
"currency": "GBP",
"due_date": "2026-07-01",
"generated_at": "2026-06-08T14:00:00Z"
}billing.payment.received
Event types: PAYMENT_RECEIVED
Producer: Billing Service. Consumers: Enrollment Service, Notifications Service.
{
"event_type": "PAYMENT_RECEIVED",
"payment_locator": "PAY-456",
"account_locator": "ACC-789",
"amount": 400.00,
"currency": "GBP",
"method": "DIRECT_DEBIT",
"external_ref": "GoCardless-PMT-xyz",
"received_at": "2026-06-08T13:00:00Z"
}billing.payment.completed
Event types: PAYMENT_COMPLETED
Producer: Billing Service. Consumers: Claims Service, Notifications Service.
{
"event_type": "PAYMENT_COMPLETED",
"payment_locator": "PAY-456",
"account_locator": "ACC-789",
"invoice_locator": "INV-2026-000123",
"amount": 400.00,
"currency": "GBP",
"completed_at": "2026-06-08T13:00:30Z"
}billing.payment.missed
Event types: PAYMENT_MISSED, GRACE_PERIOD_STARTED, GRACE_PERIOD_EXPIRED
{
"eventType": "GRACE_PERIOD_STARTED",
"memberId": "uuid",
"invoiceId": "uuid",
"gracePeriodEndDate": "2026-03-30",
"premiumCents": 48000,
"missedAt": "2026-02-28T23:59:59Z"
}Provider Domain
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
provider.credentialing.status_changed | 6 | 7 days | Provider Service | Claims Service, Eligibility Service, Notifications Service |
provider.network.updated | 6 | 7 days | Provider Service | Claims Service, Eligibility Service |
provider.network.updated
Event types: NETWORK_UPDATED
Producer: Provider Service (Temporal credentialing workflow). Consumers: Claims Service, Eligibility Service.
{
"event_type": "NETWORK_UPDATED",
"provider_locator": "PROV-987654",
"previous_status": "PENDING",
"new_status": "ACTIVE",
"changed_at": "2026-06-08T14:30:00Z",
"changed_by": "credentialing-temporal-workflow",
"credentialing_request_locator": "CRED-321"
}new_status enum: ACTIVE / INACTIVE / SUSPENDED / TERMINATED.
provider.credentialing.status_changed
Event types: CREDENTIALING_APPROVED, CREDENTIALING_DENIED, CREDENTIALING_EXPIRED, CREDENTIALING_SUSPENDED
{
"eventType": "CREDENTIALING_APPROVED",
"providerId": "uuid",
"npi": "1234567890",
"providerName": "Jane Smith MD",
"specialty": "Orthopedic Surgery",
"credentialingStatus": "APPROVED",
"networkStatus": "IN_NETWORK",
"effectiveDate": "2026-02-01",
"approvedAt": "2026-01-28T10:00:00Z"
}EDI Domain
🚧 Target-state - not yet built (and out of market scope)
Olly operates in the United Kingdom (NHS-111 pathways, ICO/UK-GDPR, Companies House, GBP). The X12/EDI transaction set below (837/834/270/271/834/835) is a US clearinghouse operating model carried over from an earlier US plan. None of it is built - there is no EDI ingestion, no Mirth-driven X12 pipeline, and no edi.* topics in the running system. This section is retained only as historical/aspirational reference and should not be treated as current state.
| Topic | Partitions | Retention | Producer | Consumers |
|---|---|---|---|---|
edi.inbound.received | 6 | 30 days | EDI (Mirth Connect) | Claims Service, Enrollment Service, Eligibility Service |
edi.outbound.generated | 6 | 30 days | EDI (Mirth Connect) | OpenSearch (audit index) |
Event types for inbound: EDI_837_RECEIVED, EDI_834_RECEIVED, EDI_270_RECEIVED Event types for outbound: EDI_835_GENERATED, EDI_834_GENERATED, EDI_271_GENERATED
Raw EDI storage. Raw X12 documents are too large for Kafka payloads. Each event carries a
raw_uripointing to the full document:gs://olly-prod-edi-{inbound,outbound}/in production (GCS),s3://olly-dev-edi-{inbound,outbound}/as a dev backup. Consumers that need to inspect the document fetch it by URI using themessage_idas the filename stem. The Kafka payload itself is metadata + pointer only.
edi.inbound.received - inbound events
EDI_837_RECEIVED (claim submission batch)
Producer: Mirth Connect. Consumers: Claims Service, Enrollment Service, Eligibility Service.
{
"event_type": "EDI_837_RECEIVED",
"message_id": "edi-837-2026-06-08-00001",
"received_at": "2026-06-08T13:42:11Z",
"submitter_id": "PROV-987654",
"transaction_count": 24,
"interchange_control_number": "000001234",
"raw_uri": "gs://olly-prod-edi-inbound/2026/06/08/edi-837-00001.x12"
}submitter_id maps to EDI ISA06. transaction_count is the number of individual claims in the batch.
EDI_834_RECEIVED (member enrollment batch)
Producer: Mirth Connect. Consumers: Enrollment Service.
{
"event_type": "EDI_834_RECEIVED",
"message_id": "edi-834-2026-06-08-00002",
"received_at": "2026-06-08T13:50:00Z",
"sponsor_id": "EMP-123",
"member_count": 1850,
"interchange_control_number": "000001235",
"raw_uri": "gs://olly-prod-edi-inbound/2026/06/08/edi-834-00002.x12"
}sponsor_id maps to EDI ISA08 (employer/group sponsor).
EDI_270_RECEIVED (eligibility inquiry)
Producer: Mirth Connect. Consumer: Eligibility Service.
{
"event_type": "EDI_270_RECEIVED",
"message_id": "edi-270-2026-06-08-00003",
"received_at": "2026-06-08T13:52:00Z",
"submitter_id": "PROV-987654",
"subscriber_id": "MEM-456",
"service_date": "2026-06-08",
"raw_uri": "gs://olly-prod-edi-inbound/2026/06/08/edi-270-00003.x12"
}edi.outbound.generated - outbound events
EDI_835_GENERATED (payment remittance)
Producer: Mirth Connect (triggered by Billing Service). Consumer: OpenSearch (audit index).
{
"event_type": "EDI_835_GENERATED",
"message_id": "edi-835-2026-06-08-00004",
"generated_at": "2026-06-08T15:00:00Z",
"payer_id": "OLLY-1",
"payee_id": "PROV-987654",
"claim_count": 22,
"total_paid": 42500.75,
"interchange_control_number": "000005001",
"raw_uri": "gs://olly-prod-edi-outbound/2026/06/08/edi-835-00004.x12"
}EDI_834_GENERATED (enrollment confirmation outbound)
Producer: Mirth Connect (triggered by Enrollment Service). Consumer: OpenSearch (audit index).
{
"event_type": "EDI_834_GENERATED",
"message_id": "edi-834-out-2026-06-08-00005",
"generated_at": "2026-06-08T16:00:00Z",
"sponsor_id": "EMP-123",
"member_count": 1850,
"interchange_control_number": "000005002",
"raw_uri": "gs://olly-prod-edi-outbound/2026/06/08/edi-834-00005.x12"
}EDI_271_GENERATED (eligibility response)
Producer: Mirth Connect (triggered by Eligibility Service). Consumer: OpenSearch (audit index).
{
"event_type": "EDI_271_GENERATED",
"message_id": "edi-271-2026-06-08-00006",
"generated_at": "2026-06-08T13:52:05Z",
"submitter_id": "PROV-987654",
"subscriber_id": "MEM-456",
"service_date": "2026-06-08",
"eligible": true,
"deductible_remaining": 250.00,
"oop_max_remaining": 4800.00,
"raw_uri": "gs://olly-prod-edi-outbound/2026/06/08/edi-271-00006.x12"
}Schema governance
Current state: there is none. Messages are plain versionless JSON (see Event Envelope). There is no Avro encoding, no wire-format magic prefix, no Confluent/_schemas registry, and no compatibility checking. payload field names are whatever the producer writes; consumers must match casing exactly.
🚧 Target-state - not yet built
Introducing a schema registry with explicit event versioning is a real planned improvement. The motivation is concrete: three production incidents to date were caused by producer/consumer field-casing drift in the unversioned JSON payloads (e.g. policyLocator casing, PascalCase, and snake_case mismatches). A registry with BACKWARD compatibility, a per-topic subject naming strategy, and CI-enforced schema evolution would catch these at publish time. None of this exists yet - do not encode Avro, register subjects, or emit a schemaVersion field in current code.
Delivery, retries and idempotency
Current state. Consumers read from <service>.events and update their local projection (read-model) tables, tracking durable per-partition checkpoints. Delivery semantics today:
- Producer side. The outbox worker publishes and marks rows published in separate steps. If a service restarts between the Kafka write and the
MarkPublishedstamp, the row is re-written on the next poll, so a given event can be delivered more than once (at-least-once). The outbox fetch is a plainSELECT ... WHERE published_at IS NULL ORDER BY created_at ASC LIMIT nwith noFOR UPDATE SKIP LOCKED- publishing is correct only because exactly one replica runs the worker today. - Consumer side. Consumers dedupe opportunistically on the envelope
eventId, and idempotent handlers guard against re-application via state-transition checks (ErrInvalidTransition). There is no dedicated inbox/dedup table, no Valkey positional-key guard, and no dead-letter queue: a message that fails to process is retried on redelivery, not routed to a.dlqtopic.
🚧 Target-state - not yet built
The following are planned but not implemented today:
- A dead-letter queue (
<topic>.dlq) with bounded retries + backoff and an alerting DLQ consumer. - Exactly-once-ish consumer idempotency via a durable inbox table (or Valkey guard) keyed on
eventId. - Multi-replica-safe publishing via
FOR UPDATE SKIP LOCKEDon the outbox fetch, so more than one outbox worker can run without double-publishing.
Do not assume any of these exist when reasoning about failure handling in the current build.
