Agent-era architecture · Practical technical guide · 1 November 2026
What changes when an agent, rather than a service, consumes domain events: why thin notification events turn into bursts of callback reads and race conditions, what event-carried state transfer fixes and costs, and how to version the snapshot an agent acts on.
Two kinds of event
Martin Fowler separates several things people mean by event-driven. Two matter here. In event notification, an event says that something happened and carries little more than an identifier; a consumer that needs details calls back to the source. In event-carried state transfer, the event carries the data consumers need, so they can act, or keep their own copy, without calling back. Notification keeps events small and the source authoritative. Carried state decouples consumers from the source's availability but copies data and makes the event schema a contract.
Most existing event streams at Belgavi's Book Store are notifications: return.updated with a case number. They were designed for services that call back once, cache aggressively and tolerate a little lag. An agent consumer behaves differently, and that changes which kind of event is the better choice.
What an agent does with a thin event
When return.updated for CASE-4219 arrives, the returns agent needs the return, the order, the customer's recent messages and the refund history before it can decide anything. A service would make one call. The agent makes four to eight tool calls, spread over the seconds the model takes to think between them. Three things follow.
- Callback amplification. One event becomes several reads against the source. If a batch job updates 2,000 returns, the source sees a read storm that its normal traffic never produced.
- Reads from different moments. The agent reads the return at second 1 and the refund history at second 6. If another update lands in between, its picture mixes two states of the case, and it can approve a refund that was already issued.
- Events overtaken by later state. By the time the agent runs, the case may have moved twice more. The agent acts on whatever it reads now, not on what the event described, so the event no longer explains the decision.
A worked example
Take a day with 6,000 return updates, one agent run per event, six tool reads per run, and a source that handles 40 reads a second comfortably. Spread evenly, 36,000 reads over a working day is nothing. The problem is bursts: a carrier file that updates 1,500 returns in one minute produces 9,000 reads in the minutes it takes the agents to work through it.
| Design | Reads on the source for the burst | Decision based on |
|---|---|---|
| Notification events, agent reads everything | 9,000 | A mix of states read over several seconds |
| Notification, agent reads one versioned snapshot | 1,500 | One consistent snapshot, possibly newer than the event |
| Event-carried state with a version | 0 (plus reads for anything not carried) | Exactly the state the event describes |
| Carried state, agent re-checks the version before writing | 1,500 cheap version checks | The event state, rejected if superseded |
These are arithmetic on the stated assumptions, not measurements. The pattern is general: notification events let read load scale with agent behaviour, which you control less well than service code, while carried state lets it scale with the event rate.
Designing the carried snapshot
If you move to event-carried state, carry what the agent decides on, plus the version it describes. A CloudEvents envelope already has the slots: id for deduplication, source and type for routing, subject for the case and time for when it happened. Put the snapshot and its version in data.
Illustrative CloudEvents JSON for a carried snapshot; field names in data are this guide's choices.
{
"specversion": "1.0",
"id": "6f1c2a90-return-4219-v17",
"source": "/returns",
"type": "store.return.updated.v2",
"subject": "CASE-4219",
"time": "2026-11-01T09:14:03Z",
"data": {
"case_id": "CASE-4219",
"version": 17,
"status": "inspected",
"paid_pence": 2499,
"refunded_pence": 0,
"refund_method": "card",
"last_customer_message_id": "msg-88120"
}
}Leave out what the agent does not need, especially personal data: event streams are copied into logs, replay stores and analytics, and every field you carry is retained in all of them. Carry references, such as the latest message identifier, for content the agent can fetch when it decides to.
Acting on a snapshot safely
A snapshot with a version turns "act on what I read" into "act on what this event says, if it is still true". The write path checks the version, as an optimistic concurrency condition, and the agent re-plans if the case has moved on.
Illustrative python, checked for syntax; adapt names and storage to your system.
def handle(event, decide, issue_refund, current_version):
snap = event['data']
if snap['version'] < current_version(snap['case_id']):
return 'skipped: superseded by a newer version' # a later event will arrive
action = decide(snap) # model call on the snapshot only
if action is None:
return 'no action'
ok = issue_refund(snap['case_id'], action['amount_pence'],
expected_version=snap['version'],
idempotency_key=event['id'])
return 'refunded' if ok else 'conflict: re-plan on the newer version'Skipping superseded events is safe only if every version produces an event and the stream preserves per-key order, so the newest event for a case will also be handled. If the stream can drop or coalesce events, read the latest snapshot instead of skipping.
What stays deterministic
- Building the snapshot, at the source, from one transaction, so it is internally consistent.
- Deduplication by event id and ordering by case, in the consumer framework, before any model call.
- The supersession check and the version condition on writes, in code.
- Deciding what to do with the case is the only step that needs the agent.
Migration steps for an existing notification stream
- Measure first: reads per event for agent consumers, and how often the source version changed during a run. Both numbers usually surprise people.
- Add a version to the source entity if it lacks one, and include it in the existing notification event. This alone lets agents detect mid-run changes.
- Publish a second event type that carries the snapshot, built in the same transaction as the change, using an outbox so the event and the write cannot diverge. Keep the notification type for existing consumers.
- Move agent consumers to the carried type and change tools to accept an expected version on writes.
- Review the carried fields with whoever owns data retention, and set retention on the topic accordingly.
Failure modes
- Snapshots that are not snapshots. Assembling the payload from several reads after commit reintroduces the mixed-state problem at the producer.
- Schema drift. Carried state is a contract. Version the event type and run compatibility checks on the producer, or a renamed field reaches the agent as a missing one.
- Growing payloads. Teams add fields "for the agent" until events are megabytes. Carry references for large or rarely used data.
- Silent skips. Count superseded and conflicted events by reason; a sudden rise means a producer or ordering problem, not a quiet day.
When this is unnecessary
If the agent needs one small read per event and the source is cheap to read, notification events are fine. If events are rare, such as a few per hour, callback load is irrelevant and a re-read before acting is simpler than a new event type. And if the decision must reflect the very latest state, for example a fraud hold, a fresh authoritative read at decision time beats any carried copy; carry state for context, then confirm.
Sources and scope
Checked against these sources on 1 November 2026. Event volumes, read counts and the source capacity are assumptions for the arithmetic, not measurements.
Thin notification events make an agent call back several times per event, multiplying reads and mixing states from different moments. Carry a versioned snapshot built in the producing transaction, skip superseded versions only with per-key ordering, and write with an expected version.