Phase 2 - Core Concepts | Post 5 | Events — Fire and Forget Communication Between Services

Post 5 of 15 | Phase 2: Core Concepts


Events — Fire and Forget Communication Between Services

In the previous post you learned about Actions, which follow a request-reply pattern. You call an action, you wait, you get a result back. This is perfect for most situations.

But sometimes you do not want to wait for a response. Sometimes you just want to say "this thing happened" and let whoever cares react to it. That is exactly what Events are for.


The Real World Analogy

Think about what happens when you place an order on an e-commerce website.

The moment you click "Place Order", the website confirms your order immediately. But behind the scenes, many things need to happen:

  • The inventory system needs to reduce stock
  • The email system needs to send a confirmation email
  • The notification system needs to send a push notification
  • The analytics system needs to record the purchase

Should the order service wait for all of these to finish before telling you your order is placed? No. That would be slow and wrong. The order is placed. Everything else is a reaction to that fact.

This is the event pattern. The order service emits one event called order.created. Every other service that cares listens to that event and reacts independently. The order service does not know or care who is listening.


Actions vs Events — When to Use Which

Before writing any code, understand this distinction clearly.

Use an Action when:

  • You need a result back from the other service
  • The operation must complete before you continue
  • Example: Get user details, create a record, check stock availability

Use an Event when:

  • You do not need a response back
  • Multiple services might react to the same thing
  • The operation can happen asynchronously in the background
  • Example: Send email after registration, update analytics after purchase, notify after payment

Emitting Events

Inside any action or method, you emit an event using broker.emit() or ctx.emit().

// From outside a service (using broker directly)
broker.emit("order.created", { orderId: "123", userId: "456", total: 999 });

// From inside a service action (preferred)
actions: {
    async create(ctx) {
        const order = {
            id: "123",
            userId: ctx.params.userId,
            total: ctx.params.total
        };

        // Save order to database here

        // Emit the event — fire and forget
        // We do not await this. We do not care about the response.
        ctx.emit("order.created", order);

        // Return immediately without waiting for listeners
        return order;
    }
}

Notice there is no await before ctx.emit(). Events are fire and forget. Your code continues immediately after emitting.


Listening to Events

Any service can listen to any event using the events property in its service schema.

// email.service.js
module.exports = {
    name: "email",

    events: {
        // The key is the event name you are listening to
        "order.created"(ctx) {
            // ctx.params contains the data that was emitted
            const order = ctx.params;
            this.logger.info(`Sending confirmation email for order ${order.id}`);
            // Send email logic here
        }
    }
};
// inventory.service.js
module.exports = {
    name: "inventory",

    events: {
        "order.created"(ctx) {
            const order = ctx.params;
            this.logger.info(`Reducing stock for order ${order.id}`);
            // Reduce stock logic here
        }
    }
};
// analytics.service.js
module.exports = {
    name: "analytics",

    events: {
        "order.created"(ctx) {
            const order = ctx.params;
            this.logger.info(`Recording purchase of ${order.total} for analytics`);
            // Record analytics logic here
        }
    }
};

All three services listen to the same event. When order service emits order.created, all three handlers run. The order service knows nothing about any of them.


Full Event Handler Syntax

Just like actions, events have a shorthand and a full syntax.

Shorthand:

events: {
    "order.created"(ctx) {
        // handle
    }
}

Full syntax:

events: {
    "order.created": {
        handler(ctx) {
            // handle
        }
    }
}

Always use the full syntax in real projects because it supports additional options we will cover next.


Two Types of Events

Moleculer has two different ways to emit events. Understanding the difference is important.

Type 1: Balanced Event using ctx.emit()

When you use ctx.emit() or broker.emit(), the event is balanced. This means if you have multiple instances of the same service running, only one instance receives the event. The broker distributes events across instances in a round-robin fashion.

This is what you want in most cases. If you have three instances of email.service running, you only want one of them to send the confirmation email, not all three.

// Only ONE instance of email service receives this
ctx.emit("order.created", { orderId: "123" });

Type 2: Broadcast Event using ctx.broadcast()

When you use ctx.broadcast() or broker.broadcast(), the event is sent to ALL instances of ALL services that are listening. Every single listener receives it regardless of how many instances exist.

// ALL instances of ALL listening services receive this
ctx.broadcast("config.updated", { newConfig: {} });

Use broadcast when you need every instance to know about something. A common use case is a configuration change that every instance needs to reload, or a cache clear that every instance needs to perform.


Practical Example — Complete Flow

Let us build a realistic example. Create these three files in your services folder.

services/order.service.js

"use strict";

const orders = [];

module.exports = {
    name: "order",

    actions: {
        create: {
            rest: {
                method: "POST",
                path: "/"
            },
            params: {
                userId: "string",
                product: "string",
                total: "number"
            },
            async handler(ctx) {
                // Create the order
                const order = {
                    id: String(Date.now()),
                    userId: ctx.params.userId,
                    product: ctx.params.product,
                    total: ctx.params.total,
                    status: "confirmed",
                    createdAt: new Date()
                };

                orders.push(order);

                // Emit event — do not await
                // order service does not care who handles this or when
                ctx.emit("order.created", order);

                this.logger.info(`Order ${order.id} created and event emitted`);

                // Return immediately
                return order;
            }
        },

        list: {
            rest: {
                method: "GET",
                path: "/"
            },
            handler(ctx) {
                return orders;
            }
        }
    }
};

services/email.service.js

"use strict";

