Exploring event-driven architecture patterns
Event-driven architecture leverages various design patterns to effectively handle events and ensure robust, scalable, and maintainable systems. These patterns help manage event generation, transmission, processing, and storage complexities. We will present some commonly used patterns in event-driven architecture.
Publish-subscribe
The publish-subscribe pattern in event-driven architecture involves producers sending events to a broker, which distributes the events to multiple consumers. Messages are pushed to consumers, and consumers can be added or removed without affecting the producer. A durable subscription allows inactive consumers to receive missed events upon reconnection.
An example is a social media platform where users subscribe to updates from other users. When a user posts a new update, it is published to all subscribers.
Event notification
Event notification is a simple pattern where the producer emits an event to inform consumers that a change or action has occurred. These events carry minimal information, so consumers may need to fetch additional data if required. While this approach reduces event size and is easy to implement, it can increase latency and create tighter coupling by requiring follow-up requests.
The following JSON snippet illustrates an example of a user placing a bid on an item in the auction system:
{ "userId": "1", "productId": "2", "bidAmount": 150.00 } WITH: { "eventType": "BidAdded", "userId": "1", "productId": "2", "bidAmount": 150.00 }
Note that the event notification pattern only includes the user and product IDs, so the event contains minimal information for other services. Figure 8.6 illustrates service interactions using an event notification pattern.

Figure 8.6: Service interaction using the event notification pattern
If the consumer needs more user or product data, additional service requests are required, creating a coupling between services. This can cause delays, lower system performance, and create dependencies that make the system harder to manage and maintain. Possible solutions to minimize the tight coupling between services would be implementing a shared cache or using the pattern event-carried state transfer.
Event-carried state transfer
In the event-carried state transfer pattern, events contain all the information consumers need to process, eliminating the need to fetch additional data.
The following code snippet presents an example of the previous JSON with complete user and item data:
{ "user": { "id": "112233", "name": "Wanderson Xesquevixos" }, "product": { "id": "98765", "description": "Sport Car 1977" }, "bidAmount": 150.00 }
This reduces latency by providing all necessary data within the event and simplifies consumer logic. However, events can become large and may contain redundant information.
Message outbox pattern
The message outbox pattern ensures reliable event publishing in distributed systems while preserving data consistency. Before sending a message, the service saves it to a database outbox table within the same transaction that updates the business data. A background process then reads from this table and publishes the event to the broker. This approach prevents message loss if the service crashes after committing the transaction but before sending the event, ensuring that every persisted change is eventually reflected in the event stream. An example is an e-commerce application that saves an order in the database while simultaneously storing an OrderPlaced
event in the outbox table. This event is later published to notify other services, maintaining a consistent view between the internal data state and external event consumers.
Message inbox pattern
The message inbox pattern consumes and processes messages reliably in distributed systems while preserving data consistency. When a service receives an event, it first stores the message in a dedicated inbox table before executing any business logic. This approach helps prevent duplicate processing and enables exactly-once or idempotent execution, even in crashes or retries. By persisting the message before processing, the system ensures that operations are only applied once, maintaining consistency between the event stream and the service’s internal state. For example, a billing service receiving a PaymentConfirmed
event writes the message to its inbox table and checks whether it has already been processed. Only then does it apply the payment logic, avoiding double charging and ensuring a consistent transactional outcome.
Saga pattern
The Saga pattern manages long-running transactions and ensures data consistency across multiple services by breaking a transaction into smaller steps, each of which can be compensated if it fails. It represents a single business process, as illustrated in Figure 8.7.

Figure 8.7: The Saga pattern process
In a travel booking system, a saga coordinates the booking of flights, hotels, and vehicle rentals. If any step fails, the saga triggers compensating actions to cancel the already completed bookings.
The Saga pattern ensures data consistency and handles long-running transactions. However, it is complex to implement and manage compensating transactions.
We’ve learned many new concepts; now, let’s move on to the next section and apply them by implementing event-driven services in our online auction application.