Introduction: Event Driven Architecture Is Not Magic, It's Trade-offs
Event Driven Architecture (EDA) has been sold for years as the silver bullet for scalability, decoupling, and "future-proof" systems. The reality is harsher. EDA is not inherently simpler, safer, or cheaper than request/response architectures. It merely shifts complexity from synchronous coordination to asynchronous uncertainty. If you don't explicitly design for that shift, you don't get a resilient system - you get distributed chaos with better marketing. Many teams adopt events because "microservices need them," without understanding the patterns that make events useful rather than destructive.
The modern EDA conversation often skips the uncomfortable parts: event granularity decisions, semantic stability, consumer-driven evolution, and the operational cost of running event infrastructure at scale. Worse, teams frequently conflate events, messages, commands, and logs, treating them as interchangeable. They are not. This confusion leads directly to anti-patterns like the infamous Swarm of Gnats, where systems drown in tiny, meaningless events that increase coupling instead of reducing it.
This article is a guide to the EDA patterns that actually matter in production systems. We'll focus on derived event granularity - when to emit coarse versus fine-grained events, how to avoid consumer explosion, and how real-world systems fail when these patterns are ignored. The goal is not theoretical purity, it's systems that survive contact with production traffic, schema drift, and team turnover.
What an Event Really Is (And Why Most Systems Get This Wrong)
An event is a statement of fact about something that already happened, expressed in a way that other parts of the system can rely on. This definition aligns with the way Martin Fowler describes event-driven systems and with the vocabulary Gregor Hohpe uses in Enterprise Integration Patterns. Events are not instructions, they are not questions, and they are not "things we might want someone to react to". They are immutable records of state change, published after the fact, and owned by the producer's domain. Many systems fail because they treat events like remote procedure calls in disguise. They emit events such as ValidateUser, CalculateDiscount, or SendEmail. These are commands, not events. Commands imply intent and expectation; events imply history. Once you cross that line, you've already reintroduced tight coupling - just asynchronously. Consumers now depend on producer intent, not producer truth, which makes evolution painful and coordination fragile.
A correctly designed event communicates business meaning, not technical steps. OrderPlaced, PaymentCaptured, or ShipmentDispatched are stable facts that can be reasoned about years later, regardless of how the internal implementation changes. This distinction is not pedantic; it directly affects how granular your events should be, how many consumers you can support without breaking them, and whether your system remains intelligible to new engineers over time. A useful litmus test: if you can't explain an event to a non-engineer in plain business language, it's probably the wrong event, or it's actually a command wearing an event's name.
Event Granularity: The Most Expensive Decision You'll Make
Event granularity is the decision of how much information and semantic weight each event carries. Too coarse, and consumers lose flexibility. Too fine, and you create a dependency nightmare. This is where most EDA implementations quietly fail, because teams treat granularity as a technical detail to be settled during implementation, instead of a product decision that deserves the same scrutiny as an API contract.
Fine-grained events - like UserEmailUpdated, UserAddressUpdated, UserPhoneUpdated - seem attractive at first. They feel precise, composable, and reusable. In practice, they explode consumer complexity. Each downstream service must now reconstruct business meaning by correlating multiple events, often across time and across partitions where ordering guarantees are weak or absent. This introduces temporal coupling and makes reasoning about system state significantly harder. Debugging stops being observability and starts being archaeology, where you're reconstructing what "really" happened from a pile of disconnected fragments.
On the other end of the spectrum, overly coarse events - like a single UserProfileChanged event with a sprawling payload containing the entire user record - create a different set of problems. Consumers now depend on internal producer structure rather than a stable contract, payload schemas grow uncontrollably over time, and backward compatibility becomes fragile because every small internal change risks breaking multiple unrelated consumers. Neither extreme scales well, and the right answer is rarely found by defaulting to either one out of convenience.
Granularity is not fundamentally about payload size; it's about semantic completeness. An event should represent a business-relevant transition that is meaningful on its own, without requiring the consumer to hold a mental model of the producer's internals. If a consumer needs to listen to five different event types just to understand what happened, your granularity is too fine. If a consumer needs to parse half your domain model out of a single payload to extract value, it's too coarse.
Derived Event Granularity: Designing for Consumers Without Coupling
Derived event granularity is the practice of deriving additional, coarser events from internal state changes, rather than exposing every internal mutation directly to the outside world. This pattern allows producers to keep fine-grained, flexible internal models while emitting coarser, consumer-aligned events at the system boundary. It is widely used in mature event-driven systems, even when teams don't explicitly name the pattern they're applying.
Instead of publishing raw internal events like OrderItemAdded or OrderPriceRecalculated directly to external consumers, a service might process those internal changes and emit a single derived event such as OrderConfirmed once a meaningful milestone is reached. This derived event represents something downstream systems actually care about: a point in the domain lifecycle where action makes sense. Internals remain free to evolve and churn, while the external contract stays stable and legible. This idea resonates strongly with concepts described by Martin Kleppmann in Designing Data-Intensive Applications, particularly around materialized views derived from an internal log and the separation between a system of record and the views built on top of it.
Derived events are commonly produced using stream processing tools such as Kafka Streams, Apache Flink, or Amazon Kinesis Data Analytics, though a simple internal workflow or application-level aggregator can accomplish the same thing at smaller scale. The key insight is that not every state change deserves to be public. Public events should be curated, opinionated, and boringly stable - the kind of interface you'd be comfortable versioning for years. If you treat your event stream like an internal debug log that happens to be exposed externally, your consumers will inherit all the noise and instability of your internals, with none of the context you have to interpret it.
This pattern also enables multiple, purpose-built views of the same underlying reality. Different derived events can be produced from the same internal state transitions for different audiences - a billing system might need InvoiceFinalized while a fulfillment system needs OrderReadyForPacking, both derived from the same confirmation workflow - without either consumer leaking into the other's concerns or into the producer's internal complexity. That's not duplication for its own sake; it's intentional abstraction, and systems that scale over multiple years tend to apply it relentlessly rather than treating a single generic event as a one-size-fits-all solution.
The Swarm of Gnats Anti-Pattern: Death by a Thousand Events
The Swarm of Gnats anti-pattern, a term associated with Gregor Hohpe's writing on integration and messaging, describes systems that emit massive volumes of tiny, low-value events that overwhelm consumers and infrastructure. Each individual event does almost nothing on its own, but together they create noise, cost, and coupling that's disproportionate to the value delivered. It remains one of the most common failure modes in real-world EDA adoption, precisely because it's the path of least resistance when teams wire event emission directly into persistence hooks or ORM callbacks.
You see this pattern when teams emit an event for every CRUD operation: EntityCreated, EntityUpdated, FieldChanged, FlagToggled. These events are trivial to generate and feel "complete" because nothing is technically missing, but they push all the interpretation burden downstream onto consumers. Consumers now need deep domain knowledge just to react correctly, reconstructing business meaning from a stream of low-level mutations. Worse, small internal producer changes ripple through dozens of consumers who were never meant to know about those internals in the first place. Operationally, Swarm of Gnats systems are expensive in very concrete ways: high event volume increases broker costs, storage costs, and the operational burden of monitoring and alerting. Latency-sensitive consumers struggle to keep up during traffic spikes, and replaying streams for recovery or backfill becomes impractical when every change, however trivial, was captured as its own event. Teams typically respond by bolting on filters, projections, and compensating logic downstream - effectively rebuilding, at significant cost, the structure that should have existed at the producer boundary from the start. The blunt version of this lesson: if your event stream looks like a database changelog, you didn't design an event-driven system, you exported your ORM.
Practical Patterns That Actually Work in Production
The first pattern that consistently works is business milestone events. These are events emitted only when something meaningful has completed from a domain perspective, rather than whenever an underlying field changes. They are infrequent relative to internal state churn, semantically stable, and easy for new engineers to reason about without deep producer knowledge. Examples include SubscriptionActivated, LoanApproved, or GameRoundSettled. Because they represent completed transitions rather than intermediate steps, they drastically reduce consumer complexity and tend to survive schema evolution far better than low-level, field-level events ever do.
Another effective pattern is event versioning via additive evolution, not breaking changes. This aligns with guidance found in both the Confluent Schema Registry documentation and AWS EventBridge's schema evolution guidance: you add new fields, you don't remove or repurpose existing ones, and consumers opt into new semantics explicitly rather than having meaning silently shift underneath them. This sounds obvious in principle, yet many teams break consumers by "cleaning up" payloads during a refactor without recognizing that published events are contracts, not internal DTOs that can be freely reshaped.
Finally, successful systems embrace asynchronous boundaries with synchronous islands rather than forcing every interaction through an event bus. Not everything needs to be event-driven, and critical paths - payment authorization, inventory reservation, anything requiring an immediate, consistent answer - often remain synchronous for clarity and correctness, while events are reserved for propagation, integration, and side effects that can tolerate eventual consistency. Systems that force everything through events, including flows that need an immediate answer, usually end up both slower and harder to reason about than a deliberately hybrid design that uses each style where it fits.
Implementing Derived Events: A Practical Example
The following TypeScript example illustrates how internal state changes can be consolidated into a single derived, consumer-facing event rather than being published as a stream of low-level mutations. The internal Order class is free to track item additions and price recalculations however it likes; none of that churn becomes public API.
// Internal domain logic - these mutations never leave the service boundary
class Order {
readonly id: string;
private items: Item[] = [];
private status: 'DRAFT' | 'CONFIRMED' = 'DRAFT';
constructor(id: string) {
this.id = id;
}
addItem(item: Item) {
this.items.push(item);
// No event emitted here. This is internal churn, not a public fact.
}
confirm() {
if (this.items.length === 0) {
throw new Error('Cannot confirm an empty order');
}
this.status = 'CONFIRMED';
}
get totalPrice(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
get itemCount(): number {
return this.items.length;
}
}
// Derived event emission at the service boundary
function handleOrderConfirmation(order: Order, eventBus: EventBus) {
order.confirm();
eventBus.publish({
type: 'OrderConfirmed',
occurredAt: new Date().toISOString(),
payload: {
orderId: order.id,
itemCount: order.itemCount,
totalPrice: order.totalPrice,
},
});
}
This approach hides internal churn and exposes a single, semantically rich event at the point where it matters to the business. Consumers don't care how many times items were added, removed, or recalculated along the way - they care that the order is now confirmed and what that implies for them. That distinction between internal mutation and public fact is the entire difference between a signal and noise, and it's the difference that determines whether a downstream team can build reliably on top of your service.
The 80/20 of Event Driven Architecture
If you want 80% of the benefits of EDA with 20% of the pain, three practices account for most of that value. First, design events as products, not as side effects of whatever your persistence layer happens to do. Name them carefully, document their meaning and guarantees, and treat them as long-term contracts with the same rigor you'd apply to a public API, because that's effectively what they are.
Second, opt for fewer, richer events over many tiny ones. Volume is not a proxy for correctness or completeness, and a stream with ten well-designed event types will almost always outperform one with a hundred narrowly-scoped ones, both in consumer ergonomics and in long-term maintainability. Third, derive events at the boundary, not directly from persistence hooks, database triggers, or ORM lifecycle callbacks. The moment your event emission logic lives inside your data access layer, you've guaranteed that every future schema change becomes a potential breaking change for every consumer downstream.
Most teams fail at EDA not because the underlying technology is hard - Kafka, EventBridge, and similar tools are well-documented and battle-tested - but because they refuse to say no to bad events during design review. Discipline beats tooling every time, and the discipline required here is almost entirely about restraint: resisting the urge to publish everything just because the infrastructure makes it technically easy.
Analogies and Mental Models
Think of events like newspaper headlines, not raw surveillance footage. A headline summarizes what matters and gives a reader enough context to decide whether to read further; footage is exhaustive but largely useless without someone doing the work of interpreting it. Swarm of Gnats systems publish footage and expect every consumer to do their own editing. Good EDA publishes headlines, and leaves the footage - the fine-grained internal history - where it belongs, inside the producer.
Another useful mental model is urban planning. Internal state changes are like foot traffic inside buildings: necessary, constant, and largely private. Public events are the roads and intersections that connect those buildings to the rest of the city. If you expose every hallway and stairwell as a public road, the city becomes unusable - too much surface area, too many paths that were never meant to carry external traffic. Abstraction, in this sense, is not about hiding information out of secrecy; it's a basic precondition for the system remaining navigable as it grows.
Key Takeaways
If you only take five things away from this article, make them these:
- Separate commands from events explicitly. If an event name implies an instruction or intent, rename it or rethink it - it's a command in disguise.
- Treat granularity as a product decision, reviewed with the same care as a public API, not an implementation detail decided inside a single pull request.
- Derive public events from internal state, using a stream processor or an application-level aggregator, instead of publishing raw persistence-layer mutations.
- Evolve schemas additively. Add fields, don't repurpose or remove them, and let consumers opt into new semantics on their own schedule.
- Keep critical, consistency-sensitive paths synchronous. Reserve events for propagation and side effects that can tolerate eventual consistency, not for everything by default.
Conclusion: Event Driven Architecture Rewards Maturity, Not Enthusiasm
EDA is not a beginner-friendly architecture, and pretending otherwise does teams a disservice. It demands strong domain modeling, disciplined boundaries, and a willingness to say "no" to technically correct but strategically harmful events. Patterns like derived event granularity exist precisely because real systems hurt without them - not as an academic refinement, but as a response to production incidents, debugging nightmares, and consumer teams quietly giving up on the event stream altogether.
If you remember one thing from this article, remember this: events are a language your systems use to communicate across team and service boundaries. If you speak in noise - every field change, every internal mutation, every CRUD operation - your system will listen in confusion, and so will every engineer who has to maintain it. If you speak in meaning, curated at the boundary and stable over time, it might actually scale the way EDA promised it would.
Design fewer events. Make them boring. Let your consumers sleep at night.
References
- Fowler, Martin. "What do you mean by Event-Driven?" - martinfowler.com
- Hohpe, Gregor, and Bobby Woolf. Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Addison-Wesley, 2003.
- Hohpe, Gregor. "Starbucks Does Not Use Two-Phase Commit" and related writing on event granularity and messaging anti-patterns - enterpriseintegrationpatterns.com
- Kleppmann, Martin. Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems. O'Reilly Media, 2017.
- Confluent. "Schema Evolution and Compatibility" - Confluent Schema Registry documentation.
- Amazon Web Services. "Amazon EventBridge schema registry" - AWS documentation.
- Apache Kafka Documentation. "Kafka Streams" - kafka.apache.org
- Apache Flink Documentation. "Stream Processing" - flink.apache.org