Introduction
Event-driven architecture allows application components to communicate through events instead of direct calls. This makes the system more flexible, scalable, and easier to maintain. In this tutorial you will build a simple order processing system where each step (creation, payment, shipping) reacts to events. This approach eliminates tight coupling between modules and simplifies adding new features.
Prerequisites
- Node.js 20 or higher
- Basic JavaScript knowledge
- A code editor (VS Code recommended)
Project Initialization
mkdir event-driven-demo
cd event-driven-demo
npm init -y
npm install eventsThis command creates the project folder and initializes a Node.js project. The events module is native but we import it explicitly for clarity.
Creating the Event Bus
const EventEmitter = require('events');
class EventBus extends EventEmitter {
emitEvent(eventName, payload) {
console.log(`[EventBus] Événement émis: ${eventName}`);
this.emit(eventName, payload);
}
}
const eventBus = new EventBus();
module.exports = eventBus;We create an EventBus class that extends EventEmitter. The emitEvent method centralizes logging and event emission to simplify debugging.
Order Service
const eventBus = require('./eventBus');
function createOrder(orderData) {
console.log('Commande créée:', orderData.id);
eventBus.emitEvent('order.created', orderData);
}
module.exports = { createOrder };The order service simply emits an 'order.created' event after creating an order. It has no knowledge of payment or shipping.
Payment Handler
const eventBus = require('./eventBus');
eventBus.on('order.created', (order) => {
console.log(`Paiement traité pour la commande ${order.id}`);
eventBus.emitEvent('payment.completed', { orderId: order.id, status: 'paid' });
});This handler subscribes to 'order.created' and emits a new 'payment.completed' event. It demonstrates event chaining without direct coupling.
Shipping Handler
const eventBus = require('./eventBus');
eventBus.on('payment.completed', (payment) => {
console.log(`Expédition lancée pour la commande ${payment.orderId}`);
});The final handler reacts only to the payment event. Adding new handlers requires no changes to existing services.
Main Application
const { createOrder } = require('./orderService');
require('./paymentHandler');
require('./shippingHandler');
const newOrder = { id: 'ORD-123', amount: 49.99 };
createOrder(newOrder);The main file imports the handlers (to register them) then triggers the flow. Run with node index.js to see the complete event flow.
Best Practices
- Name events descriptively (order.created, payment.completed)
- Centralize the event bus in a single module
- Avoid side effects in handlers
- Add clear logs on every event emission
- Test each handler in isolation
Common Mistakes to Avoid
- Forgetting to import handlers before emitting events
- Using overly generic event names
- Not handling errors in listeners
- Creating circular dependencies between handlers
Going Further
To explore advanced event-driven architectures and patterns, check out our Learni courses.