Imagine you're at a restaurant. In a synchronous world, you'd place your order and the waiter would stand frozen at your table, unable to serve anyone else, until the kitchen finished cooking your meal. Every customer would wait in a single line. One slow dish would block the entire restaurant.
Real restaurants don't work this way. You place your order (a command), the kitchen receives it, and you're free to chat with friends while your food is being prepared. When it's ready, a runner delivers it (an event has occurred). Multiple orders process in parallel. A slow steak doesn't stop someone else from getting their salad.
Event-driven architecture applies this same principle to software. Instead of services waiting on each other in locked conversations, they communicate through events—facts about things that happened. This chapter explores the core concepts that make this work.
This is the single most important concept in event-driven architecture. Get this right, and everything else follows.
An event is an immutable fact about something that happened in the past. It's already occurred—you can't change it, reject it, or fail to process it in a way that un-does it.
Event characteristics:
Example events:
A command is a request to perform an action. It might succeed or fail. It's directed at a specific handler that decides what to do.
Command characteristics:
Example commands:
When you send a command, you're coupled to the receiver. You must know who handles CreateTask, wait for their response, and handle their errors.
When you publish an event, you're decoupled. You announce "TaskCreated happened" and walk away. Zero, one, or fifty services might react. You don't know. You don't care.
This decoupling is what enables systems to scale independently, fail gracefully, and evolve without coordination.
In synchronous systems, when you save data, it's immediately visible everywhere. This is strong consistency.
In event-driven systems, changes propagate through events. There's a delay between when something happens and when all services have processed it. This is eventual consistency.
Consider transferring $100 from your checking account to your savings account:
Strong consistency approach:
This requires a distributed transaction across accounts. If either account is unavailable, the entire transfer fails.
Eventual consistency approach:
For a brief moment, the $100 exists in neither account (already debited, not yet credited). But:
Humans already accept delays. Consider:
Most business processes don't require instant consistency. They require correct eventual state.
Some scenarios genuinely need immediate consistency:
For these, you either:
The key insight: Most systems don't need strong consistency, but developers default to it because it's simpler to reason about. Event-driven architecture makes you explicitly choose.
Event sourcing is an advanced pattern where you store events as the primary source of truth, not just current state.
Traditional approach (state-based):
You have complete history. You can:
When to consider event sourcing:
When to avoid:
We won't implement event sourcing in this chapter, but understanding it helps you see why events (not just state changes) are valuable.
CQRS (Command Query Responsibility Segregation) separates the models for reading and writing data.
Traditional approach:
Same model for everything.
CQRS approach:
Write side focuses on business logic and event generation. Read side has specialized views optimized for each query pattern.
Why separate?
This chapter doesn't implement CQRS, but knowing the concept helps you understand why Kafka (which excels at event distribution) pairs well with read-optimized databases for complex systems.
Not everything should be event-driven. Here's a decision framework:
Example: Task creation When a task is created, you need to:
With sync APIs, you'd call 4 services sequentially. Slow analytics blocks notification. EDA: publish TaskCreated, all services consume independently.
Example: Login validation When a user logs in, you need to:
This is inherently synchronous. The user can't proceed until validated. Event-driven login would be awkward: "We'll email you when you're logged in."
Most real systems use both patterns:
The initial request is synchronous (user needs confirmation). Downstream processing is event-driven (services work independently).
As you design event-driven systems, avoid these mistakes:
If you're expecting a specific receiver to do something specific, that's a command—even if you publish it to a queue.
If you're blocking until the event is processed, you've lost the decoupling benefits.
Events should represent what happened in the producer's domain, not what consumers need.
Events are for facts and commands, not request-response queries.
You built a kafka-events skill in Chapter 1. Test and improve it based on what you learned.
Ask yourself:
If you found gaps:
You now understand the core concepts. Use AI to explore their application to your own systems.
Open your AI assistant with context about event-driven architecture. These prompts help you apply concepts to real scenarios.
What you're learning: The distinction between events and commands is subtle but crucial. This exercise forces you to think about who initiates the action, whether it can fail, and how many consumers need to react. Notice how some items could be modeled either way depending on your system's needs.
What you're learning: Real systems mix consistency models. You'll discover that seat reservation might need synchronous locking (to prevent overselling), while email and analytics can be async. This is the judgment call you'll make repeatedly in distributed systems.
What you're learning: Event design is about finding the right level of abstraction. Include too little and consumers can't do their job. Include too much and you're coupling to consumer needs. This exercise helps you find the balance by thinking about what genuinely represents "task was created" versus "what notification service needs."
When designing event schemas, remember that events become contracts. Once consumers depend on a field, removing or changing it breaks them. Start with minimal events and add fields carefully. Schema evolution (covered in Chapter 11) provides patterns for safe changes, but prevention is better than migration.