Nyimbi Odero

Series 01 · a numbered run · kept in public

Lesson 8: Event-Driven Architectures and Asynchronous Processing in DSLs

Welcome back! In this lesson, we’ll take the concept of workflows further by integrating event-driven architectures and asynchronous processing into our DSL. This allows workflows to be triggered by external events and supports real-time notifications and asynchronous task processing, which are crucial for large-scale systems like ERP/CRM platforms or multi-sided marketplaces.

Table of Contents

  1. Introduction to Event-Driven Architectures

  2. Defining Events in the DSL

  3. Generating Asynchronous Code with Celery and Flask

  4. Handling Real-Time Notifications with WebSockets

  5. Practical Example: Event-Driven Order Fulfillment System

  6. Conclusion and Next Steps

1. Introduction to Event-Driven Architectures

What is an Event-Driven Architecture?

An event-driven architecture (EDA) is a design pattern in which the flow of program execution is determined by events, such as user actions (e.g., clicking a button), sensor outputs, or messages from other programs. Instead of executing code in a strict sequence, event-driven systems respond to events and process them asynchronously.

Key Components in Event-Driven Architectures:

  1. Events: Changes or actions that trigger reactions within the system.

  2. Event Producers: Components that generate events (e.g., a payment system confirming a transaction).

  3. Event Consumers: Components that listen for and react to events (e.g., a workflow moving to the next state).

  4. Event Handlers: Logic that defines what happens when an event occurs.

Why Use Event-Driven Architectures in DSLs?

  • Real-Time Responses: Event-driven systems can react to external triggers in real-time.

  • Asynchronous Processing: Allows tasks to be performed in the background (e.g., sending emails, processing orders).

  • Decoupling: By decoupling event producers from consumers, the system becomes more modular and scalable.

2. Defining Events in the DSL

How to Define Events in a DSL

Events can be introduced into our DSL to allow certain actions or transitions in a workflow to be triggered by external systems or time-based events. For example, in an order fulfillment system, the shipmentConfirmed event could be triggered by a third-party logistics provider, updating the order’s workflow.

Syntax for Events in the DSL:

Let’s extend our DSL to define events and event handlers.

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

transition fulfill -> Fulfilled by FulfillmentSystem if inventoryIsAvailable();

event shipmentConfirmed -> Shipped by LogisticsSystem;

}

state Fulfilled {

// End of workflow

}

state Shipped {

// End of workflow

}

}

Key Elements in the Event-Driven DSL:

  • Events: Defined with the event keyword. For example, shipmentConfirmed is an event.

  • Event Handlers: The system will move to a new state when an event is triggered and handled. In this case, shipmentConfirmed moves the order from Fulfilled to Shipped.

  • Transitions and Conditions: You can combine events with conditions and actors, allowing the DSL to handle more complex scenarios.

3. Generating Asynchronous Code with Celery and Flask

Why Use Celery?

For handling background tasks and asynchronous processing, we can integrate Celery, a distributed task queue, with Flask. Celery allows us to process tasks like sending notifications, managing external API calls, or updating workflows without blocking the main application.

Step 1: Setting Up Celery

First, install the necessary dependencies:

pip install celery redis

Next, configure Celery in your Flask application:

from celery import Celery

def make_celery(app):

celery = Celery(

app.import_name,

backend=app.config

′CELERYRESULTBACKEND′'CELERY_RESULT_BACKEND'

,

broker=app.config

′CELERYBROKERURL′'CELERY_BROKER_URL'

)

celery.conf.update(app.config)

return celery

app = Flask(name)

app.config.update(

CELERY_BROKER_URL=‘redis://localhost:6379/0’,

CELERY_RESULT_BACKEND=‘redis://localhost:6379/0’

)

celery = make_celery(app)

Step 2: Generating Asynchronous Task Code

In our DSL, events like shipmentConfirmed should trigger asynchronous tasks in the background. For example, when an order is shipped, a notification could be sent, or the system could call an external API.

Asynchronous Task in Celery:

@celery.task

def handle_shipment_confirmation(order_id):

Logic to handle shipment confirmation

order = Order.query.get(order_id)

order.state = ‘Shipped’

db.session.commit()

Notify external system or user

print(f”Order {order_id} has been shipped.”)

Triggering the Task in the Generated Workflow Code:

def shipment_confirmed(order_id):

handle_shipment_confirmation.delay(order_id)

This ensures that the shipmentConfirmed event is processed in the background, allowing the main application to continue handling other requests without delay.

4. Handling Real-Time Notifications with WebSockets

Why Use WebSockets for Real-Time Notifications?

WebSockets allow for two-way communication between a client and server, enabling real-time updates. This is useful when you want the client (e.g., an admin dashboard or mobile app) to receive immediate notifications when an event (such as an order being fulfilled or shipped) occurs.

Step 1: Setting Up Flask-SocketIO

Install the necessary dependencies:

pip install flask-socketio

Configure Flask-SocketIO in your application:

from flask_socketio import SocketIO

