home..

Integration Of Event Driven Systems

In today’s fast-paced and interconnected world, businesses and developers are constantly seeking innovative ways to build efficient and scalable applications. One of the most compelling architectural paradigms that has gained significant momentum is event-driven architecture (EDA). Event-driven applications are designed to respond to real-time events and have become the go-to choice for building systems that can adapt and thrive in dynamic environments. Central to the success of event-driven applications are integrations, which play a pivotal role in ensuring seamless communication and collaboration between various services and components. In this blog post, we will explore how the Saga pattern complements integrations in the context of event-driven applications.

Understanding Event-Driven Architecture

Event-driven architecture is an approach that allows systems to communicate and interact through the exchange of events. An event can be any noteworthy occurrence or action result, such as a user clicking a button, a sensor reading, a message arriving in a queue, or a database record being updated. Unlike traditional request-response models, EDA enables decoupled components, making applications more flexible, extensible, and resilient.

The Importance of Integrations

Integrations lie at the heart of event-driven applications, enabling them to leverage the power of multiple services and applications. They allow different components of the system to exchange events and data seamlessly, fostering a loosely coupled and modular architecture. Integrations simplify the complexity of building event-driven systems by promoting reusability and standardization.

With integrations, developers can:

  1. Enable Interoperability - Integrations facilitate interoperability between diverse systems and services. By defining common event formats and communication protocols, developers can ensure that each component understands and reacts to events appropriately. This enables easy integration of new services into the existing ecosystem without disrupting the entire application.

  2. Enhance Scalability - In a rapidly growing application, scaling individual components can become challenging. Integrations allow horizontal scaling, where new instances of services can be added to handle increasing event loads. This ensures that the system maintains performance and responsiveness even under heavy traffic.

  3. Foster Microservices Architecture - Event-driven applications often adopt a microservices architecture, where functionalities are split into smaller, specialized services. Integrations act as the glue that holds these microservices together, facilitating smooth communication between them. This modularity makes it easier to develop, deploy, and maintain individual services, promoting agility and faster development cycles.

  4. Improve Resilience - In event-driven applications, failure of one component should not bring down the entire system. Integrations allow for graceful degradation, where the application can continue to function despite the failure of a single service. By incorporating event queues, retries, and error handling mechanisms, developers can ensure that critical events are not lost and that the system can recover from failures autonomously.

The Saga Pattern: Coordinating Local Transactions

In event-driven systems, an operation often spans multiple services or components, and ensuring data consistency can be challenging. The Saga pattern introduces a way to choreograph a series of local database transactions across multiple services, ensuring that either all the transactions succeed, or the system reverts to a consistent state if any of them fail.

“A saga is a sequence of local transactions. Each local transaction updates the database and publishes a message or event to trigger the next local transaction in the saga. If a local transaction fails because it violates a business rule then the saga executes a series of compensating transactions that undo the changes that were made by the preceding local transactions. In a Saga, each step in the process is represented by a local transaction associated with a compensating action. If any of the steps fail, the Saga executes the corresponding compensating actions to rollback the changes made by the previous steps, thus maintaining data integrity.” – Chris Richardson

The idea of breaking down long-running operations into shorter, self-contained executions isn’t new. This principle was first formalized in the 1987 paper Sagas1 by Hector Garcia-Molina and Kenneth Salem.

In 2009, Pat Helland extended this concept to modern systems in his paper Building on Quicksand2. By dividing long-lived operations into idempotent, short-lived steps while capturing the intermediate state, applications can maintain stability even in the face of interruptions or failures. This concept forms the foundation of Event-Driven Architecture.

Fmodel and Saga Pattern

In Fmodel3, Saga pattern is implemented as a Saga data type.

data class Saga<in AR, out A>(
    val react: (AR) -> Flow<A>
) 

The component is generic in two type parameters:

The output/event/action result of one service is translated into the input/command/action of another service. This is the essence of the Saga pattern. It reacts!

By implementing Event Sourcing pattern and storing events as a result of local transactions, we can easily implement the Saga pattern. It fits perfectly into the robust event-driven architecture. We prefer to model business errors as events4 and not as exceptions. This way you can easily implement compensating actions/commands in case of business errors.

The saga component is pure, and on this level, we are not dealing with any side effects. It is a simple function that takes an Action Result and returns a Flow of Actions.

Integrating two components/deciders within the single system

An example of Saga component that is integrating two components/deciders within the single system. It is exhaustively reacting on all Events (OrderPlacedAtRestaurantEvent, RestaurantCreatedEvent, RestaurantMenuChangedEvent, RestaurantErrorEvent, OrderEvent) and translating them into Commands (CreateOrderCommand), as shown below:

typealias OrderSaga = Saga<Event?, Command>

fun orderSaga() = OrderSaga(
    react = { e ->
        when (e) {
            is OrderPlacedAtRestaurantEvent -> flowOf(
                CreateOrderCommand(
                    e.orderId,
                    e.identifier,
                    e.lineItems
                )
            )

            is RestaurantCreatedEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
            is RestaurantMenuChangedEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
            is RestaurantErrorEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
            is OrderEvent -> emptyFlow() // We are not interested in Order events, so we return the empty flow of commands.
            null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas.
        }
    }
)

The order saga represented as a sequence diagram:

order saga sequence diagram

Imagine extending this saga to include compensating actions in case of order being rejected:

order saga sequence diagram

This is an orchestrating Saga. It orchestrates Order and Restaurant deciders.