module.exports = {
    name: "email",

    events: {
        "order.created": {
            handler(ctx) {
                const order = ctx.params;

                // In a real project you would use nodemailer or sendgrid here
                this.logger.info("-------------------------------");
                this.logger.info("EMAIL SERVICE: New email triggered");
                this.logger.info(`To: User ${order.userId}`);
                this.logger.info(`Subject: Order Confirmation - ${order.id}`);
                this.logger.info(`Your order for ${order.product} worth ${order.total} is confirmed`);
                this.logger.info("-------------------------------");
            }
        }
    }
};

services/notification.service.js

"use strict";

module.exports = {
    name: "notification",

    events: {
        "order.created": {
            handler(ctx) {
                const order = ctx.params;

                this.logger.info("-------------------------------");
                this.logger.info("NOTIFICATION SERVICE: Push notification triggered");
                this.logger.info(`Sending push to user ${order.userId}`);
                this.logger.info(`Your order ${order.id} has been placed successfully`);
                this.logger.info("-------------------------------");
            }
        }
    }
};

Now run npm run dev and make a POST request to create an order:

POST http://localhost:3000/api/order

Body:

{
    "userId": "user-1",
    "product": "Mechanical Keyboard",
    "total": 2999
}

In your terminal you will see all three services reacting:

[INFO]  order: Order 1715234567890 created and event emitted
[INFO]  email: EMAIL SERVICE: New email triggered
[INFO]  email: To: User user-1
[INFO]  email: Subject: Order Confirmation - 1715234567890
[INFO]  notification: NOTIFICATION SERVICE: Push notification triggered
[INFO]  notification: Sending push to user user-1

The order action returned instantly. The email and notification services reacted in the background. This is the power of event-driven architecture.


Event Naming Conventions

Use dot notation for event names. The convention is:

entity.action

Good event names:

  • order.created
  • order.cancelled
  • user.registered
  • user.passwordChanged
  • payment.completed
  • payment.failed

Bad event names:

  • orderCreated (no dot notation)
  • ORDER_CREATED (wrong convention in Moleculer)
  • order (too vague)

Wildcard Event Listeners

You can listen to multiple events using wildcards.

events: {
    // Listen to ALL order events
    "order.*"(ctx) {
        this.logger.info(`An order event occurred: ${ctx.eventName}`);
        this.logger.info("Data:", ctx.params);
    },

    // Listen to ALL events from any service
    "**"(ctx) {
        this.logger.info(`Any event: ${ctx.eventName}`);
    }
}

ctx.eventName inside the handler tells you the exact event name that triggered this handler. This is useful for a logging or audit service that wants to record every event that happens in the system.


Getting the Event Name Inside the Handler

events: {
    "order.*": {
        handler(ctx) {
            // ctx.eventName is the actual event name
            // For example "order.created" or "order.cancelled"
            this.logger.info(`Received event: ${ctx.eventName}`);
            this.logger.info(`From node: ${ctx.nodeID}`);
            this.logger.info(`Data:`, ctx.params);
        }
    }
}

Local Events

Sometimes you want to emit an event that only services in the same process can hear. Remote nodes using a transporter should not receive it. Use ctx.emit with the local option or broker.emitLocal():

// Only services in THIS process receive this event
broker.emitLocal("internal.cache.clear", { key: "user-list" });

This is useful for internal coordination within a single node without broadcasting to the entire network.


Async Event Handlers

Event handlers can be async. This is fine and common when the handler needs to do database operations.

events: {
    "order.created": {
        async handler(ctx) {
            const order = ctx.params;

            // Async operation — database write, API call, etc.
            await this.saveToAnalyticsDB(order);

            this.logger.info(`Analytics saved for order ${order.id}`);
        }
    }
},

methods: {
    async saveToAnalyticsDB(order) {
        // Database logic here
    }
}

One thing to know: if your async event handler throws an error, it does not affect the emitter. The order service already returned its response before this runs. The error will be logged but it will not propagate back to the caller. This is by design — events are decoupled.


Summary of emit vs broadcast

broker.emit()        — balanced, one instance receives it
broker.broadcast()   — all instances receive it
broker.emitLocal()   — only local process receives it

ctx.emit()           — same as broker.emit(), use inside actions
ctx.broadcast()      — same as broker.broadcast(), use inside actions

Complete Picture — Actions vs Events Side by Side

// ACTION — I need an answer back
const user = await ctx.call("user.getById", { id: "123" });
// I wait here until user service responds
console.log(user.name);

// EVENT — I am announcing something happened
ctx.emit("user.registered", { id: "123", email: "rahul@example.com" });
// I do not wait. I continue immediately.
// Whoever cares will handle it in their own time.

Summary

  • Events are fire-and-forget. You emit and move on. No waiting for a response.
  • Use actions when you need a result. Use events when you are announcing something happened.
  • ctx.emit() sends a balanced event — one instance per service receives it.
  • ctx.broadcast() sends to all instances of all services.
  • broker.emitLocal() sends only within the current process.
  • Multiple services can listen to the same event independently.
  • Event names follow dot notation convention — entity.action.
  • Wildcard listeners use asterisk — order.* or ** for everything.
  • ctx.eventName inside the handler gives you the exact event name.
  • Async event handlers are fine. Errors in them do not affect the emitter.
  • The order service does not know or care who is listening. That is the point.

Up Next

Post 6 covers the Context object in depth. You have been using ctx everywhere — ctx.params, ctx.meta, ctx.call, ctx.emit — but we have not looked at the full picture. The Context object carries far more information than you have seen so far, and understanding it completely will make you a much stronger Moleculer developer.


Course Progress: 5 of 15 posts complete.

No comments:

Post a Comment

Phase 4 - Fault Tolerance | Post 8 | Fault Tolerance — Keeping Your App Alive When Things Break

Post 8 of 15 | Phase 4: Fault Tolerance Fault Tolerance — Keeping Your App Alive When Things Break In every post so far we have assumed that...