Phase 2 - Core Concepts | Post 3 | The ServiceBroker — The Heart of Moleculer

Post 3 of 15 | Phase 2: Core Concepts


The ServiceBroker — The Heart of Moleculer

In the previous post you created a project, ran it, and wrote your first service. You saw broker.start() and moleculer.config.js but we did not go deep into what the broker actually is and how it works. This post covers that completely.

Understanding the broker well is the most important thing in this entire course. Every other concept — services, actions, events, transporters — all sit on top of the broker. If you understand the broker, everything else becomes easy.


What is the ServiceBroker?

Think of a large company. The company has multiple departments — HR, Finance, Engineering, Sales. Each department does its own job. But there is a central management office that:

  • Knows which departments exist
  • Routes requests to the right department
  • Manages communication between departments
  • Handles failures when a department is unavailable
  • Keeps logs of everything happening

The ServiceBroker is exactly that central management office for your Moleculer application. Every service registers itself with the broker. Every action call goes through the broker. Every event passes through the broker. Nothing happens in Moleculer without the broker knowing about it.


Creating a Broker — Three Ways

Way 1: Default broker with no configuration


    const { ServiceBroker } = require("moleculer");

    const broker = new ServiceBroker();

This creates a broker with all default settings. Logger is enabled, no transporter, no caching. Fine for quick experiments.

Way 2: Inline configuration


    const { ServiceBroker } = require("moleculer");

    const broker = new ServiceBroker({
        nodeID: "my-node-1",
        logLevel: "info",
        transporter: null
    });

You pass a configuration object directly when creating the broker.

Way 3: Using moleculer.config.js (recommended for real projects)

This is what the CLI generates. The configuration lives in a separate file and moleculer-runner reads it automatically when you run npm start or npm run dev.


    // moleculer.config.js
    module.exports = {
        nodeID: "my-node-1",
        logLevel: "info",
        transporter: null
    };

In real projects you always use Way 3. Ways 1 and 2 are for quick scripts and learning purposes.


Understanding moleculer.config.js

Open the moleculer.config.js file in your my-project folder. It looks like this:


    "use strict";

    module.exports = {
        namespace: "",
        nodeID: null,

        metadata: {},

        logger: {
            type: "Console",
            options: {
                colors: true,
                moduleColors: false,
                formatter: "full",
                objectPrinter: null,
                autoPadding: false
            }
        },

        logLevel: "info",

        transporter: null,

        cacher: null,

        serializer: null,

        requestTimeout: 10 * 1000,

        retryPolicy: {
            enabled: false,
            retries: 5,
            delay: 100,
            maxDelay: 1000,
            factor: 2,
            check: err => err && !!err.retryable
        },

        maxCallLevel: 100,

        heartbeatInterval: 10,
        heartbeatTimeout: 30,

        contextParamsCloning: false,

        tracking: {
            enabled: false,
            shutdownTimeout: 5000,
        },

        disableBalancer: false,

        registry: {
            strategy: "RoundRobin",
            preferLocal: true
        },

        circuitBreaker: {
            enabled: false,
            threshold: 0.5,
            minRequestCount: 20,
            windowTime: 60,
            halfOpenTime: 10 * 1000,
            check: err => err && err.code >= 500
        },

        bulkhead: {
            enabled: false,
            concurrency: 10,
            maxQueueSize: 100,
        },

        validator: true,

        errorHandler: null,

        metrics: {
            enabled: false
        },

        tracing: {
            enabled: false
        },

        middlewares: [],

        replCommands: null,

        created(broker) { },
        started(broker) { },
        stopped(broker) { },
    };

This looks like a lot but you do not need to understand everything right now. Let us go through the important ones one by one.


Key Configuration Options Explained

nodeID


    nodeID: null

Every broker instance has a unique ID called nodeID. When set to null, Moleculer auto-generates one using your hostname and a random number, for example my-pc-3721.

The nodeID matters when you run multiple instances of your application on different machines. Each machine needs a unique nodeID so the broker knows which node is which. For local development, auto-generate is fine.

namespace


    namespace: ""

Think of namespace as a room name. If you have multiple Moleculer applications running on the same network, they can accidentally talk to each other. Setting a namespace isolates them.

