Event-Driven Architecture
Picture a common scenario with two microservices: one handling Orders and another managing Users. The Orders microservice needs to verify if a user is eligible to place an order based on their age.
Since the data is decentralized, the user’s information resides in the Users microservice.
The most straightforward and obvious solution that comes to mind is simply querying the missing information.
While simple, it adds latency because of the network overhead; the communication is synchronous, blocking the client until a response is received; services become dependent on each other’s functionality, resulting in tight coupling (if you move some behavior to another microservice, you need to rewrite all dependent clients), and so on and so forth.
An alternative approach would be to use events and solve the original problem this way:
- OrdersAPI creates its own projection of
[User]table (but only with the columns it needs) - then OrdersAPI subscribes for changes in UsersAPI
- when the user’s age changes, OrdersAPI updates the corresponding records in its own
[User]table
Yes, it means data duplication; however, this time, OrdersAPI is self-sufficient. It has all the data needed to execute an action without making additional requests, without relying on the client microservice to be up and running, and without introducing coupling between services.
The approach where the system is designed to communicate by emitting and reacting to events, instead of calling each other directly, is called Event-Driven Architecture.
Nothing stops you from applying Event-Driven Architecture with CRUD applications, but it is usually a bad fit if strong consistency is a must.
On the other hand, it works great in distributed systems that require high scalability, loose coupling, and asynchronous workflows.
Let’s explore the core components that make up an event-driven architecture, examine the key characteristics of each one, and discuss the patterns that can be applied in practice. This will help build a solid understanding of how EDA works and how to design reliable, scalable systems with it 😜.
Overview
At its core, event-driven architecture models systems around the occurrence of events rather than direct method calls or requests.
Something happens → An event is published → Listeners react to events.
In this architecture, you can highlight the following components:
- event— a fact that something happened (
UserRegisteredEvent, etc) - event source (producer/publisher/emitter) — someone who produces an event
- event listener (consumer/subscriber/sink)— someone who reacts to the event
- event broker (message channel)— a tool through which the source and the listener exchange events
While it may look simple, let’s take a closer look at each of these concepts, as they are quite different from what we are used to in REST.
Events
In Event-Driven Architecture (EDA), events are facts that describe something that has already happened in the system.
Despite the name, an event is just a plain DTO class used as a data container.
class UserCreatedEvent : IEvent
{
// metadata
public Guid EventId { get; }
// payload
public int UserId { get; }
public string UserName { get; }
public string UserSurname { get; }
}You can also inherit the marker interface to categorize messages or to apply cross-cutting behavior consistently.
Event Immutability
One of the most important characteristics is that events are immutable types. It derives from their nature, as an event represents something that happened in the past.
But there is also a practical reason for it. It is critical that no one can modify the contents of an event after it has been published; otherwise, it becomes impossible to know what was sent originally.
Event Types
In Event-Driven Architecture, messages can be classified by different types:
- Message — a generic term for any piece of information sent between components
- Event / Notification— something happened (
UserCreatedEvent) - Command — do something (
CreateUserCommand) - Document — here is the data (
UserInfoDocument)
An event represents something that happened (UserCreatedEvent). They usually broadcast to multiple consumers. The publisher does not care who consumes it, what actions they take, and does not wait for a response from them.
A command instructs the receiver to take a specific action (CreateUserCommand). It is sent to a single receiver — similarly to a REST request, but without exposing the receiver’s API or coupling the sender to its implementation. The publisher may or may not expect a response through the same channel.
Messages are usually poorly suited for the request-response model, since you need to serialize the message, store it, retrieve and deserialize it back. However, it allows you to shift a heavy task to asynchronous background processing.
A document message conveys the entire snapshot of the state.
It allows the publisher to ignore how the data is used, what information is needed, or how many consumers will process it. Consumers can process, store, or forward the data without affecting the publisher or needing to coordinate with it.
As you might guess, in the Event-Driven Architecture, the most popular and used type is events 😅 . However, commands and document messages also play important roles.
⚠️ By the way ⚠️
Some projects may adopt different naming conventions or even assign their own terms to existing concepts — for example, some teams call an
EventaMessage. Therefore it is important to make sure, you understand their language, to avoid confusion and keep communication clear.
Event Semantic
Events can be categorized by their usage:
- domain events — are used to communicate changes and trigger reactions inside the application within the current bounded context
- integration events — are used to communicate changes and trigger reactions outside the application across different bounded contexts
Domain events are usually easy to work with since:
- they operate in-process and are primarily used to reduce coupling between components
- they can be published at any moment, as they are part of local transaction boundaries that respect ACID principles
- they typically contain smaller payloads, since event listeners are located within the same service, and their data requirements are clearly defined
- their structure is simpler to change, as versioning is often unnecessary
- they are usually exchanged through an in-memory bus and are not persisted; it is fine to lose them when the application crashes
Integration events require more careful design because:
- they are out-of-process and are used to notify external systems about the changes in state
- should be published only after the local transaction completes, when the local state has become stable, as they coordinate distributed transactions
- they carry more complex payloads with additional attributes, as different listeners may require different information
- their structure is more robust to evolve, as they need to be backward compatible, often requiring versioning
- they must be persisted in a durable out-of-process event bus, as they play a critical role in maintaining data consistency across the entire system
It is common to model the same business fact as both a domain event (such as UserAgeChangedDomainEvent) and an integration event (for example UserAgeChangedIntegrationEvent), with the integration event being mapped from the domain event for external communication.
Event Structure
An event typically consists of three parts:
- name — identifies the event and often reflects what happened
- metadata — contains technical information about the event
- payload — the actual data describing what happened
The event structure should be consistent and applied across all events, allowing consumers to handle them in a predictable way.
— name
In Event-Driven Architecture, the event name is not just a label — it’s a contract. It tells consumers what exactly happened in the system (UserAgeChanged).
By convention, event classes are usually named:
- in the past tense (
UserCreated,OrderPaid) - with an
Eventsuffix (UserCreatedEvent)
This convention is widely used because it makes the intent obvious, but it’s not strictly required.
The event name also influences what must be included in the payload.
Generic names require richer payloads. An event like UserUpdatedEvent is vague. To make it useful, you typically need to include the previous and new states of an entity, so consumers can understand what actually changed.
Specific names can minimize payload. An event like UserDeletedEvent may need no payload at all. The name itself already fully describes what happened.
Additionaly, the name may include metadata, such as a version (UserCreatedEventV2). This can be useful for evolution and backward compatibility, but it should be applied to all events when.
— metadata
It is recommended, but not required, for every event to include metadata — technical information that makes events reliable, traceable, and safe to process.
Medatada typically includes the following properties:
- eventId — a unique event identifier (usually based on a GUID). Its primary purpose is audit and idempotency: ensuring that the same event is not processed more than once
- entityId (subjectId) — the identifier of the entity associated with this event (for example, an order or a user). It can be numeric, but in practice, a string is often chosen as a more generic and implementation-agnostic option
- version — indicates the schema version of the event. It allows events to evolve over time (adding or changing fields) without breaking consumers
- type — the event type that uniquely describes what happened (e.g.,
PaymentSuccess)
You may be wondering why the hell we need a type in metadata, if the event’s name should be sufficient 😅. Yes, event names are useful, but having a type in the metadata allows us to ease the refactoring (event renaming does not require changes in all consumers).
Similarly to eventId, it is also recommended to use a string instead of an enum for type. As it would allow for avoiding frequent changes to shared libraries, and reduce coupling between services.
You can also include other fields in metadata, like publishedBy, createdAt, correlationId, tenantId, and so on.
Metadata makes events self-describing. It allows consumers to process messages safely, trace their origin, avoid duplicates, and remain independent.
— payload
The payload is an optional part of an event that contains business data specific to that event.
There is no universal rule for what must be inside it. A common question is: what data should be included? How can a publishing service know in advance what data other consumers might need?
In theory, the publisher and consumers could agree on the exact data structure. And this is often an acceptable option if you have control over the producer and the consumer.
However, this creates tight coupling, which is especially undesirable in large systems or when events are exposed for external consumption.
Therefore, it is better when the publisher does not know how the data will be used, so publishers and consumers remain isolated and decoupled.
Choosing the right level of data granularity is crucial, so we’ll explore this in the next section.
Event Granularity
In EDA, it is important to choose how detailed or coarse your events are — how much information each event contains and how frequently they are emitted. Selecting the right granularity is crucial for clarity, performance, and maintainability.
For some events, no additional data is needed — their name or type is self-explanatory, so only the entity identifier is provided. The approach is called Event Notification.
While it may work for some events, others like UserAgeChanged, will cause consumers to fetch additional missing data explicitly via an API.
Small messages are lightweight, faster to process, minimize bandwidth, and easier for consumers to subscribe to. However, it is less scalable, slower due to additional queries, and creates coupling.
An alternative would be the Event-Carried State Transfer pattern, where the event carries all relevant state information related to the entity.
This method decreases coupling, scales more efficiently because consumers can handle the event without sending additional requests or putting extra load on the source.
However, larger messages increase network load, make serialization slower, can lead to data inconsistencies (as you have multiple sources of truth), and potentially can lead to data structure coupling.
The final choice is to use a document message and convey the entire snapshot of the entity state.
Document events are more challenging to listen to for specific changes, often requiring consumers to calculate deltas manually, and are less efficient in terms of memory and network bandwidth. However, they are easier to publish and eliminate the need for data fetching.
So, which option to choose?
The choice between those approaches depends on factors such as performance, memory usage, data ownership and consistency, consumer flexibility, coupling, and other requirements.
Publishing too many small events can overload the system and make the overall flow hard to understand — especially when you need to roll back changes. On the other hand, grouping too much into a single event can trigger unnecessary processing by consumers.
First and foremost, events exist because they are important for your domain. You should avoid modeling technical changes; focus on business-relevant transitions. An event should describe something that matters to a business stakeholder or domain expert.
Aim for balance by thinking about the impact of each event and whether consumers must dig into the payload just to decide how to react.
Event Versioning
Your application’s business logic changes over time. You may add fields, rename concepts, split/merge entities, etc.
However, old events that are already serialized, published, and stored in the broker still exist and will be consumed with outdated structures.
Therefore, you would have to manage event changes somehow.
First, keep in mind that some changes are backward-compatible — for example, adding a new property. If consumers don’t need this new property, they can simply ignore it.
However, if the change is significant (for example, renaming a property or removing important data), consumers will need to adapt. In such cases, you will likely introduce a version, either in the event name (e.g., UserNameChangedV1, UserNameChangedV2) or in its metadata. Consumers can support multiple versions in parallel and migrate safely as the event contract changes.
It’s also important to continue publishing older event versions until all consumers have migrated.
Event Reuse
When you have a publisher and subscribers sharing events, the first instinct is often to reduce code duplication by placing event definitions in some shared location.
You generally have a few options:
- Shared library (or a NuGet package) — this works well, but only if both producers and consumers are written in the same programming language
- Interface definition language (IDL) — you define an event schema, and each team uses tooling to generate event classes in their own language (similar to how Swagger/OpenAPI generates code from an API specification)
struct UserEvent {
1: i32 userId,
2: string name,
}- Event duplication — sometimes duplicating code is perfectly fine 🙃. If events are pure DTOs with no logic, duplication avoids stamp coupling and allows consumers to have different schemas, deserializing only the fields they actually need
Event Source (Producer/Publisher/Emitter)
The next component we are going to discuss is an event source.
It is responsible for creating and publishing events when something meaningful happens in the system.
public class UserService : IUserService
{
private readonly IEventProducer _eventProducer;
public UserService(IEventProducer eventProducer)
{
_eventProducer = eventProducer;
}
public async Task CreateUser(User user)
{
// business logic
. . .
// publish events
var event = new UserCreatedEvent(user.Id, user.Name);
await _eventProducer.Publish(@event);
}
}The emitter does not care who will handle the event or what will happen next — it simply reports a fact and moves on.
This keeps the producer loosely coupled from consumers and helps the system evolve independently.
Let’s see how an event producer differs from the traditional REST approach.
Uncertainty in Event Delivery
With REST APIs, we send a request and can determine how successful it was based on the response. In the Event Driven Architecture, things become more complicated.
When we publish an event, there is no guarantee that it will not be lost.
For example, the event may be delivered to the message broker but not yet consumed. If the message broker fails, the message could be lost. From the service’s perspective, the delivery operation appears successful.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
Even if the event is successfully delivered, we still have no way of knowing whether a consumer successfully processed it or if the order was right.
There could be no subscriber at all.
Moreover, we cannot know in advance when the event will be consumed.
This significantly complicates the overall application logic. If, for any reason, the consumer fails to receive or process this event, the system may end up in an inconsistent state. This responsibility is usually delegated to the message broker, which we’ll cover in the next sections.
Outbox Pattern
Ideally, all parts of your infrastructure (databases, message brokers, and other dependencies) should always be up and running. In practice, this is hard to achieve, and therefore, you should always try making your code robust and prepared for failure.
Consider a typical scenario where the message broker is unavailable. Your service successfully persists data in the database, but fails to publish the corresponding event.
As a result, an inconsistency appears between the event publisher and its consumers: the state has changed, but no event was emitted to notify other services.
This problem is commonly addressed using the Outbox pattern. Instead of publishing events after an operation has completed, the service stores them in the database during the transaction.
Later, a background process reads these stored events and publishes them to the message broker, ensuring reliable event delivery.
Batching Events
Sending (or persisting) messages one by one is highly inefficient. Moreover, since we do not expect an immediate response and communication is asynchronous, there is no real need to process each message individually.
To address this, the Message Sequence pattern is used. Messages are typically grouped into sequences (batches) and sent together, which improves throughput and reduces the overhead associated with message delivery.
Event Sourcing
When applying the Outbox pattern, an application performs two operations: persisting state and publishing events. These operations must stay in sync because they represent the same state transition. But what if you could rely only on events and compute the current application state from them? That is the idea of Event Sourcing.
Event-Driven Architecture and Event Sourcing are frequently used together because they complement each other naturally.
In EDA, events represent facts about what has already happened and are used to notify other parts of the system.
Event Sourcing takes this idea further by making those same events the source of truth for application state. Instead of storing only the current state, the system persists a sequence of domain events and rebuilds the state by replaying them.
This aligns perfectly with EDA, where events are already being produced and consumed.
⚠️ By the way ⚠️
When using a broker like Kafka to store events instead of a traditional database, it can effectively serve as both a data store and a message broker.
CQRS
If you opted in for Event Sourcing, it’s also a good idea to consider CQRS, especially when your service is frequently queried.
Implementing CQRS can help by improving query performance and reducing the need to constantly recalculate state from events.
Event Broker
After the event is created and published, it is sent to the event broker, a component through which the source and the listener exchange events
Sometimes developers wonder why we even need this component instead of directly sending events to each listener. This would result in tight coupling, making the system harder to maintain and scale.
Additionaly, handling failures or retries would become the producer’s responsibility, but as we are following the Single Responsibility principle, it's better to delegate the work to someone else 😅.
Technically, you could implement your own broker by storing events in a database and sending them to consumers, similar to a webhook mechanism. However, event-driven systems often have complex architectures and require advanced algorithms for efficient processing, routing, and delivery of events. Instead of building this from scratch, it is common practice to use existing, battle-tested solutions or proven architectural patterns.
Canonical Data Model
After the event is published, it is likely to be stored. However, which format should we use?
Technically, it’s possible to use one format for writing and another for reading, but this makes the system harder to understand and integrate.
The Canonical Data Model pattern addresses this by proposing a single, standardized data format for all participants in the message exchange.
Think of how we handle REST APIs: we usually pick a popular format like JSON and use HTTP as the transport protocol. This works well because almost every programming language has an HTTP client and libraries for creating and parsing JSON documents. It allows new microservices to be written in any language and run on any platform.
While being widely understood, JSON is not the most compact or performance-efficient format. There are others, like Protobuf or others. Just ensure it is platform-independent.
Topologies
In the Broker topology (Choreography), components publish events to a shared event broker, and other components decide independently whether to react to them or ignore them.
This approach works well when the event flow is relatively simple, but the lack of coordination makes multi-step business processes harder to manage.
Alternatively to this approach, we have the Mediator topology (Orchestration). Instead of broadcasting events, components publish events as commands to a specific mediator component.
The mediator manages process state, error handling, and retries, providing better control and consistency across distributed operations.
The trade-off is tighter coupling and the risk of the mediator becoming a bottleneck or single point of failure if poorly designed.
⚠️ By the way ⚠️
Typically, each domain has its own mediator, which helps reduce the risk of a single point of failure.
Communication Patterns
With the Event Bus pattern, participants are completely decoupled from each other. Services don’t need to know about each other, only about the bus. Consumers can act upon or filter out unwanted events, join or leave at any time.
If a message must have exactly one recipient, the Point-to-Point pattern is recommended, where both parties know about the channel. Even if multiple consumers are connected to the same message channel, only one of them will receive a given message. This pattern is usually implemented using queues and is more suitable for commands.
If a message should be delivered to all consumers, the Publish/Subscribe pattern should be used. This is typically implemented using topics and is ideal for events, where multiple consumers may need to react to the same events.
Datatype Channel
The Datatype Channel pattern promotes the idea of using one channel per event type.
In this approach, both the sender and the receiver know the channel name in advance and, based on it, understand what kind of data is published there and in which format (e.g UserCreatedEvent is published in UserCreatedEventChannel).
As a result, messages do not need to include extra metadata describing their structure or having additional logic to filter, transform or deserialize to correct types when processing the events.
Message Consumption Models
Messages can be delivered in two ways: push-based, where consumers receive messages automatically, and pull-based, where consumers read stored messages when ready.
In the publish/subscribe model, the messaging infrastructure manages subscriptions. When an event is published, it is pushed to all registered subscribers. It is simple, provides low latency, and is good for real-time reactions, but you cannot replay events, or you can even miss some if subscribed later.
In the event streaming model, events are appended to the list. Consumers read events from this stream. It is harder to implement, requires more storage space, but you can make sure events will be handled even when the consumer disconnects, you can easily reply to events, and handle them at your own pace.
Broker Reliability
A message broker can handle events in different ways: it may persist them temporarily in memory (for example, RedisMQ) or store them in durable storage, ensuring that messages remain in the system even after broker restarts or shutdowns.
The choice depends on balancing these factors:
- durability: Do I need a history of incoming messages (pull model), or messages are deleted after they are processed (push model)?
- scalability: Is the incoming message processed by another application? By multiple consumers?
- latency: How crucial is the time it takes to store and retrieve the message?
- resilience: Can I afford to lose all persisted inbox messages when the application goes down?
Often, in a large distributed system, it’s important that messages are not lost— even if the broker was temporarily down. Therefore, this requires the messaging system to have reliable storage for messages.
The Guaranteed Delivery pattern ensures that a message is stored in the system and not removed until all consumers have processed it.
Additionaly, it is also necessary to track the state of each consumer — what messages they have already read and which are still pending. This prevents missed messages if a consumer becomes overloaded or temporarily unavailable.
The Durable Subscriber pattern addresses this, requiring each consumer to have a unique identifier so it can be distinguished from others.
Message-Delivery Semantics
Depending on what matters more to your business — guaranteed delivery or single processing — different message delivery semantics can be applied.
Notice, exactly-once seems like an ideal option, but achieving it is nearly impossible because a message may be processed multiple times — especially after failures — so consumers must be designed to handle reprocessing safely using the Idempotent Receiver pattern to prevent duplicate side effects.
Dead Letter Channel
If an error occurs while processing a message due to its invalid content, the Dead Letter Channel pattern suggests routing the “poisoned” message to a special channel for further analysis and troubleshooting.
Note, this pattern applies both to push-based topics and pull-based channels, where messages can be skipped or deferred during processing.
Event Listener
The final component we are going to discuss is event listeners. They are responsible for receiving events from the event broker and reacting to them by executing some business logic.
An event consumer subscribes to one or more event types and is triggered whenever a relevant event occurs.
public class UserCreatedEventHandler : IEventHandler<UserCreatedEvent>
{
public async Task Handle(UserCreatedEvent @event)
{
// react to the event
// for example: send email, update local record in DB, etc...
}
}The consumer focuses only on what to do when the event happens, not on who produced the event.
Multiple consumers can react to the same event independently, each implementing its own behavior.
This loose coupling is one of the key strengths of Event-Driven Architecture and is a major difference compared to traditional REST-based communication, where services must explicitly call each other and coordinate execution.
Message Consumption Models
We already discussed this from the broker’s perspective, but let’s revisit the same terminology from the consumer’s.
When the consumer subscribes to messages and provides an asynchronous callback method that is automatically invoked whenever a message arrives, it is the Event-Driven Consumer pattern.
If a consumer is ready to receive messages and synchronously waits for new messages, it is known as the Polling Consumer pattern.
Asynchronous Processing
In an event-driven architecture, producers and consumers are decoupled, allowing them to work independently of each other’s performance.
This is especially useful when consumers are slower than producers, as messages can be buffered and processed at their own pace without blocking the producer.
From a performance and scalability perspective, asynchronous event processing is a major advantage. However, it also introduces additional complexity in managing cross-service communication, including logging, distributed tracing, error handling, and distributed transactions.
Competing Consumers Pattern
Processing messages one by one is simple, but it is not the fastest approach. And what do we do to improve throughput? We scale, vertically or horizontally.
Let’s say we add more instances of the same server. It means messages can be processed in parallel. However, when multiple consumers compete for messages in the same message channel, multiple challenges arise, like handling messages only once, proper message ordering, load balancing, fault tolerance, etc.
⚠️ By the way ⚠️
Although multiple consumers are involved, this is not the same as the Publish-Subscribe pattern. With Competing Consumers, each message is handled once, whereas in Pub/Sub, every consumer receives a message.
Eventual Consistency
As components are decoupled and communicate asynchronously, it is quite hard to achieve strong consistency.
When data is distributed across components and updated via events, changes don’t propagate instantly — they require time to reach all parts of the system. Users may see outdated data. This results in eventual consistency. It means that data will be updated… eventually 😁. But not immediately.
Of course, if both the publisher and the consumer are part of the same process, they can be grouped under a single transactional scope, ensuring atomic execution. However, this effectively collapses the system back into a tightly coupled, synchronous design and defeats the core purpose of EDA.
As a result, most EDA systems typically favor eventual consistency, accepting temporary inconsistencies in exchange for scalability, resilience, and loose coupling.
Client Acknowledgement Mode
A message can be acknowledged as soon as it is received or processed, but this approach can lead to serious issues.
One common problem is consumer failure — either software or hardware — during message processing. If the message is marked as acknowledged in the broker before processing completes, the message is effectively lost and will never be handled.
To avoid this, consumers can use manual acknowledgements, confirming message consumption only after successful processing.
Idempotency
Idempotency is a property of an operation, which guarantees that performing the operation multiple times will yield the same result as if it was executed only once
Or in plain English: if an event consumer, due to a system bug, receives the same “charge money” event ten times, the payment will be processed only once.
Idempotency is a critical concept in event-driven architectures because message delivery is not guaranteed to happen exactly once.
Picture this: a consumer starts processing an event, updates the database, but fails before acknowledging it; the message broker retries delivering the same event. As a result, a consumer receives duplicate messages.
To prevent this, consumers should be designed so that processing the same event multiple times produces the same outcome as processing it once. This is commonly achieved by tracking processed event identifiers, using idempotency keys, or relying on database constraints.
Without idempotent handling, retries can lead to inconsistent state, duplicated side effects, or corrupted data.
Compensating Transactions
Error handling becomes especially challenging in distributed systems due to the lack of atomicity. The more services that participate in a single business transaction, the harder it is to guarantee that all steps either succeed or fail together.
For instance, a service executes an action successfully, but later the system discovers that the next consumer fails. In such cases, it must reverse the operation by publishing a compensating event that undoes the previous action.
The difficulty is that these steps occur asynchronously. Compensating transactions are rarely a simple rollback of the original operation. By the time compensation runs, the system state may have already changed due to other transactions, which makes reliable recovery even more complex.
CQRS
CQRS is not limited to the producer side and can be effectively applied to consumers, too.
Since CQRS operates in an asynchronous world, commands can be delivered to consumers via a queue, processed independently, and the resulting outcomes can be published back to the event bus.
Summary
As you see, compared to REST, Event Driven Architecture does not have well-established conventions.
Which event (or events) should be emitted? How should event classes be named, and what data should they contain? How can we be sure that the provided information is sufficient for other services to handle the event correctly?
Fortunately, we can rely on multiple practices and patterns 🙃.
- The core idea of Event-Driven Architecture (EDA) is to produce an event on every state change. Something happens → an event is created
- Despite its conceptual simplicity, this architecture works regardless of your scale: with classes, modules, or distributed microservices
- Many projects combine Event-Driven Architecture with others like microservices, modular monoliths, etc
- EDA does not mean microservices, but often complete them
- Use this architecture when multiple subsystems need to handle the same events, and you want to decouple producers from consumers to scale and evolve them independently
- The clear benefits are that it decouples producers and consumers, improves scalability, resilience, and performance through asynchronous processing and horizontal scaling
- The challenges it brings are increased infrastructure costs and operational complexity, complex error handling, data inconsistency, event ordering, idempotency, and data loss
Pick the approach that best suits the complexity and requirements of your domain 😉
Give this article a clap or a few if you like it 👏
You can support me with the link below ☕️
Don’t forget to follow to receive more ✅
Out the door, dinosaur 🦖







