Event-driven architecture with CQRS & event sourcing

vinay kumar
Techartifact-Technology learning
10 min readJan 19, 2023

Event-driven architecture (EDA) is a software architecture paradigm that is based on the concept of events and event handling. In an event-driven system, a state change (an “event”) is handled by one or more “event handlers” that respond to the event in a specific way.

In an event-driven architecture, the system is composed of loosely coupled components that communicate with each other through the exchange of events. These events can be triggered by external inputs (such as a user action), internal state changes (such as a timer expiring), or messages received from other systems (such as a message from a message queue).

One of the key benefits of an event-driven architecture is that it promotes loose coupling between components, which makes it easier to change, test, and scale the system. It also allows for more efficient use of resources, as components only need to react to events that are relevant to them.

Event-driven architectures can be implemented in a variety of ways, including through the use of message queues, event buses, or event-driven microservices. Some critical technologies and patterns associated with event-driven architecture include event sourcing, Command Query Responsibility Segregation (CQRS), and Domain-Driven Design (DDD).

Event-driven architecture can be beneficial in several ways:

  1. Decoupling: By using events to drive data flow and application logic, event-driven architecture decouples different components and services, making the system more modular and easier to maintain.
  2. Scalability: Event-driven architecture is highly scalable, as new components and services can be added to handle the increased load without affecting existing components.
  3. Flexibility: Event-driven architecture allows for a more flexible and responsive system, as different components and services can react to real-time events.
  4. Microservices: Event-driven architectures are well suited to building microservices, as they enable services to communicate through events, enabling each service to be developed, deployed, and scaled independently.
  5. Asynchronous: Event-driven systems are usually asynchronous, meaning that events can be processed independently of the order in which they were received. This allows for efficient use of resources and can improve the system's overall performance.
  6. Loose coupling: Event-driven architecture allows different components and services to communicate without being tightly bound to each other, which makes it easier to add, remove, or update components without affecting the rest of the system.
  7. Real-time: Event-driven systems can provide real-time functionality and can quickly respond to events as they happen.
  8. Event-sourced: Event-driven systems can be event-sourced which enables tracking the history of the system, making it easier to understand how the system evolved and simplifying debugging and auditing.

Event-driven architecture is not a one-size-fits-all solution, it depends on the use case and context, but it can be a powerful tool for building large, complex systems that need to be highly responsive, scalable, and flexible.

Event-driven architecture has several disadvantages, including:

  1. Complexity: Event-driven systems can be complex to design, build, and maintain, as they involve multiple components and services that need to be coordinated and integrated.
  2. Debugging: Debugging an event-driven system can be difficult, as it can be hard to trace the flow of events and understand how different components and services are interacting.
  3. Latency: Event-driven systems can introduce latency, as events may need to be passed through multiple components and services before they are processed.
  4. Event Spaghetti: With too many event listeners and emitters, the system can become hard to understand and maintain, making it difficult to reason about the system’s behavior.
  5. Handling errors: In event-driven systems, handling errors can be complicated, as it can be difficult to determine the source of an error and how it should be handled.
  6. Testing: Event-driven systems can be difficult to test, as it can be hard to create test scenarios that mimic the flow of events in a real-world system.
  7. Dependency on event broker: Event-driven systems rely on event brokers or message queues to handle events, if these systems fail, the whole system may fail.

Event-driven architecture can present several challenges, including:

  1. Event modeling: Modeling events and determining how they should be used to drive the flow of data and application logic can be a complex task. It requires a deep understanding of the business domain and the interactions between different components and services.
  2. Event consistency: Ensuring consistency across events and maintaining a consistent view of the system state can be challenging, as different components and services may process events differently.
  3. Event order: In some cases, it’s essential to ensure that events are processed in a specific order; this can be a challenge in event-driven systems as events are usually processed asynchronously.
  4. Event handling: Handling events in a way that ensures reliability and consistency can be a challenge, as events may be lost or duplicated.
  5. Event security: Event-driven systems can present security challenges, as events may be intercepted and tampered with while in transit.
  6. Event Auditing: Keeping track of events and their flow is important for compliance, debugging, and troubleshooting. This can be challenging in event-driven systems that process a large number of events.
  7. Deployment: Event-driven systems can be challenging to deploy, as they may require complex networking and infrastructure to handle the flow of events.
  8. Event management: Managing the lifecycle of events, including persistence, retrieval, and archiving, can be a challenge in event-driven systems.

CQRS-

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates the operations that read data (queries) from the operations that update data (commands). The idea behind CQRS is to separate the concerns of reading and writing data so that the system can be optimized for each use case.

In a CQRS system, the read side and the write side are separated, with different models and data stores for reading and writing data. The read model is optimized for read-only operations, such as displaying data to the user, while the write model is optimized for updates and data validation. This separation allows for better scalability, as the read side can be optimized for high-performance read operations, and the write side can be optimized for transactional consistency.

CQRS is often used in conjunction with event-driven architecture, where events are generated when data is updated, and the read model is updated in response to these events. This allows for the read model to be kept in sync with the write model, so that the system is always consistent.

CQRS can be beneficial in systems that need to handle high traffic, handle high data volume and provide an optimized read and write performance. However, it can make the system more complex and harder to reason about the system’s behavior.

Usage of CQRS in event-driven architecture

CQRS and event-driven architecture (EDA) complement each other well and can be used together to build highly scalable, flexible and responsive systems.