For example:

// App 1
namespace: "ecommerce"

// App 2
namespace: "blog"

These two apps will never interfere with each other even if they share the same transporter.

logLevel


    logLevel: "info"

Controls how much the broker prints to the console. Available levels from most to least verbose:

  • trace — prints everything, very noisy
  • debug — prints debugging details
  • info — prints normal operational messages (recommended for development)
  • warn — prints only warnings and errors
  • error — prints only errors
  • fatal — prints only fatal errors
  • silent — prints nothing

During development use info. In production use warn or error.

requestTimeout


    requestTimeout: 10 * 1000

This is 10000 milliseconds, meaning 10 seconds. If you call an action and it does not respond within 10 seconds, Moleculer automatically throws a RequestTimeoutError. This prevents your application from hanging forever waiting for a response.

You can override this per call as well, which we will see later.

transporter


    transporter: null

When null, all services run in the same process and communicate directly in memory. No network involved. This is perfect for development.

When you want services on different machines to communicate, you set a transporter here, for example NATS or Redis. We cover this in Post 7.

validator


    validator: true

When true, Moleculer automatically validates action params using the fastest-validator library. This is what makes the params schema work in your service actions. Keep this true always.

retryPolicy


    retryPolicy: {
        // Enable feature
        enabled: false,
        // Count of retries
        retries: 5,
        // First delay in milliseconds.
        delay: 100,
        // Maximum delay in milliseconds.
        maxDelay: 1000,
        // Backoff factor for delay. 2 means exponential backoff.
        factor: 2,
        // A function to check failed requests.
        check: err => err && !!err.retryable
    }

When enabled, if an action call fails, Moleculer automatically retries it. retries is how many times to retry. delay is the initial wait in milliseconds between retries. factor means each retry waits longer — first retry waits 100ms, second waits 200ms, third waits 400ms, and so on. We cover this in depth in Post 8.

circuitBreaker


    circuitBreaker: {
        // Enable feature
        enabled: false,
        // Threshold value. 0.5 means that 50% should be failed for tripping.
        threshold: 0.5,
        // Minimum request count. Below it, CB does not trip.
        minRequestCount: 20,
        // Number of seconds for time window.
        windowTime: 60,
        // Number of milliseconds to switch from open to half-open state
        halfOpenTime: 10 * 1000,
        // A function to check failed requests.
        check: err => err && err.code >= 500
    }

The circuit breaker is a fault tolerance mechanism. If a service keeps failing, the circuit breaker stops sending requests to it temporarily and returns an error immediately instead of waiting for a timeout. We cover this in Post 8.

registry.strategy


    registry: {
        // Define balancing strategy. More info: https://moleculer.services/docs/0.15/balancing.html
        // Available values: "RoundRobin", "Random", "CpuUsage", "Latency", "Shard"
        strategy: "RoundRobin",
        // Enable local action call preferring. Always call the local action instance if available.
        preferLocal: true
    },

When you have multiple instances of the same service running, the broker uses a load balancing strategy to decide which instance handles each request. RoundRobin means requests are distributed evenly, one by one to each instance in turn. preferLocal means if a service is available on the same machine, prefer that over a remote one.


The Broker Lifecycle

The broker has a clear lifecycle with three stages. Understanding this is important when you need to do things like connect to a database when the app starts, or clean up resources when it stops.

Stage 1: created

This runs immediately when the broker object is created, before any services start. Use this for very early initialization.

Stage 2: started

This runs after all services have started and the broker is fully ready to handle requests. This is where you put code that needs the broker to be ready — like seeding initial data or connecting to external services.

Stage 3: stopped

This runs when the broker is shutting down. Use this for cleanup — closing database connections, flushing logs, etc.

In moleculer.config.js these are defined as:


    created(broker) {
        console.log("Broker created");
    },

    started(broker) {
        console.log("Broker started, ready to handle requests");
    },

    stopped(broker) {
        console.log("Broker stopped, cleaning up");
    }

Each service also has its own lifecycle hooks which we cover in Post 4.


The Broker Lifecycle in a Service

