A greenfield referral service ships with four synchronous REST calls to billing, notifications, audit, and analytics. Six months later, adding a fifth consumer — fraud scoring — requires a multi-team release train and a change-advisory slot. The integration count did not explode because the domain grew complex. It exploded because the team chose request-response where an event would have decoupled blast radius. Picking the wrong event-driven pattern costs the same whether you are in fintech or a regional NHS trust: every new consumer becomes a producer dependency.
Structural artifact: Fowler’s four patterns as a decision tree
Martin Fowler’s event-driven taxonomy — event notification, event-carried state transfer, event sourcing, and CQRS — is not a maturity ladder. Each pattern optimises for a different coupling and consistency trade-off. The diagram below is the decision tree we use in architecture reviews before anyone provisions a broker.
flowchart TD
A[State change in service] --> B{Need full history as source of truth?}
B -->|Yes| C[Event Sourcing]
B -->|No| D{Multiple consumers need same event?}
D -->|No, single downstream| E[Request-response or queue - not EDA]
D -->|Yes| F{Consumers need payload in event?}
F -->|No, notify only| G[Event Notification]
F -->|Yes| H{Read and write models diverge?}
H -->|No| I[Event-Carried State Transfer]
H -->|Yes| J[CQRS + read model projection]
C --> K{Read model separate from event log?}
K -->|Yes| J
G --> L[Add Outbox if DB + broker atomicity needed]
I --> L
J --> L
Figure 1: Pattern selection for a single domain event — start with history requirements, not broker brand.
Encore’s event-driven architecture guide makes the same distinction in operational terms: Pub/Sub is the primitive; event sourcing, CQRS, saga, and outbox are patterns you layer on when the domain demands them — not defaults. Confluent’s EDA introduction stresses that most production incidents trace to pattern misuse rather than broker failure — a useful reminder before the procurement deck picks Kafka first.
Pattern reference: when to use each and what breaks
| Pattern | When to use | Failure mode |
|---|---|---|
| Event Notification | Three or more consumers react to the same fact; payload can be fetched via API after notify | Thundering herd on producer API if notify storms without rate limits |
| Event-Carried State Transfer | Consumers are geographically or organisationally separate; you want to avoid chatty callbacks | Large payloads, schema drift across versions, stale embedded state |
| Event Sourcing | Audit trail is regulatory; replay and temporal queries are first-class requirements | Projection lag, complex schema migration on immutable log, storage growth |
| CQRS | Read and write shapes diverge sharply (dashboards vs transactional writes) | Eventual consistency surprises in UI; dual-model operational burden |
| Outbox (supporting) | Database write and event publish must be atomic | Relay process becomes critical path; poison messages block relay |
| Saga (supporting) | Multi-step workflow across services without two-phase commit | Compensation logic untested; choreography becomes incomprehensible past six steps |
Integration tax: what a wrong pattern choice costs
Teams that skip the decision tree pay an integration tax — measurable in sprint capacity and incident load, not slide decks. For a referral microservice crossing several bounded contexts, the recurring line items look like this.
- Producer coupling tax: each new consumer adds an API client, contract test, and deployment dependency in the producer — recurring engineering effort per consumer, every release cycle.
- Schema registry tax: Avro or Protobuf subjects, compatibility checks, and CI gates — platform overhead that grows with event-type count.
- Idempotency tax: at-least-once delivery means every consumer implements dedup keys or naturally idempotent handlers; without this, duplicate referrals or double charges surface in UAT.
- Tracing tax: distributed traces across async boundaries require trace context propagation in every publisher and subscriber; without it, diagnosis time stretches across teams.
- Dead-letter tax: DLQ infrastructure, replay tooling, and runbooks per subscription — neglected queues stall production subscriptions silently.
- Ordering tax: partition-key design, out-of-order compensations, and UI copy explaining eventual consistency — recurring cost, not one-off.
- Test harness tax: ephemeral broker in CI, contract tests against golden events, and snapshot tests on projections — a standing cost for every new async service.
Boundary rules: what stays inside the service
Anti-corruption layers belong at the domain edge, not in the broker. The referral service owns its aggregate lifecycle; it publishes a versioned domain event. Billing translates that into its own invoice model — it does not reach into referral tables. Facades are permitted for legacy HL7v2 bridges; they are not a substitute for declaring the pattern explicitly in service README and ADR-0001.
Acceptance protocol: pick the pattern for a greenfield service
Run this protocol in a sandbox VPC before the first production topic is created. A buyer or lead engineer should complete it in one working session.
- Write the domain event list. Enumerate state changes (max ten for v1). For each, mark consumers and whether they need full payload or notification only.
- Apply the decision tree. Record which Fowler pattern applies per event. If any event requires event sourcing, document retention and replay SLO explicitly.
- Prove atomic publish. Implement outbox table plus relay; kill the broker mid-transaction; verify no orphaned DB rows without events.
- Load-test idempotency. Replay duplicate events at volume; assert exactly-once side effects via dedup table or idempotent keys.
- Measure propagation lag. Define p95 source commit to consumer handler complete for clinical-adjacent paths; attach a dashboard panel.
- Trace across boundaries. Single trace ID visible from HTTP ingress through publish to consumer span; export screenshot for architecture sign-off.
- Document failure mode. One paragraph per pattern choice from the table above, linked in the service runbook.
When bespoke orchestration still makes sense
Event sourcing for an entire hospital PAS is almost never the right first move. It makes sense when regulation demands immutable decision logs (AI triage, financial ledger) or when you must reconstruct state at arbitrary timestamps for incident investigation. CQRS without sourcing is justified when read models require streaming materialised views and writes stay OLTP-normalised. If you have one consumer and need a synchronous answer, use request-response and keep the integration tax at zero.
Broker selection is downstream of pattern choice
Teams often reverse the order: pick Kafka because it is the brand name, then force event sourcing because the broker rewards log retention. The decision tree above should complete before broker sizing. Event notification on managed Pub/Sub handles most referral workflows at moderate throughput. Event sourcing with multi-year retention belongs on a log-based broker with tiered storage — but only when the domain audit requirement is signed by legal, not inferred by architects.
Partition-key design follows pattern choice. CQRS projections keyed by patient identifier preserve order per patient while allowing parallel consumers across the keyspace. Event-carried state transfer with bloated payloads chokes brokers long before CPU does — keep references, not full clinical bundles, in the envelope. Consider ReferralSubmitted: billing and notifications use event notification; analytics uses event-carried state transfer; outbox wraps the DB transaction. No event sourcing unless legal signs the audit requirement. Document the ADR before sprint two.
Start with Pub/Sub and event notification. Add carried state when callback volume hurts. Add CQRS when reads hurt. Add event sourcing when the law or the ledger demands history — not because Kafka appeared on an enterprise licence spreadsheet.