In an event-driven system, events are generated by one component or service and are then consumed by other components or services, allowing them to take appropriate action. With CQRS, the read and write operations are separated, so the read operations can be optimized for high-performance read operations and the write operations can be optimized for transactional consistency. This separation allows for better scalability and fault tolerance, as the read and write sides can be scaled independently.

When events are generated in a CQRS system, they can be used to update the read model, keeping it in sync with the write model. This allows the read model to be optimized for read-only operations, such as displaying data to the user, and the write model to be optimized for updates and data validation.

CQRS and EDA also allows to handle eventual consistency, as the read model can be updated asynchronously after an event is processed, so the user can see the updated data with a delay. This can be useful in systems where high availability is required and it’s acceptable to have a delay in the updated data.

Overall, the combination of CQRS and event-driven architecture can be used to build highly scalable, flexible and responsive systems that can handle high traffic and large data volumes, while providing optimized read and write performance.

Event Sourcing

Event sourcing stores the state of a database object as a sequence of events — essentially a new event for each time the object changed state, from the beginning of the object’s existence. An event can be anything user-generated — a mouse click, a key press on a keyboard, and so on.

The system of record is a sequential log of all events that have occurred during a system’s lifetime.

Principles of an event sourcing pattern:

  • The events are immutable.
  • There can be as many events for the given entity as needed — that is, there is no limit on the number of events per object.
  • Every event name represents the event’s meaning — for example, NewUserCreationEvent.
  • To use the entity in the application (for example, to display the name of a user in the UI), you create a flat representation of the entity. Each subsequent use of the entity recalculates its current state via the sequence of state-changing events.

Event sourcing is a technique often used in conjunction with event-driven architecture, where the state of a system is determined by the sequence of events that have occurred.

Event sourcing has several benefits, including:

  1. Auditing and traceability: Event sourcing allows for the complete history of a system to be recorded, making it easy to trace the changes that have occurred and to perform auditing.
  2. Flexibility: Event sourcing allows for new business rules to be added to a system without modifying the existing data, which makes it more flexible and easier to change.
  3. Scalability: Event sourcing allows for horizontal scaling, as new servers can be added to handle increased load without affecting existing servers.
  4. Fault tolerance: Event sourcing allows for the system to be rebuilt from the event log, which makes it more fault tolerant and able to recover from failures.
  5. Simplicity: Event sourcing can simplify the codebase and the overall architecture, because it separates the concerns of storing state and applying business rules.
  6. Versatility: Event sourcing can be applied to a variety of systems, from simple CRUD-based systems to complex, event-driven systems.
  7. Event-sourced systems: Event sourcing is the foundation of event-sourced systems, where the state of the system is determined by the stream of events that have occurred. Event-sourced systems can provide a single source of truth, making it easy to reason about the system’s behavior.
  8. Time-travel debugging: Event sourcing allows developers to recreate the system’s state at any point in time, which can be helpful for debugging and troubleshooting.

Event sourcing is not a silver bullet and it depends on the use case and the context, it’s important to weigh the pros and cons of this technique and decide whether it’s the right fit for a given project.

Understanding the CQRS Architecture

Let’s look at the schema that demonstrates how you can implement a CQRS architecture without event sourcing. The first diagram illustrates an example of CQRS architecture without event sourcing:

Understanding the CQRS Architecture

The user interface (UI) component issues commands to update data. The “write” part of the system processes these commands and saves data in the “write” database. This part simultaneously uses data from the “write” database to calculate the state of the data and write it in the “read” database. The UI then interacts with the “read” database to fetch needed data.

Now let’s illustrate how the schema changes when we use both CQRS and event sourcing.

As shown, the “read” part remains unchanged. But the “write” database is now represented by the queue of events (the event store). The “write” segment of the application publishes events (commands) in the queue (a Kafka topic, for example). The event handler consumes events from the event store and updates data in the “read” database. The current states of the entities are stored only in the “read” database. You can extract the entire history of entity transformation using a sequence of the events stored in the “write” database. CQRS is implemented by a separation of responsibilities between commands and queries, and event sourcing is implemented by using the sequence of events to track changes in data.

Best Use Cases for CQRS Implementation

The CQRS approach (with event sourcing) may be appropriate when:

  1. The business logic of the system is complex. CQRS keeps logic of data changes apart from reading data. This means different components of the system can evolve in their own way, and be optimized as needed. This is akin to a microservices architecture. CQRS makes the system more flexible, ready for scaling and changes, and easier to maintain.
  2. One of the key benefits of CQRS is easier scalability; it’s easier to work individually with isolated components of the system without worrying about other components.
  3. Teams of developers are large and distributed. It’s more convenient to split work between teams when the elements of the system are loosely coupled. The most experienced developers can organize the work of the system as a whole; less skilled developers work on specific components.
  4. You want to test different data processing logic. This is especially true if the system has multiple components even without CQRS (for example, a monitoring tool, a search app, an analytics app, and so on). All these tools are consumers of data (or events).
  5. You built your production system in the traditional CRUD paradigm, but you have serious performance issues that are hard to solve without changing the architecture. Splitting the application into the “read” and “write” parts can help to improve performance. It enables you to focus on system bottlenecks and critical points of failure.

--

--

vinay kumar
Techartifact-Technology learning

Head of API/Integration & Application, Enterprise Architect Leader. Author ,Blogger #api #apimanagement #azure#productdevelopment #kafka #Architecture #DataMesh