Services also participate in the broker lifecycle through their own hooks:


    module.exports = {
        name: "greeter",

        created() {
            // Runs when the service is created
            // broker is not yet started here
            this.logger.info("Greeter service created");
        },

        async started() {
            // Runs when the broker starts
            // Safe to call other services from here
            // Good place to connect to database
            this.logger.info("Greeter service started");
        },

        async stopped() {
            // Runs when the broker stops
            // Good place to close connections
            this.logger.info("Greeter service stopped");
        },

        actions: {
            hello(ctx) {
                return `Hello ${ctx.params.name}!`;
            }
        }
    };

Notice this.logger inside the service. Every service automatically gets a logger instance from the broker. You do not need to import or configure it. Just use this.logger.info(), this.logger.warn(), this.logger.error() anywhere inside a service.


Customizing the Logger

The default logger prints to the console. You can customize the format in moleculer.config.js:


    // Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.15/logging.html
    // Available logger types: "Console", "File", "Pino", "Winston", "Bunyan", "debug", "Log4js", "Datadog"
    logger: {
        type: "Console",
        options: {
            // Using colors on the output
            colors: true,
            // Print module names with different colors (like docker compose for containers)
            moduleColors: false,
            // Line formatter. It can be "json", "short", "simple", "full", a `Function` or a template string like "{timestamp} {level} {nodeID}/{mod}: {msg}"
            formatter: "full",
            // Custom object printer. If not defined, it uses the `util.inspect` method.
            objectPrinter: null,
            // Auto-padding the module name in order to messages begin at the same column.
            autoPadding: false
        }
    }

The formatter option controls the log line format. Options are:

  • full — shows timestamp, level, nodeID, service name, and message. Best for development.
  • short — shorter format, less information.
  • simple — minimal format, just level and message.
  • json — outputs logs as JSON objects. Best for production log aggregation tools.

For development, full with colors is the most readable.


Running Multiple Brokers Locally (Preview)

Right now everything runs in one process with one broker. In real microservices you run each service as a separate process, possibly on separate machines. Each process has its own broker. They communicate via a transporter.

Here is a preview of what that looks like. Do not run this yet, just read it to understand the concept:


    // Process 1: runs user-service
    const broker1 = new ServiceBroker({
        nodeID: "node-user",
        transporter: "NATS"
    });
    broker1.loadService("./services/user.service.js");
    broker1.start();


    // Process 2: runs order-service
    // This is a completely separate Node.js process
    const broker2 = new ServiceBroker({
        nodeID: "node-order",
        transporter: "NATS"
    });
    broker2.loadService("./services/order.service.js");
    broker2.start();

Both brokers connect through NATS. The order service can call user.getUser and the broker automatically routes it to Process 1 over the network. From the developer's perspective the call looks exactly the same as a local call.

This is the power of Moleculer. The location of a service is transparent to the caller.


Practical Exercise

Open your my-project folder and make these changes to moleculer.config.js to understand how config changes affect behavior.

Change 1: Change logLevel to debug and restart the server. Notice how much more information appears in the console. Change it back to info after.

Change 2: Change requestTimeout to 3 * 1000. This sets a 3 second timeout. Restart and call any action. It still works because actions respond in milliseconds.

Change 3: Add your own started hook:


    async started(broker) {
        broker.logger.info("==== Application is ready ====");
    }

Restart and look for your message in the console output.

After each change, restore the file to its original state before moving on.


Summary

  • The ServiceBroker is the central manager of your entire Moleculer application.
  • Every service registers with the broker. Every call and event passes through it.
  • moleculer.config.js is the recommended place to configure the broker in real projects.
  • nodeID identifies each broker instance. Namespace isolates multiple apps on the same network.
  • logLevel controls console output verbosity. Use info for development.
  • requestTimeout prevents your app from hanging when a service does not respond.
  • transporter: null means all services run in one process. Setting a transporter enables multi-node communication.
  • The broker has three lifecycle stages: created, started, stopped.
  • Services also have their own lifecycle hooks: created, started, stopped.
  • Every service gets this.logger automatically from the broker.

Up Next

Post 4 goes deep into Services and Actions — the building blocks you write every single day in Moleculer. We will cover service schema in full detail, action parameters, validation, shorthand vs full action syntax, calling actions with options, and service mixins.


Course Progress: 3 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...