The printer analogy — why this isn't event-driven architecture.
Calling something event-driven doesn't make it event-driven. When the architecture forces a system to send one million codes as one hundred batches of ten thousand, and receive one hundred responses back — that's not eventing. That's a for loop with a distributed systems wrapper. Here's why, and what it should look like instead.
Nobody sets out to build a bad architecture. The decisions that lead to it are usually made under time pressure, by people who know the platform well but haven't stress-tested the approach at the volume it will actually run at.
The scenario I want to describe is one I've seen in production. A system needs to process one million codes. The architecture uses events as its primary message distribution mechanism. The receiving system has a limit of ten thousand records per request. The pods running the workload are single-threaded. Parallelism is achieved by scaling the number of requests, not by designing the architecture to be inherently parallel.
The result is an architecture that looks event-driven on a diagram — and technically is firing events — but forces the calling system to manage one hundred outbound messages and read back one hundred responses. The caller owns the complexity of a problem that belongs inside the receiving system's boundary.
The printer analogy explains exactly why this is the wrong way to think about it.
The analogy
The printer you'd never buy.
Imagine you need to print a one million page document. You wouldn't expect to sit at your desk and manually click print one hundred times — requesting ten thousand pages at a time — and then wait for each batch to confirm before sending the next.
You'd select the document, click print, and walk away. The printer handles the complexity internally. It manages the feed rate, the buffer, the order. You fire and forget. The document arrives at the output tray when it's ready.
The reality
What this architecture is actually doing.
The limit of ten thousand records per request is a constraint of the receiving system. That constraint is real and it has to be respected. The question is: where should the complexity of managing that constraint live?
In this architecture, it lives in the calling system. The caller is responsible for knowing the limit, slicing the data accordingly, managing the sequence, sending each batch, and reading back each response. The calling system is doing the work that the infrastructure should be doing.
Here's what one million codes costs in network operations under this architecture:
That is 400 message operations for one logical request — before a single line of logging, before network latency on each hop, before the infrastructure cost of processing each message through the event pipeline. Every one of those 400 operations generates a log entry. Every log entry is storage, query overhead, and noise in your observability tooling.
The hidden cost
400 messages. 400 log entries. 400 network hops.
The message count is the headline number but the operational overhead compounds well beyond it. Every message operation in an event pipeline generates a log entry. Every log entry is storage, indexing overhead, and noise in your observability tooling. At 400 operations per logical request, a moderately busy system generates logging volume that obscures the signals that actually matter.
- 100 outbound events dispatched by the calling system — 100 network calls, 100 log entries on the sending side, 100 entries in the event pipeline's own audit log.
- 100 inbound messages received and processed by the receiving system — 100 network acknowledgements, 100 processing log entries, 100 entries tracking batch state.
- 100 response messages sent back by the receiving system — another 100 network calls, 100 log entries on dispatch, 100 pipeline entries.
- 100 response reads consumed by the calling system — 100 acknowledgements, 100 log entries, 100 state updates as the caller tracks which responses have arrived.
- Every one of those 400 operations carries network latency, serialisation overhead, and infrastructure cost. Multiply by concurrent requests and the noise floor rises fast — legitimate failures become hard to find.
The compounding problem
Why using events here makes it worse, not better.
Events are the right tool for broadcasting state changes to interested subscribers. Something happened — a record was created, a payment was received, a status changed. Multiple consumers can react independently. The producer doesn't know or care who is listening.
That is not what is happening here. The calling system is not broadcasting a state change. It is making a request and waiting for a response. That is a request/response pattern — and trying to implement it using events introduces all the complexity of asynchronous messaging with none of the benefits, because the calling system is blocking on the result anyway.
The fix
One message. One job. One response.
The fix is conceptually simple. The calling system should not be responsible for managing the receiving system's internal limits. It should send one message describing what it needs, receive a job identifier back, and wait for the result to be delivered asynchronously when it is ready.
The receiving system handles the chunking internally. It knows its own limits. It manages the parallelism. It assembles the result and delivers it back — in chunks if necessary, but as a coherent response to a single logical request, not as one hundred separate messages the caller has to reassemble.
The distinction
What actually makes an architecture event-driven.
- An event says "something happened." It does not say "please do this and tell me when you're done." The moment a producer needs a response from a consumer, it is no longer using events correctly — it is implementing request/response over an event bus.
- True event-driven architecture is fire and forget from the producer's perspective. The producer does not track whether consumers received the event, does not wait for a reply, and does not care how many consumers acted on it.
- If your "event-driven" architecture has a calling system waiting for responses, managing batches, and reassembling results — it is not event-driven. It is synchronous request/response with extra steps.
- When you need to process a large volume of data and collect a result, the correct pattern is an async job: submit the work, receive a job ID, poll or subscribe for completion, consume the result when it arrives.
- The receiving system owns the complexity of chunking, parallelism, and internal limits. The caller's concern is the result — not how the result was produced.
- This is what Service Bus, Azure Durable Functions, and similar tools are designed for. A durable orchestration can fan out to a thousand parallel workers and fan back in to a single result — entirely internally, invisible to the caller.
- A ten thousand record limit per request is an implementation detail. It belongs inside the receiving system's boundary. When the limit changes, the receiving system changes its internal chunking logic. The caller does not change at all.
- The moment the caller knows about the receiving system's internal limits and manages against them, you have tight coupling. It doesn't matter that it's communicating over a message bus — the coupling is in the knowledge, not the transport.
- Design the contract between systems around logical operations, not implementation constraints. "Process these codes and return the result" is a logical operation. "Here are ten thousand of them, I'll send the rest shortly" is an implementation concern that has leaked across a boundary.
Applicability
Choosing the right pattern.
The right pattern depends on what the operation actually is — not what it's called in the design document.
The name on the diagram doesn't change what it does.
Calling an architecture event-driven does not make it event-driven. The test is whether the producer cares about the consumer's response. If it does — if it is waiting, polling, batching, or reassembling — it is not using events. It is using events as a transport layer for something that is fundamentally a request/response operation.
The printer doesn't ask you to send the document ten thousand pages at a time. It accepts the whole job, manages its own constraints internally, and delivers the output when it's ready. That is the correct mental model for any system that receives a large volume of work and returns a result.
One message. One job ID. One result. The receiving system handles everything in between. That is the architecture. Everything else is a for loop in a distributed systems costume.
"Don't write directly to Dataverse — and don't let Power Platform call external systems directly either."
The inbound and outbound integration patterns that protect your Power Platform environment from the same class of problem.
Part 1 — Inbound pattern →Part 2 — Outbound pattern →