Event Driven Architecture
Purpose
Decide whether two components should exchange a fact or a call. An event is an
immutable observation about something that happened in the publisher's domain
(OrderPlaced); a command is an instruction to a named recipient with an expected outcome
(ShipOrder); request/response can carry a command or a query and returns an outcome. An
asynchronous command can return outcome later through status, callback or event. This
is a coupling decision, not a technology one, and the honest answer is often
request/response — a broker between two parties that each need the other's outcome buys a
new failure domain, added latency and no answer.
The failure this prevents is the distributed monolith: services that talk only over a broker
yet cannot be released independently, because one team's event is another team's function
call in disguise. The second is the flow that exists nowhere — every step is a handler,
the sequence is emergent, and answering "why did this order never ship" means reconstructing
it from logs.
Workflow
- Name the semantic contract, not just the tense. Past tense is a useful event smell;
imperative naming suggests a command. Verify ownership, recipient, whether rejection is
possible, and whether the message remains meaningful with no consumer.
- Ask when and where the outcome is needed. An outcome needed in the current latency
budget favors request/response. Deferred completion may use an addressed async command
with status/callback; independent reactions to a fact favor events.
- Choose coordination from flow semantics. Independent reactions can choreograph. A
branching business workflow needing explicit state, deadlines, compensation or one
recovery owner favors orchestration—participant count alone is not a threshold.
- Design the payload. Decide what the event carries versus what the consumer fetches,
and name the authority for the current value —
references/event-design.md.
- Fix the compatibility direction and the window. Historical reader support follows the
oldest data that can reappear from topics, archives or DLQs; old-reader/new-writer overlap
follows deployment and consumer support policy. These are not one additive duration;
use
schema-evolution-and-compatibility for the format-specific contract.
- Prove the commit boundary. A local DB transaction does not include an ordinary broker
send. Use an outbox/CDC, an explicitly enlisted XA resource, or a broker-local transaction
whose exact boundary fits; “before versus after commit” alone leaves a failure window.
- Choose the consumer's runtime last — long-lived process or FaaS — from throughput,
burst shape and whether a partition assignment must be held.
Inspect broker/client, serializer and Java/framework versions plus retention, replay and retry
configuration before implementation advice. The envelope reference uses Java 16+ record syntax;
preserve the target rather than upgrading it. Deliver the interaction choice, outcome/recovery
owner, commit boundary, reader/writer horizon and one confirming failure/compatibility case.
If these facts are missing, state a conditional choice and the smallest contract/configuration
evidence needed to resolve it.
Decision block
Publish an event when:
- the producer completes its own work without the consumer's outcome
- the consumer set is open: a new reader must be addable without changing the producer
- consumer unavailability must not bound producer availability, and a backlog is acceptable
- fan-out or replay from retained history is a requirement, not a nice-to-have
Avoid events when:
- the caller must answer its own caller with the result (any synchronous read path)
- there is one known recipient, the message is semantically a command, and no buffering,
replay or asynchronous completion requirement justifies the broker
- the producer needs to know the work was rejected, and the rejection is a business outcome
- the boundary has no independent lifecycle/scaling/resilience driver and the broker only
obscures a synchronous dependency
Prefer request/response instead when:
- the interaction is a query. Publishing an event to ask a question is a request/response
interaction requiring correlation, timeout and reply lifecycle; model it as such
- the outcome must be surfaced to a user inside the current request
- the consumer count is one and stable, and the added broker is pure operational surface
Rules
- Events can reduce synchronous temporal coupling while increasing schema, semantic,
operational and retention coupling. Maintain consumer ownership/usage evidence where
possible; a schema registry checks structural compatibility, not business meaning.
- New readers must read or transform historical events within the supported replay horizon;
old readers must tolerate new events for their supported deployment overlap. Seven-day
topic retention alone establishes neither every reader's support duration nor archive/DLQ
replay limits. Archives can use versioned
upcasters/migrations; “forever” is a costly policy, not a default.
- Adding a subscriber is a capacity and governance change when it adds broker reads,
fan-out or shared downstream load. Budget quotas, PII access and replay impact per consumer;
it does not automatically multiply load on the publisher.
- Anti-pattern — the event that is a command.
ShipOrder published to a topic with one
subscriber. Observable shapes: an imperative name; exactly one consumer that must exist; a
correlation id used to wait synchronously for a reply topic. Model it explicitly as an async
command with an outcome contract, or use request/response when the caller is actually blocked.
- Anti-pattern — the distributed monolith. Observable shapes: a release checklist naming
two services; a consumer that breaks when a producer adds a field; a shared library of event
classes that every service must upgrade in lockstep. Publishing over a broker did not
decouple anything; the schema is a compile-time dependency wearing a wire format.
- Anti-pattern — projection without authority or recovery. Event-carried state transfer with no named
authority for the current value: each consumer keeps its own projection, they diverge, and
no service can answer "what is true now". Name the owner of each entity and how a consumer
resyncs after a gap.
- Anti-pattern — publish inside the transaction. A
send() between the write and the
commit publishes facts that may never become true; a send() after the commit loses them on
a crash. For independent sends, these are dual-write windows: select an atomic publication
intent (such as outbox/CDC) or an explicitly supported transaction boundary, then test
relay/retry recovery and duplicates
(distributed-transactions-and-sagas).
- End-to-end redelivery is common but product/configuration boundaries differ: at-most-once,
at-least-once and transactional broker-local processing all exist. Handlers that may see a
duplicate must be repeat-safe:
the guarantee vocabulary is
delivery-semantics, the handler technique is idempotency.
Never write "exactly-once" about an event pipeline without naming the boundary.
- Choreography needs durable observability: event ID, causation ID, trace context and business
correlation identity have different roles. Propagate them with bounded cardinality and
retain a queryable event/workflow view where the business must answer current status.
- Orchestration's cost is a component that knows every step. That is acceptable; a coordinator
that also holds business rules for each participant is not — it has become the monolith the
events were meant to split.
- FaaS is a placement/runtime decision, not an architecture. Pricing, cold starts,
concurrency, batching, retry/partial-batch behavior, maximum duration, connection reuse and
ordering are provider/event-source specific. Execution environments may reuse pools, while
burst scaling can multiply them; managed pollers can preserve per-partition order. Compare
measured end-to-end latency, backlog recovery, connection quotas and control limits against
a long-lived consumer.
References
CloudEvents 1.0.2 specification
Transactional outbox: local atomic publication intent and duplicate relay delivery.
AWS Lambda with Kafka event sources
Choosing the style — events versus commands versus
request/response with the condition that selects each, choreography versus orchestration
compared on debuggability, coupling, failure handling and participant count, and the FaaS
versus long-lived-consumer decision with the Java cold-start considerations. Read when
deciding how two components should communicate, or when a saga is being designed.
Designing an event — naming, fat versus thin payloads and the
read-back stampede, the event schema as a contract with unknown consumers, which direction
of compatibility events actually need, and what belongs in the payload versus what must be
fetched. Read before publishing a new event type or changing an existing one.
1---2name: event-driven-architecture3description: Choosing facts, asynchronous commands or request/response across services; then designing choreography/orchestration, payload authority, evolution horizon and consumer runtime. Use when a broker masks synchronous outcome dependence, workflows are unreconstructable, consumers read back every event, or publish and database commit form a dual write. Delivery, idempotency, ordering, outbox mechanics and schema evolution remain in their owning skills.4---56# Event Driven Architecture78## Purpose910Decide whether two components should exchange a **fact** or a **call**. An event is an11immutable observation about something that happened in the publisher's domain12(`OrderPlaced`); a command is an instruction to a named recipient with an expected outcome13(`ShipOrder`); request/response can carry a command or a query and returns an outcome. An14asynchronous command can return outcome later through status, callback or event. This15is a coupling decision, not a technology one, and the honest answer is often16request/response — a broker between two parties that each need the other's outcome buys a17new failure domain, added latency and no answer.1819The failure this prevents is the distributed monolith: services that talk only over a broker20yet cannot be released independently, because one team's event is another team's function21call in disguise. The second is the flow that exists nowhere — every step is a handler,22the sequence is emergent, and answering "why did this order never ship" means reconstructing23it from logs.2425## Workflow26271. **Name the semantic contract, not just the tense.** Past tense is a useful event smell;28 imperative naming suggests a command. Verify ownership, recipient, whether rejection is29 possible, and whether the message remains meaningful with no consumer.302. **Ask when and where the outcome is needed.** An outcome needed in the current latency31 budget favors request/response. Deferred completion may use an addressed async command32 with status/callback; independent reactions to a fact favor events.333. **Choose coordination from flow semantics.** Independent reactions can choreograph. A34 branching business workflow needing explicit state, deadlines, compensation or one35 recovery owner favors orchestration—participant count alone is not a threshold.364. **Design the payload.** Decide what the event carries versus what the consumer fetches,37 and name the authority for the current value — `references/event-design.md`.385. **Fix the compatibility direction and the window.** Historical reader support follows the39 oldest data that can reappear from topics, archives or DLQs; old-reader/new-writer overlap40 follows deployment and consumer support policy. These are not one additive duration;41 use `schema-evolution-and-compatibility` for the format-specific contract.426. **Prove the commit boundary.** A local DB transaction does not include an ordinary broker43 send. Use an outbox/CDC, an explicitly enlisted XA resource, or a broker-local transaction44 whose exact boundary fits; “before versus after commit” alone leaves a failure window.457. **Choose the consumer's runtime last** — long-lived process or FaaS — from throughput,46 burst shape and whether a partition assignment must be held.4748Inspect broker/client, serializer and Java/framework versions plus retention, replay and retry49configuration before implementation advice. The envelope reference uses Java 16+ record syntax;50preserve the target rather than upgrading it. Deliver the interaction choice, outcome/recovery51owner, commit boundary, reader/writer horizon and one confirming failure/compatibility case.52If these facts are missing, state a conditional choice and the smallest contract/configuration53evidence needed to resolve it.5455## Decision block5657```text58Publish an event when:59- the producer completes its own work without the consumer's outcome60- the consumer set is open: a new reader must be addable without changing the producer61- consumer unavailability must not bound producer availability, and a backlog is acceptable62- fan-out or replay from retained history is a requirement, not a nice-to-have63Avoid events when:64- the caller must answer its own caller with the result (any synchronous read path)65- there is one known recipient, the message is semantically a command, and no buffering,66 replay or asynchronous completion requirement justifies the broker67- the producer needs to know the work was rejected, and the rejection is a business outcome68- the boundary has no independent lifecycle/scaling/resilience driver and the broker only69 obscures a synchronous dependency70Prefer request/response instead when:71- the interaction is a query. Publishing an event to ask a question is a request/response72 interaction requiring correlation, timeout and reply lifecycle; model it as such73- the outcome must be surfaced to a user inside the current request74- the consumer count is one and stable, and the added broker is pure operational surface75```7677## Rules7879- Events can reduce synchronous **temporal** coupling while increasing schema, semantic,80 operational and retention coupling. Maintain consumer ownership/usage evidence where81 possible; a schema registry checks structural compatibility, not business meaning.82- New readers must read or transform historical events within the supported replay horizon;83 old readers must tolerate new events for their supported deployment overlap. Seven-day84 topic retention alone establishes neither every reader's support duration nor archive/DLQ85 replay limits. Archives can use versioned86 upcasters/migrations; “forever” is a costly policy, not a default.87- Adding a subscriber is a capacity and governance change when it adds broker reads,88 fan-out or shared downstream load. Budget quotas, PII access and replay impact per consumer;89 it does not automatically multiply load on the publisher.90- **Anti-pattern — the event that is a command.** `ShipOrder` published to a topic with one91 subscriber. Observable shapes: an imperative name; exactly one consumer that must exist; a92 correlation id used to wait synchronously for a reply topic. Model it explicitly as an async93 command with an outcome contract, or use request/response when the caller is actually blocked.94- **Anti-pattern — the distributed monolith.** Observable shapes: a release checklist naming95 two services; a consumer that breaks when a producer adds a field; a shared library of event96 classes that every service must upgrade in lockstep. Publishing over a broker did not97 decouple anything; the schema is a compile-time dependency wearing a wire format.98- **Anti-pattern — projection without authority or recovery.** Event-carried state transfer with no named99 authority for the current value: each consumer keeps its own projection, they diverge, and100 no service can answer "what is true now". Name the owner of each entity and how a consumer101 resyncs after a gap.102- **Anti-pattern — publish inside the transaction.** A `send()` between the write and the103 commit publishes facts that may never become true; a `send()` after the commit loses them on104 a crash. For independent sends, these are dual-write windows: select an atomic publication105 intent (such as outbox/CDC) or an explicitly supported transaction boundary, then test106 relay/retry recovery and duplicates107 (`distributed-transactions-and-sagas`).108- End-to-end redelivery is common but product/configuration boundaries differ: at-most-once,109 at-least-once and transactional broker-local processing all exist. Handlers that may see a110 duplicate must be repeat-safe:111 the guarantee vocabulary is `delivery-semantics`, the handler technique is `idempotency`.112 Never write "exactly-once" about an event pipeline without naming the boundary.113- Choreography needs durable observability: event ID, causation ID, trace context and business114 correlation identity have different roles. Propagate them with bounded cardinality and115 retain a queryable event/workflow view where the business must answer current status.116- Orchestration's cost is a component that knows every step. That is acceptable; a coordinator117 that also holds business rules for each participant is not — it has become the monolith the118 events were meant to split.119- **FaaS is a placement/runtime decision, not an architecture.** Pricing, cold starts,120 concurrency, batching, retry/partial-batch behavior, maximum duration, connection reuse and121 ordering are provider/event-source specific. Execution environments may reuse pools, while122 burst scaling can multiply them; managed pollers can preserve per-partition order. Compare123 measured end-to-end latency, backlog recovery, connection quotas and control limits against124 a long-lived consumer.125126## References127128- [CloudEvents 1.0.2 specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md)129- [Transactional outbox](https://microservices.io/patterns/data/transactional-outbox.html): local atomic publication intent and duplicate relay delivery.130- [AWS Lambda with Kafka event sources](https://docs.aws.amazon.com/lambda/latest/dg/with-kafka-configure.html)131132- [Choosing the style](references/choosing-the-style.md) — events versus commands versus133 request/response with the condition that selects each, choreography versus orchestration134 compared on debuggability, coupling, failure handling and participant count, and the FaaS135 versus long-lived-consumer decision with the Java cold-start considerations. Read when136 deciding how two components should communicate, or when a saga is being designed.137- [Designing an event](references/event-design.md) — naming, fat versus thin payloads and the138 read-back stampede, the event schema as a contract with unknown consumers, which direction139 of compatibility events actually need, and what belongs in the payload versus what must be140 fetched. Read before publishing a new event type or changing an existing one.