Event-Driven Architecture (EDA) is a design style in which the components of a system communicate by producing and consuming events, instead of invoking one another directly. This approach favors decoupling, scalability, and the ability to react in real time to what happens in the business. In this lesson we will lay the conceptual foundations you will need to understand the rest of the module: what an event is, how it differs from a command and a message, what role producers and consumers play, and what topologies exist to connect them.
Understanding these fundamentals well is essential because nearly all modern large-scale systems (banking, e-commerce, streaming, IoT) rely on events to integrate microservices without them knowing about each other.
Contents
- What is event-driven architecture?
- Event vs command vs message
- Producers and consumers
- Mediator topology
- Broker topology
- General event flow diagram
- Common mistakes and tips
- Exercises and solutions
- Conclusion
- What is event-driven architecture?
An event represents something that has already happened in the system and is relevant to the business. Examples: "OrderCreated", "PaymentConfirmed", "UserRegistered". What matters is the verb tense: the event is an accomplished, immutable fact.
In an event-driven architecture:
- Components emit events when their state changes.
- Other components react to those events without the emitter knowing who is listening.
- Communication is typically asynchronous: the emitter does not wait for a response.
This contrasts with the traditional architecture based on synchronous calls (REST/RPC), where the client knows about and depends on the server.
| Characteristic | Synchronous call (REST/RPC) | Event-driven architecture |
|---|---|---|
| Coupling | Strong (client knows the server) | Loose (emitter does not know the receiver) |
| Timing | Blocking (waits for a response) | Non-blocking (fire-and-forget) |
| Scalability | Limited by the slowest component | High (independent consumers) |
| Fault tolerance | If the server goes down, the call fails | The event can be processed later |
- Event vs command vs message
These three terms are often confused. They are types of messages, but with different intentions.
| Concept | Intention | Verb tense | Direction | Awaits a response? |
|---|---|---|---|---|
| Command | Order that something happen | Imperative: "CreateOrder" | 1 emitter → 1 specific receiver | Sometimes |
| Event | Notify that something happened | Past: "OrderCreated" | 1 emitter → N unknown receivers | No |
| Message | Generic term encompassing both | — | — | Depends |
Let's look at a code example to nail down the difference between a command and an event:
// COMMAND: an instruction directed at a specific recipient.
// SOMEONE is expected to execute it. Name in the imperative.
public record CreateOrderCommand(
String customerId,
List<OrderLine> lines) {
}
// EVENT: the fact that the order has ALREADY been created.
// It is immutable, carries an identifier and a timestamp.
// Name in the past tense.
public record OrderCreatedEvent(
String orderId,
String customerId,
BigDecimal total,
Instant occurredAt) {
}Detailed explanation of the snippet:
recordis an immutable, concise Java class (since Java 16), ideal for representing data that does not change, such as events.CreateOrderCommanduses a verb in the imperative because it expresses an intention that something happen. It is normally processed by a single responsible component.OrderCreatedEventuses the participle "Created" because it describes a past fact. It includesoccurredAt(anInstant, a UTC timestamp) because every event must be orderable in time.- The event contains the already-calculated
total: it transports the result, not the order to calculate it.
Practical rule: if you can reject the request, it is probably a command. If you are only reporting that something happened and no one can "refuse", it is an event.
- Producers and consumers
In EDA we distinguish two fundamental roles:
- Producer (Producer / Publisher): the component that detects a change and emits the event. It does not know (nor care) who will consume it.
- Consumer (Consumer / Subscriber): the component that receives the event and reacts (updates a database, sends an email, calls another service...).
// PRODUCER: publishes the event to a channel. It does not know the consumers.
@Service
public class OrderService {
private final EventPublisher publisher; // abstraction of the channel
public OrderService(EventPublisher publisher) {
this.publisher = publisher;
}
public void createOrder(CreateOrderCommand cmd) {
// 1. Business logic: persist the order
String orderId = UUID.randomUUID().toString();
// ... save to the database ...
// 2. Publish the event describing what happened
var event = new OrderCreatedEvent(
orderId, cmd.customerId(), calculateTotal(cmd), Instant.now());
publisher.publish("orders.created", event);
}
}Key points:
EventPublisheris an abstraction: it hides whether we use Kafka, RabbitMQ, etc. underneath. This allows changing the technology without touching the business logic.- After saving the order, the service publishes to the logical channel
"orders.created". There is no reference to who is listening.
// CONSUMER: subscribes to the channel and reacts to the event.
@Component
public class CustomerNotifier {
@EventListener("orders.created") // subscribes to the same channel
public void onOrderCreated(OrderCreatedEvent event) {
// Reaction: send a confirmation to the customer
sendConfirmationEmail(event.customerId(), event.orderId());
}
}- The
CustomerNotifierconsumer reacts by sending an email. Another consumer (for example,BillingService) could listen to the same event to issue an invoice. Adding new consumers does not require modifying the producer: that is where the power of EDA lies.
- Mediator topology
There are two major topologies for organizing the event flow. The first is the mediator topology.
Here, a central component (the mediator or orchestrator) receives an initial event, breaks it down into steps, and coordinates their execution by directing the workflow.
- Useful when the process has several steps with ordering and conditional logic (for example, opening an insurance policy: validate data → rate → issue policy → charge).
- The mediator knows the complete flow, which centralizes the coordination logic.
- Drawback: the mediator can become a bottleneck or a single point of failure, and it couples the knowledge of the process into a single place.
flowchart LR
A[Customer] -->|Initial event| M{Mediator / Orchestrator}
M -->|step 1| S1[Validation Service]
M -->|step 2| S2[Rating Service]
M -->|step 3| S3[Issuance Service]
S1 -.response.-> M
S2 -.response.-> M
S3 -.response.-> MIn the diagram, the mediator (M) is the one that decides the order and collects the responses from each service. The services do not know each other; they only talk to the mediator.
- Broker topology
The second is the broker topology (no mediator). There is no central coordinator: events flow freely through a broker (channel/queue) and each service reacts autonomously, and can in turn emit new events that trigger others.
- Ideal for simple and highly decoupled flows.
- Maximum scalability and resilience: there is no central point.
- Drawback: the overall flow ends up "spread out" and is harder to visualize and debug (there is no single place to read the whole process).
flowchart LR
P[Orders Service] -->|OrderCreated| B((Broker))
B --> I[Inventory Service]
B --> F[Billing Service]
I -->|StockReserved| B
F -->|InvoiceIssued| B
B --> E[Shipping Service]Here Inventory Service reacts to OrderCreated and, in turn, emits StockReserved, which triggers Shipping Service. The flow emerges from the sum of individual reactions, with no coordinator.
| Criterion | Mediator | Broker |
|---|---|---|
| Flow control | Centralized | Distributed |
| Coupling | Medium (everyone knows the mediator) | Minimal |
| Process visibility | High (a single place) | Low (spread out) |
| Resilience | Single point of failure | Very high |
| Ideal cases | Complex, conditional flows | Simple, decoupled flows |
- General event flow diagram
Combining the concepts, here is what the basic producer → channel → consumers flow looks like:
sequenceDiagram
participant P as Producer
participant C as Event channel
participant C1 as Consumer 1 (Email)
participant C2 as Consumer 2 (Billing)
P->>C: publish(OrderCreatedEvent)
C-->>C1: deliver event
C-->>C2: deliver event
Note over C1,C2: They process in parallel and<br/>independentlyThe producer publishes only once; the channel distributes the event to all interested subscribers, which work in parallel and without knowing about each other.
Common Mistakes and Tips
- Confusing commands with events. If you name an event in the imperative ("SendEmail") you are disguising a command. Always use the past tense for events: "EmailRequested" or "OrderCreated".
- Putting the consumer's business logic in the producer. The producer must not know what the consumers do. If it does, you couple things again.
- Events that are too "fat" or too "thin". An event must carry enough data for the consumer to react without having to call back to the producer (which reintroduces coupling), but without becoming a dump of the entire database.
- Forgetting the timestamp and the identifier. Every event must be traceable and idempotently identifiable.
- Tip: start with a broker topology for simple flows and reserve the mediator for processes with many conditional steps.
Exercises
- Classify the following messages as a command or an event and justify your answer: (a) "ReserveRoom", (b) "PaymentRejected", (c) "UpdatePrice", (d) "SessionStarted".
- Sketch (in pseudocode or words) what a "customer onboarding" process would look like with a mediator topology versus a broker topology. Which one would you choose and why?
- Design a Java event
recordfor "InvoiceIssued" that includes the minimum fields needed for an accounting consumer to react without querying back to the emitter.
Solutions
- (a) Command (imperative, directed, can be rejected). (b) Event (past fact, notification). (c) Command (order to change something). (d) Event (the session has already started).
- With a mediator: a
CustomerOnboardingOrchestratorreceives the request and calls, in order, validation → creation → welcome, handling failures. With a broker:RegistrationServiceemitsCustomerRegistered;EmailServiceandCRMServicereact independently. For a simple onboarding, the broker is preferable because of its low coupling; if there were many conditional steps (identity verification, scoring), the mediator makes control easier. - One possible solution:
public record InvoiceIssuedEvent(
String invoiceId,
String orderId,
String customerId,
BigDecimal totalAmount,
BigDecimal taxes,
String currency,
Instant issuedAt) {
}It includes amount, taxes, and currency so that accounting can post the entry without querying again.
Conclusion
We have seen that event-driven architecture replaces direct calls with the emission and consumption of business facts, achieving strong decoupling. We distinguished commands (orders), events (past facts), and messages (the generic term), and we understood the producer and consumer roles. Finally, we compared the two key topologies: mediator (centralized control) and broker (maximum autonomy).
In the next lesson, "Asynchronous Messaging: Queues and Brokers", we will dig deeper into the infrastructure that transports these events: queues versus topics, delivery guarantees, and specific technologies such as RabbitMQ, Kafka, and SQS.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