In FModel, you can choose to create two choreography sagas and combine them into single orchestrating saga (the one that you have now):

typealias OrderChoreographySaga = Saga<RestaurantEvent?, OrderCommand>

fun orderChoreographySaga() = OrderChoreographySaga(
    react = { e ->
        when (e) {
            is OrderPlacedAtRestaurantEvent -> flowOf(
                CreateOrderCommand(
                    e.orderId,
                    e.identifier,
                    e.lineItems
                )
            )

            is RestaurantCreatedEvent -> emptyFlow()
            is RestaurantMenuChangedEvent -> emptyFlow()
            is RestaurantErrorEvent -> emptyFlow()
            null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas.
        }
    }
)
typealias RestaurantChoreographySaga = Saga<OrderEvent?, RestaurantCommand>

fun restaurantChoreographySaga() = RestaurantChoreographySaga(
    react = { e ->
        when (e) {
            is OrderCreatedEvent -> emptyFlow()
            is OrderPreparedEvent -> emptyFlow()
            is OrderPaidEvent -> emptyFlow()
            is OrderErrorEvent -> emptyFlow()
            null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas.
        }
    }
)

Because OrderCommand and RestaurantCommand are extending the Command, and OrderEvent and RestaurantEvent are extending the Event, you can combine these two sagas into one:

orderSaga() = orderChoreographySaga() combine restaurantChoreographySaga()

We choose to run these deciders and saga(s) as part of a single database transaction in this case. In case of integrating with the third-party system we will use the same principle, just we are going to run it in a different way.

This demonstrates how the concept of Saga can be used to integrate both internal components/deciders and external systems consistently. The primary difference lies in the deployment strategy.

Integrating with third-party systems

An example of Saga component that is integrating our system with third-party payment provider. It is exhaustively reacting on all Events (OrderPreparedEvent, OrderCreatedEvent, …) and translating them into Commands, as shown below:

typealias PaymentSaga = Saga<Event?, Command>

fun paymentSaga() = PaymentSaga(
   react = { e ->
      when (e) {
         is OrderPreparedEvent -> flowOf(PayCommand(orderId = e.identifier)) // We are only interested in `OrderPreparedEvent` and we translate it into `PayCommand`
         is OrderCreatedEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
         is OrderErrorEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
         is OrderPayedEvent -> emptyFlow() // We are not interested in this event, so we return the empty flow of commands.
         is RestaurantEvent -> emptyFlow() // We are not interested in Restaurant events, so we return the empty flow of commands.
         null -> emptyFlow() // We ignore the `null` event by returning the empty flow of commands. Only the Saga that can handle `null` event/action-result can be combined (Monoid) with other Sagas.
      }
   }
)

payment saga sequence diagram

Check the PaymentSagaManager and how it is registered in the main application class

Check out the full example

Saga manager is dealing with all the side effects (reading events from event store, publishing commands to some command bus or invoking services directly, etc.) by using the Saga component as an established algorithm. The concrete implementation of the saga manager is on you. We vision it as an Event store (or Kafka) event subscriber/handler:

Event Modeling and Integrations

Event modeling is promoting integration by categorizing two patterns:

The Saga pattern is a perfect fit for both of these patterns. It is a translation pattern (translating events to commands) by definition. It translates foreign event into a command that is more familiar in our own system, or translates our event into a command of the foreign system, acting as an anti-corruption layer.

It can easily support automation by default. The trick is to model and track the state/status of the saga as a green sticky note (view) on the event model. That state/status can be as little as a tracking token/sequence/position of the last event that was processed successfully by the saga (imagine Event Store/Kafka ACK/NACK mechanism). As we mentioned previously, this is improving resilience of the system as you can easily recover from technical failure by replaying/retrying the saga from the last known position.

the flow

The image is showing two sagas:

Most of the well known event stores or brokers are supporting the concept of a tracking token/sequence/position of the last event that was processed successfully by the saga/event handler client in one way or another. It is good to model that tracking position as a green sticky note/state.

This demo is using PostgreSQL powered event store, optimized for event sourcing and event streaming.

Fstore-SQL is enabling event-sourcing and pool-based event-streaming patterns by using SQL (PostgreSQL) only.

Conclusion

Integrations enable seamless communication and collaboration between different services, fostering a flexible and scalable architecture. Meanwhile, the Saga pattern ensures data consistency and helps manage business transactions in event-driven systems.

As businesses continue to adopt event-driven architecture to meet the demands of real-time data processing and event handling, mastering the art of integrations and understanding how to apply the Saga pattern effectively will be essential for building robust, adaptable, and resilient applications. By combining these two approaches, developers can build event-driven applications that excel in performance, reliability, and scalability, empowering businesses to thrive in an increasingly interconnected world.

Having events durably stored is truly enabling event-streaming and event-driven architecture. It is a prerequisite for the robust automation of business processes. Consider Event Sourcing as a foundation for your event-driven architecture.

There is so much to cover on this topic. We will continue to write about it in the future. #APISchemaEvolution, #DeliveryGuarantees, #ConsumerDrivenContractTesting, #BoundedContextMappingsStay tuned, and check out our Miro board here.


  1. paper: Sagas 

  2. paper: Building on Quicksand 

  3. FModel can be used as a library, or as an inspiration, or both. It provides just enough tactical Domain-Driven Design patterns, optimised for Event Sourcing and CQRS. 

  4. Unmanaged Hazards - By modeling errors as events, we do not lose any information. Error events could be stored durably to help with future investigations. 

© 2024 fraktalio d.o.o.