socketio = SocketIO(app)

@app.route(‘/trigger-event’, methods=

′POST′'POST'

)

def trigger_event():

data = request.get_json()

order_id = data.get(‘order_id’)

Emit real-time notification via WebSocket

socketio.emit(‘order_status_update’, {‘order_id’: order_id, ‘status’: ‘Shipped’})

return jsonify({‘status’: ‘success’}), 200

Step 2: Generating WebSocket Code for Real-Time Updates

In the DSL, we can define notifications as part of the event handling process:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

event shipmentConfirmed -> Shipped by LogisticsSystem notify Admin;

}

state Fulfilled {

// End of workflow

}

state Shipped {

// End of workflow

}

}

Generating Code for Notifications:

def shipment_confirmed(order_id):

handle_shipment_confirmation.delay(order_id)

socketio.emit(‘order_status_update’, {‘order_id’: order_id, ‘status’: ‘Shipped’})

This will notify the admin in real-time when an order is marked as shipped.

Step 3: Real-Time Notification on the Client Side

On the client side, listen for the real-time updates using WebSocket connections.

const socket = io.connect(‘http://localhost:5000’);

socket.on(‘order_status_update’, function(data) {

console.log(‘Order ’ + data.order_id + ’ is now ’ + data.status);

// Update the UI with the new order status

});

Now, whenever the shipmentConfirmed event is triggered, the admin dashboard (or other clients) will receive a real-time notification.

5. Practical Example: Event-Driven Order Fulfillment System

Let’s implement a full event-driven order fulfillment system using the concepts we’ve covered so far. This system will handle order approvals, fulfillment, and shipping, with asynchronous processing and real-time notifications.

DSL for Event-Driven Order Workflow:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

transition fulfill -> Fulfilled by FulfillmentSystem if inventoryIsAvailable();

event shipmentConfirmed -> Shipped by LogisticsSystem notify Admin;

}

state Fulfilled {

// End of workflow

}

state Shipped {

// End of workflow

}

}

Step 1: Generated Workflow Code with Events and Asynchronous Tasks:

from celery import Celery

class OrderProcessingWorkflow:

states =

′Pending′,′Approved′,′Rejected′,′Fulfilled′,′Shipped′,'Pending', 'Approved', 'Rejected', 'Fulfilled', 'Shipped',

def init(self):

self.state = ‘Pending’

def approve(self, actor):

if self.state == ‘Pending’ and actor == ‘Manager’:

self.state = ‘Approved’

else:

raise ValueError(“Invalid transition or actor”)

def fulfill(self, actor):

if self.state == ‘Approved’ and actor == ‘FulfillmentSystem’ and inventoryIsAvailable():

self.state = ‘Fulfilled’

else:

raise ValueError(“Invalid transition, actor, or condition not met”)

def shipment_confirmed(self, order_id):

handle_shipment_confirmation.delay(order_id)

socketio.emit(‘order_status_update’, {‘order_id’: order_id, ‘status’: ‘Shipped’})

Step 2: Asynchronous Task for Shipment Confirmation:

@celery.task

def handle_shipment_confirmation(order

_id):

Process the shipment confirmation

order = Order.query.get(order_id)

order.state = ‘Shipped’

db.session.commit()

Notify external system or user

print(f”Order {order_id} has been shipped.”)

Step 3: Real-Time Notification Using Flask-SocketIO:

@app.route(‘/order/int:order_id/shipment-confirmed’, methods=

′POST′'POST'

)

def shipment_confirmed(order_id):

workflow = OrderProcessingWorkflow()

workflow.shipment_confirmed(order_id)

return jsonify({‘status’: ‘success’, ‘new_state’: ‘Shipped’}), 200

Step 4: Client-Side WebSocket Notification:

const socket = io.connect(‘http://localhost:5000’);

socket.on(‘order_status_update’, function(data) {

console.log(‘Order ’ + data.order_id + ’ is now ’ + data.status);

// Update the UI with the new order status

});

6. Conclusion and Next Steps

In this lesson, we’ve:

  • Introduced event-driven architectures and integrated them into the DSL.

  • Used Celery for asynchronous task processing.

  • Leveraged WebSockets for real-time notifications.

  • Implemented a full event-driven order fulfillment system using Flask, Celery, and Flask-SocketIO.

This approach allows the system to scale efficiently, handle asynchronous processes, and provide real-time feedback to users.

Practical Exercise

Task: Extend the DSL to support time-based events (e.g., sending reminders after a certain time period) and implement the necessary code to handle these events asynchronously.

Hints:

  • Use Celery beat to schedule periodic tasks.

  • Define time-based events in the DSL.

Example DSL:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

event sendReminder -> Pending by System after 24 hours;

}

}

Looking Ahead

In the next lesson, we will focus on multi-threaded and distributed systems, where workflows and events span across multiple services and machines. We’ll explore techniques for synchronizing events and handling concurrency in large-scale distributed systems.

Feel free to reach out if you have any questions. See you in the next lesson!