Phase 8 - Observability | Post 12 | Logging, Metrics, and Tracing — Understanding What Is Happening Inside Your App

Post 12 of 15 | Phase 8: Observability


Logging, Metrics, and Tracing — Understanding What Is Happening Inside Your App

You have built a working microservices backend. Services are communicating, data is persisting to MongoDB, authentication is working. Now comes the question that every developer faces in production: when something goes wrong, how do you find out what happened and where?

In a monolith you add a console.log and check one log file. In microservices you have ten services running, each producing its own logs, handling hundreds of requests per second. You need proper tools to make sense of it all.

Observability has three pillars:

  • Logging — what happened and when
  • Metrics — how is the system performing right now
  • Tracing — how did a specific request travel through the system

Moleculer has built-in support for all three.


Pillar 1: Logging

You have been using this.logger throughout the course. Let us now understand logging properly and set it up for real use.

Log Levels

Moleculer has six log levels in order from most to least verbose:

trace   — extremely detailed, every internal step
debug   — debugging information, variable values
info    — normal operational messages, service started, action called
warn    — something unexpected happened but the app is still running
error   — something failed, needs attention
fatal   — critical failure, app may not recover

In development use info or debug. In production use warn or error to reduce noise and storage cost.

Configuring the Logger in moleculer.config.js

module.exports = {
    logger: {
        type: "Console",
        options: {
            // Show colors in terminal output
            colors: true,

            // Different colors for different services
            moduleColors: true,

            // full shows timestamp, level, nodeID, service name, message
            // short shows less information
            // simple shows just level and message
            // json outputs machine-readable JSON — use in production
            formatter: "full",

            // Automatically pad service names so logs align in columns
            autoPadding: true
        }
    },

    logLevel: "info"
};

Using the Logger in Services

module.exports = {
    name: "order",

    actions: {
        async create(ctx) {
            // Different log levels for different situations
            this.logger.debug("create action called", ctx.params);
            this.logger.info(`Creating order for user ${ctx.meta.user.id}`);

            try {
                const order = await this.adapter.insert(ctx.params);
                this.logger.info(`Order created successfully: ${order._id}`);
                return order;
            } catch (err) {
                this.logger.error("Failed to create order", err);
                throw err;
            }
        }
    }
};

Multiple Loggers at Once

In production you often want logs to go to both the console and a file or external service. Moleculer supports multiple loggers simultaneously:

module.exports = {
    logger: [
        // Console logger for development visibility
        {
            type: "Console",
            options: {
                formatter: "short",
                colors: true
            }
        },

        // File logger — writes to a file
        {
            type: "File",
            options: {
                // Log file location
                filename: "logs/moleculer-{date}.log",

                // New file every day
                eol: "\n",

                // Format as JSON for log parsing tools
                formatter: "json"
            }
        }
    ],

    logLevel: {
        // Different levels for different services
        // "*" is the default for all services
        "*": "warn",

        // Show more detail for the order service
        "ORDER": "debug",

        // Show all logs from the broker itself
        "BROKER": "info"
    }
};

Structured Logging — Logging Objects Not Strings

In production, logs are parsed by tools like Datadog, Elasticsearch, or CloudWatch. These tools work best with structured JSON logs. Always log objects alongside messages instead of concatenating strings:

// Bad — hard to parse programmatically
this.logger.info("Order " + orderId + " created for user " + userId + " with total " + total);

// Good — structured, easy to parse and filter
this.logger.info("Order created", {
    orderId,
    userId,
    total,
    productId,
    timestamp: new Date().toISOString()
});

Pillar 2: Metrics

Metrics tell you how your system is performing right now. Things like:

  • How many requests per second is each service handling
  • What is the average response time of each action
  • How many errors are occurring
  • How much memory is being used
  • How many active connections exist

Moleculer has a built-in metrics system that can expose these numbers to external monitoring tools like Prometheus and Grafana.

Enabling Metrics in moleculer.config.js

module.exports = {
    metrics: {
        enabled: true,

        // Where to send metrics
        reporter: [
            // Console reporter — prints metrics to terminal periodically
            {
                type: "Console",
                options: {
                    // Print metrics every 10 seconds
                    interval: 10
                }
            },

            // Prometheus reporter — exposes metrics at an HTTP endpoint
            // Grafana reads from Prometheus to create dashboards
            {
                type: "Prometheus",
                options: {
                    // Metrics available at http://localhost:3030/metrics
                    port: 3030,
                    path: "/metrics",

                    // Default labels added to every metric
                    defaultLabels: (registry) => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    })
                }
            }
        ]
    }
};

Install the Prometheus reporter:

npm install @moleculer/lab

Actually for Prometheus you need:

npm install moleculer-prometheus

When Prometheus metrics are enabled, visit http://localhost:3030/metrics and you will see output like this:

# HELP moleculer_request_total Total count of requests
# TYPE moleculer_request_total counter
moleculer_request_total{action="user.login",nodeID="node-1"} 42

# HELP moleculer_request_time_milliseconds Request time in milliseconds
# TYPE moleculer_request_time_milliseconds histogram
moleculer_request_time_milliseconds_bucket{action="user.login",le="10"} 38
moleculer_request_time_milliseconds_bucket{action="user.login",le="100"} 42

# HELP moleculer_request_error_total Total count of request errors
# TYPE moleculer_request_error_total counter
moleculer_request_error_total{action="user.login",nodeID="node-1"} 2

Prometheus scrapes this endpoint periodically and stores the data. Grafana connects to Prometheus and displays it as beautiful dashboards.

Built-in Metrics Moleculer Tracks Automatically

When metrics are enabled, Moleculer automatically tracks these without any code from you:

moleculer.request.total           — total requests per action
moleculer.request.active          — currently processing requests
moleculer.request.error.total     — total errors per action
moleculer.request.time            — response time histogram per action
moleculer.event.total             — total events emitted
moleculer.transporter.sent.total  — messages sent via transporter
moleculer.process.memory.heap     — Node.js heap memory usage
moleculer.process.cpu             — CPU usage percentage
moleculer.os.memory.free          — free system memory

All of this is tracked automatically. Zero code needed.

Custom Metrics

You can also track your own business metrics:

module.exports = {
    name: "order",

    async started() {
        // Register a custom counter metric
        this.broker.metrics.register({
            type: "counter",
            name: "order.created.total",
            labelNames: ["status"],
            description: "Total orders created"
        });

        // Register a custom gauge metric
        this.broker.metrics.register({
            type: "gauge",
            name: "order.revenue.total",
            description: "Total revenue from orders"
        });
    },

    actions: {
        create: {
            async handler(ctx) {
                const order = await this.adapter.insert(ctx.params);

                // Increment your custom counter
                this.broker.metrics.increment("order.created.total", {
                    status: "confirmed"
                });

                // Update your custom gauge
                this.broker.metrics.increment(
                    "order.revenue.total",
                    null,
                    order.totalPrice
                );

                return order;
            }
        }
    }
};

Now your Prometheus dashboard shows business metrics alongside system metrics. You can see total orders created and total revenue in real time.


Pillar 3: Tracing

This is the most powerful observability tool for microservices. Tracing shows you the complete journey of a single request as it travels through multiple services.

Without tracing, when a request is slow or fails, you know something went wrong but not where. With tracing you can see:

  • request started at API Gateway at 10:00:00.000
  • API Gateway called order.create at 10:00:00.005 — took 250ms
  • order.create called user.get at 10:00:00.010 — took 50ms
  • order.create called product.get at 10:00:00.060 — took 30ms
  • order.create called product.reduceStock at 10:00:00.090 — took 80ms
  • order.create completed at 10:00:00.255
  • API Gateway responded at 10:00:00.260

You can see exactly where time was spent. If product.get took 5 seconds suddenly, you know immediately where to look.

Enabling Tracing in moleculer.config.js

module.exports = {
    tracing: {
        enabled: true,

        exporter: [
            // Console exporter — prints trace spans to terminal
            // Good for development
            {
                type: "Console",
                options: {
                    // Only log spans that take more than 5ms
                    // Reduces noise from fast operations
                    logger: null,
                    colors: true,
                    width: 100,
                    gaugeWidth: 40
                }
            },

            // Jaeger exporter — sends traces to Jaeger UI
            // Best tool for visualizing traces in development
            {
                type: "Jaeger",
                options: {
                    // Jaeger collector endpoint
                    endpoint: null,
                    host: "localhost",
                    port: 6832,

                    // Send all spans (1.0 = 100%, 0.1 = 10%)
                    sampler: {
                        type: "Const",
                        options: { decision: 1 }
                    }
                }
            }
        ]
    }
};

Setting Up Jaeger Locally

Jaeger is the most popular distributed tracing tool. Run it with Docker (we cover Docker fully in Post 14, but use this command now if you have Docker):

docker run -d --name jaeger \
  -p 6831:6831/udp \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

Then open http://localhost:16686 in your browser. This is the Jaeger UI where you can visualize traces.

If you do not have Docker yet, use the Console exporter for now and come back to Jaeger in Post 14.

Console Tracing Output

With the Console exporter enabled, when you make a request you will see output like this in your terminal:

=== REQUEST TRACE ===
api.rest                          ████████████████████████ 255ms
  order.create                      ██████████████████ 245ms
    user.get                          ████ 50ms
    product.get                       ███ 30ms
    product.reduceStock               ██████ 80ms

This visual representation shows you the full call chain and how long each step took.

Adding Custom Trace Spans

When you have a complex operation inside a single action, you can break it into named spans to see exactly where time is spent:

module.exports = {
    name: "order",

    actions: {
        create: {
            async handler(ctx) {
                // Start a custom span for the validation step
                const validationSpan = this.broker.tracer.startSpan(
                    "Validate order data",
                    { parentSpan: ctx.span }
                );

                // Do validation work
                await this.validateOrderData(ctx.params);

                // End the validation span
                validationSpan.finish();

                // Start a span for the database insert
                const dbSpan = this.broker.tracer.startSpan(
                    "Save order to database",
                    { parentSpan: ctx.span }
                );

                const order = await this.adapter.insert(ctx.params);

                dbSpan.finish();

                return order;
            }
        }
    }
};

Now in Jaeger you will see the order.create span broken into sub-spans showing exactly how long validation took versus database insertion.

Adding Tags to Spans

Tags let you attach metadata to a span so you can filter and search traces:

actions: {
    create: {
        async handler(ctx) {
            // Add tags to the current span
            ctx.span.setTag("userId", ctx.meta.user.id);
            ctx.span.setTag("productId", ctx.params.productId);
            ctx.span.setTag("quantity", ctx.params.quantity);

            const order = await this.adapter.insert(ctx.params);

            ctx.span.setTag("orderId", order._id.toString());

            return order;
        }
    }
}

In Jaeger you can search for traces by tag. Find all orders for a specific user, or all orders for a specific product, by searching the tag values.


Complete Observability Configuration

Here is a complete moleculer.config.js with all three pillars enabled for production:

"use strict";

require("dotenv").config();

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

    // Logging
    logger: [
        {
            type: "Console",
            options: {
                formatter: "full",
                colors: true,
                moduleColors: true,
                autoPadding: true
            }
        },
        {
            type: "File",
            options: {
                filename: "logs/moleculer-{date}.log",
                formatter: "json"
            }
        }
    ],

    logLevel: {
        "*": "warn",
        "BROKER": "info",
        "REGISTRY": "info"
    },

    // Metrics
    metrics: {
        enabled: true,
        reporter: [
            {
                type: "Prometheus",
                options: {
                    port: 3030,
                    path: "/metrics",
                    defaultLabels: (registry) => ({
                        namespace: registry.broker.namespace,
                        nodeID: registry.broker.nodeID
                    })
                }
            }
        ]
    },

    // Tracing
    tracing: {
        enabled: true,
        exporter: [
            {
                type: "Console",
                options: {
                    colors: true,
                    width: 100,
                    gaugeWidth: 40
                }
            },
            {
                type: "Jaeger",
                options: {
                    host: process.env.JAEGER_HOST || "localhost",
                    port: 6832,
                    sampler: {
                        type: "Const",
                        options: { decision: 1 }
                    }
                }
            }
        ]
    },

    // Fault tolerance
    requestTimeout: 10 * 1000,

    retryPolicy: {
        enabled: true,
        retries: 3,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    },

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

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

    // Caching
    cacher: {
        type: "Memory",
        options: {
            ttl: 30
        }
    },

    transporter: null
};

What to Do When Something Goes Wrong in Production

Here is a practical debugging workflow using all three pillars together:

Step 1: Check metrics first. Look at your Grafana dashboard. Which action has a spike in error rate or response time? Suppose you see order.create response time jumped from 50ms to 5000ms.

Step 2: Open Jaeger and filter traces for order.create in the last 30 minutes. Sort by duration. Open the slowest trace.

Step 3: The trace shows order.create called product.get and that took 4900ms. Everything else was normal.

Step 4: Check your logs filtered by the requestID from the slow trace. You see a MongoDB query timeout in the product service.

Step 5: Fix the MongoDB index on the product collection. Response time drops back to normal.

In five steps you went from knowing something is wrong to knowing exactly what and why. Without observability this could take hours.


Summary

  • Observability has three pillars: Logging, Metrics, Tracing.
  • Logging records what happened. Use structured JSON logging in production. Use appropriate log levels.
  • Multiple loggers can run simultaneously — console for development, file for production.
  • logLevel can be set per service for fine-grained control.
  • Metrics track system performance numbers. Moleculer tracks request counts, response times, errors, and memory automatically.
  • Prometheus reporter exposes metrics at an HTTP endpoint. Grafana reads Prometheus to create dashboards.
  • Custom metrics let you track business numbers like total orders and total revenue.
  • Tracing records the complete journey of each request through all services.
  • Each hop in the call chain creates a span. Spans are organized into a trace tree.
  • Jaeger is the best tool for visualizing traces locally.
  • Use ctx.span.setTag() to attach searchable metadata to traces.
  • The debugging workflow: metrics to find the problem, tracing to locate it, logging to understand it.

Up Next

Post 13 covers Docker and Docker Compose — packaging each service into a container and running the entire application with one command. This is the final step before production deployment. We will containerize all our services, add MongoDB and Redis containers, and wire everything together with a transporter.


Course Progress: 12 of 15 posts complete.

Phase 7 - Database Integration | Post 11 | Database Integration — moleculer-db with MongoDB and Mongoose

Post 11 of 15 | Phase 7: Database Integration


Database Integration — moleculer-db with MongoDB and Mongoose

In every post so far your services have been using in-memory arrays to store data. Those arrays reset every time you restart the server. In this post we replace them with a real MongoDB database using moleculer-db, Moleculer's official database integration package.

moleculer-db is one of the most useful packages in the Moleculer ecosystem. It gives you free CRUD actions out of the box. You define your model and moleculer-db automatically creates list, find, get, create, update, and remove actions for you without writing a single line of handler code.


What is moleculer-db

moleculer-db is a service mixin, just like moleculer-web. You mix it into your service and it adds database capabilities. It supports multiple database adapters:

  • Memory adapter — built-in, good for testing
  • MongoDB adapter using Mongoose — most common in Node.js projects
  • MongoDB adapter using native driver
  • Sequelize adapter — for SQL databases

In this post we use the Mongoose adapter because you already know Mongoose from your MERN experience.


Installing Required Packages

npm install moleculer-db moleculer-db-adapter-mongoose mongoose

Make sure MongoDB is running on your machine. If you have MongoDB installed locally:

mongod

Or if you use MongoDB Atlas, get your connection string ready.


How moleculer-db Works — The Big Picture

Without moleculer-db, a typical service action looks like this:

// You write all of this yourself
actions: {
    list: {
        async handler(ctx) {
            const users = await User.find({});
            return users;
        }
    },
    getById: {
        async handler(ctx) {
            const user = await User.findById(ctx.params.id);
            return user;
        }
    },
    create: {
        async handler(ctx) {
            const user = new User(ctx.params);
            await user.save();
            return user;
        }
    }
    // ... and so on for update, delete
}

With moleculer-db, you write this instead:

// moleculer-db gives you all CRUD actions for free
mixins: [DbMixin],
model: UserModel,
// That is it. list, get, create, update, remove — all done.

moleculer-db reads your model and generates all standard database operations automatically.


Built-in Actions Provided by moleculer-db

When you mix moleculer-db into a service, you automatically get these actions:

serviceName.find       — Find records with filtering and sorting
serviceName.count      — Count matching records
serviceName.list       — Paginated list with total count
serviceName.create     — Create one record
serviceName.insert     — Insert one or many records
serviceName.get        — Get one record by ID
serviceName.update     — Update one record by ID
serviceName.remove     — Delete one record by ID

All of these are available immediately. You do not write any handler code for them.


Building the User Service With moleculer-db

Create a models folder in your project root and create your first Mongoose model.

models/user.model.js

"use strict";

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: true,
            trim: true
        },
        email: {
            type: String,
            required: true,
            unique: true,
            lowercase: true,
            trim: true
        },
        password: {
            type: String,
            required: true,
            minlength: 6
        },
        role: {
            type: String,
            enum: ["admin", "user"],
            default: "user"
        },
        isActive: {
            type: Boolean,
            default: true
        }
    },
    {
        timestamps: true  // Automatically adds createdAt and updatedAt
    }
);

module.exports = mongoose.model("User", UserSchema);

Now rewrite the user service to use moleculer-db:

services/user.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const UserModel = require("../models/user.model");

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";

module.exports = {
    name: "user",

    // Mix in the database mixin
    mixins: [DbMixin],

    // Tell moleculer-db which adapter and model to use
    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    // The Mongoose model
    model: UserModel,

    // Customize built-in actions
    // You can disable, override, or add options to the free actions
    settings: {
        // Fields to return in responses
        // Fields not in this list are hidden from API responses
        fields: ["_id", "name", "email", "role", "isActive", "createdAt"],

        // Fields that are NOT allowed in create/update calls from outside
        // Password handling is done manually in custom actions
        entityValidator: {
            name: { type: "string", min: 2 },
            email: "email",
            password: { type: "string", min: 6 },
            role: { type: "string", optional: true }
        }
    },

    actions: {
        // Override the built-in create action to hash password
        create: {
            rest: { method: "POST", path: "/" },
            params: {
                name: { type: "string", min: 2 },
                email: "email",
                password: { type: "string", min: 6 },
                role: { type: "string", optional: true }
            },
            async handler(ctx) {
                // Check if email already exists
                const exists = await this.adapter.findOne({
                    email: ctx.params.email
                });

                if (exists) {
                    throw new MoleculerClientError(
                        "Email already registered",
                        422,
                        "EMAIL_EXISTS"
                    );
                }

                // Hash the password before saving
                const hashedPassword = await bcrypt.hash(ctx.params.password, 10);

                // Use this._create() — the internal moleculer-db create method
                // This is different from ctx.call("user.create") which is the public action
                const user = await this.adapter.insert({
                    name: ctx.params.name,
                    email: ctx.params.email,
                    password: hashedPassword,
                    role: ctx.params.role || "user"
                });

                // Return user without password
                return this.sanitizeUser(user);
            }
        },

        // Custom login action — not provided by moleculer-db
        login: {
            rest: { method: "POST", path: "/login" },
            params: {
                email: "email",
                password: "string"
            },
            async handler(ctx) {
                // Find user by email
                const user = await this.adapter.findOne({
                    email: ctx.params.email
                });

                if (!user) {
                    throw new MoleculerClientError(
                        "Invalid email or password",
                        401,
                        "INVALID_CREDENTIALS"
                    );
                }

                // Compare passwords
                const isMatch = await bcrypt.compare(
                    ctx.params.password,
                    user.password
                );

                if (!isMatch) {
                    throw new MoleculerClientError(
                        "Invalid email or password",
                        401,
                        "INVALID_CREDENTIALS"
                    );
                }

                // Generate JWT
                const token = jwt.sign(
                    {
                        id: user._id.toString(),
                        email: user.email,
                        role: user.role
                    },
                    JWT_SECRET,
                    { expiresIn: "24h" }
                );

                // Put token in meta so API Gateway can send it as header
                ctx.meta.token = token;

                return {
                    message: "Login successful",
                    user: this.sanitizeUser(user)
                };
            }
        },

        // Get the currently authenticated user's profile
        me: {
            rest: { method: "GET", path: "/me" },
            async handler(ctx) {
                // ctx.meta.user is set by the API Gateway authenticate hook
                const userId = ctx.meta.user.id;

                const user = await this.adapter.findById(userId);

                if (!user) {
                    throw new MoleculerClientError(
                        "User not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                return this.sanitizeUser(user);
            }
        },

        // Override built-in list to add REST mapping
        list: {
            rest: { method: "GET", path: "/" }
        },

        // Override built-in get to add REST mapping
        get: {
            rest: { method: "GET", path: "/:id" }
        },

        // Override built-in update to add REST mapping
        update: {
            rest: { method: "PUT", path: "/:id" }
        },

        // Override built-in remove to add REST mapping
        remove: {
            rest: { method: "DELETE", path: "/:id" }
        }
    },

    methods: {
        // Private helper — removes password from user object before returning
        sanitizeUser(user) {
            const sanitized = user.toObject ? user.toObject() : { ...user };
            delete sanitized.password;
            delete sanitized.__v;
            return sanitized;
        }
    },

    async started() {
        this.logger.info("User service connected to MongoDB");
    }
};

Install bcryptjs for password hashing:

npm install bcryptjs

Building the Product Service With moleculer-db

models/product.model.js

"use strict";

const mongoose = require("mongoose");

const ProductSchema = new mongoose.Schema(
    {
        name: {
            type: String,
            required: true,
            trim: true
        },
        description: {
            type: String,
            default: ""
        },
        price: {
            type: Number,
            required: true,
            min: 0
        },
        stock: {
            type: Number,
            required: true,
            min: 0,
            default: 0
        },
        category: {
            type: String,
            default: "general"
        },
        isActive: {
            type: Boolean,
            default: true
        }
    },
    {
        timestamps: true
    }
);

module.exports = mongoose.model("Product", ProductSchema);

services/product.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const ProductModel = require("../models/product.model");

module.exports = {
    name: "product",

    mixins: [DbMixin],

    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    model: ProductModel,

    settings: {
        fields: ["_id", "name", "description", "price", "stock", "category", "isActive", "createdAt"],

        populates: {}
    },

    actions: {
        // Map REST routes to built-in actions
        list: {
            rest: { method: "GET", path: "/" }
        },
        get: {
            rest: { method: "GET", path: "/:id" }
        },
        create: {
            rest: { method: "POST", path: "/" }
        },
        update: {
            rest: { method: "PUT", path: "/:id" }
        },
        remove: {
            rest: { method: "DELETE", path: "/:id" }
        },

        // Custom action — reduce stock when order is placed
        reduceStock: {
            params: {
                id: "string",
                quantity: "number"
            },
            async handler(ctx) {
                const product = await this.adapter.findById(ctx.params.id);

                if (!product) {
                    throw new MoleculerClientError(
                        "Product not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                if (product.stock < ctx.params.quantity) {
                    throw new MoleculerClientError(
                        "Insufficient stock",
                        422,
                        "INSUFFICIENT_STOCK",
                        { available: product.stock, requested: ctx.params.quantity }
                    );
                }

                // Update stock
                const updated = await this.adapter.updateById(ctx.params.id, {
                    $inc: { stock: -ctx.params.quantity }
                });

                // Clear cache
                await this.broker.broadcast("cache.clean.product");

                return updated;
            }
        }
    },

    events: {
        // When an order is created, reduce the product stock
        async "order.created"(ctx) {
            const order = ctx.params;
            this.logger.info(`Reducing stock for product ${order.productId}`);

            await ctx.call("product.reduceStock", {
                id: order.productId,
                quantity: order.quantity
            });
        },

        "cache.clean.product"() {
            if (this.broker.cacher) {
                this.broker.cacher.clean("product.**");
            }
        }
    }
};

Building the Order Service With moleculer-db

models/order.model.js

"use strict";

const mongoose = require("mongoose");

const OrderSchema = new mongoose.Schema(
    {
        userId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "User",
            required: true
        },
        productId: {
            type: mongoose.Schema.Types.ObjectId,
            ref: "Product",
            required: true
        },
        quantity: {
            type: Number,
            required: true,
            min: 1
        },
        totalPrice: {
            type: Number,
            required: true
        },
        status: {
            type: String,
            enum: ["pending", "confirmed", "shipped", "delivered", "cancelled"],
            default: "pending"
        }
    },
    {
        timestamps: true
    }
);

module.exports = mongoose.model("Order", OrderSchema);

services/order.service.js

"use strict";

const { MoleculerClientError } = require("moleculer").Errors;
const DbMixin = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const OrderModel = require("../models/order.model");

module.exports = {
    name: "order",

    mixins: [DbMixin],

    adapter: new MongooseAdapter(
        process.env.MONGO_URI || "mongodb://localhost:27017/moleculer-course"
    ),

    model: OrderModel,

    settings: {
        fields: ["_id", "userId", "productId", "quantity", "totalPrice", "status", "createdAt"]
    },

    actions: {
        list: {
            rest: { method: "GET", path: "/" }
        },
        get: {
            rest: { method: "GET", path: "/:id" }
        },

        // Override create to add business logic
        create: {
            rest: { method: "POST", path: "/" },
            params: {
                productId: "string",
                quantity: { type: "number", min: 1 }
            },
            async handler(ctx) {
                const userId = ctx.meta.user.id;

                // Get the product to check stock and get price
                const product = await ctx.call("product.get", {
                    id: ctx.params.productId
                });

                if (!product) {
                    throw new MoleculerClientError(
                        "Product not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                if (product.stock < ctx.params.quantity) {
                    throw new MoleculerClientError(
                        "Insufficient stock",
                        422,
                        "INSUFFICIENT_STOCK"
                    );
                }

                // Calculate total price
                const totalPrice = product.price * ctx.params.quantity;

                // Save order to database using adapter directly
                const order = await this.adapter.insert({
                    userId,
                    productId: ctx.params.productId,
                    quantity: ctx.params.quantity,
                    totalPrice,
                    status: "confirmed"
                });

                // Emit event — product service will reduce stock
                // email service will send confirmation
                ctx.emit("order.created", {
                    orderId: order._id.toString(),
                    userId,
                    productId: ctx.params.productId,
                    quantity: ctx.params.quantity,
                    totalPrice,
                    productName: product.name
                });

                return order;
            }
        },

        // Get all orders for the authenticated user
        myOrders: {
            rest: { method: "GET", path: "/my" },
            async handler(ctx) {
                const userId = ctx.meta.user.id;

                const orders = await this.adapter.find({
                    query: { userId }
                });

                return orders;
            }
        },

        // Update order status
        updateStatus: {
            rest: { method: "PUT", path: "/:id/status" },
            params: {
                id: "string",
                status: {
                    type: "enum",
                    values: ["pending", "confirmed", "shipped", "delivered", "cancelled"]
                }
            },
            async handler(ctx) {
                const order = await this.adapter.updateById(ctx.params.id, {
                    $set: { status: ctx.params.status }
                });

                if (!order) {
                    throw new MoleculerClientError(
                        "Order not found",
                        404,
                        "NOT_FOUND"
                    );
                }

                // Emit status change event
                ctx.emit("order.statusUpdated", {
                    orderId: ctx.params.id,
                    status: ctx.params.status
                });

                return order;
            }
        }
    }
};

Useful moleculer-db Adapter Methods

These are the methods you use inside your action handlers when you need custom database operations beyond the built-in actions:

// Find one record matching a query
await this.adapter.findOne({ email: "rahul@example.com" });

// Find by MongoDB ID
await this.adapter.findById("60d5f484b9e5c82f9c5e4d3a");

// Find multiple records
await this.adapter.find({
    query: { isActive: true },      // filter
    sort: ["-createdAt"],           // sort by createdAt descending
    limit: 10,                      // max results
    offset: 0                       // skip
});

// Count records
await this.adapter.count({ query: { isActive: true } });

// Insert one record
await this.adapter.insert({ name: "Rahul", email: "rahul@example.com" });

// Update by ID — MongoDB update operators work here
await this.adapter.updateById("60d5f484b9e5c82f9c5e4d3a", {
    $set: { name: "Updated Name" },
    $inc: { loginCount: 1 }
});

// Delete by ID
await this.adapter.removeById("60d5f484b9e5c82f9c5e4d3a");

Environment Variables — Keeping Config Out of Code

Never hardcode your MongoDB URI or JWT secret in your code. Use environment variables. Create a .env file in your project root:

.env

MONGO_URI=mongodb://localhost:27017/moleculer-course
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
PORT=3000

Install dotenv:

npm install dotenv

Load it at the very top of moleculer.config.js:

"use strict";

require("dotenv").config();

module.exports = {
    namespace: "ecommerce",

    // Now these read from .env file
    // process.env.MONGO_URI is available everywhere
};

Add .env to your .gitignore:

node_modules/
.env

Updated Folder Structure

Your project should now look like this:

my-project/
  models/
    user.model.js
    product.model.js
    order.model.js
  services/
    api.service.js
    user.service.js
    product.service.js
    order.service.js
    email.service.js
    notification.service.js
  moleculer.config.js
  package.json
  .env
  .gitignore

Testing the Complete Flow

Start your project:

npm run dev

Step 1: Create a product

POST http://localhost:3000/api/product
Body: {
    "name": "Mechanical Keyboard",
    "description": "RGB mechanical keyboard with Cherry MX switches",
    "price": 2999,
    "stock": 50,
    "category": "electronics"
}

Step 2: Register a user

POST http://localhost:3000/api/public/register
Body: {
    "name": "Rahul Sharma",
    "email": "rahul@example.com",
    "password": "password123"
}

Step 3: Login

POST http://localhost:3000/api/public/login
Body: {
    "email": "rahul@example.com",
    "password": "password123"
}

Copy the token from the Authorization response header.

Step 4: Place an order

POST http://localhost:3000/api/order
Headers: Authorization: Bearer <token>
Body: {
    "productId": "<product id from step 1>",
    "quantity": 2
}

Step 5: Check your orders

GET http://localhost:3000/api/order/my
Headers: Authorization: Bearer <token>

Step 6: Check product stock reduced

GET http://localhost:3000/api/product/<product id>

Stock should now show 48 instead of 50.

Your entire backend is now backed by MongoDB with real data persistence.


Summary

  • moleculer-db is a service mixin that adds database capabilities to any service.
  • It provides free CRUD actions: list, find, count, get, create, update, remove, insert.
  • Use MongooseAdapter to connect to MongoDB with Mongoose models.
  • Define your Mongoose model in a separate models folder and pass it to the service.
  • Override built-in actions when you need custom business logic like password hashing.
  • Use this.adapter methods directly inside custom action handlers for database operations.
  • The settings.fields array controls which fields are returned in API responses.
  • Always hash passwords with bcrypt before saving to database.
  • Always put sensitive config like MONGO_URI and JWT_SECRET in .env file.
  • Never commit .env to git.

Up Next

Post 12 covers Logging, Metrics, and Tracing — the three pillars of observability. You will learn how to set up structured logging, expose Prometheus metrics, and trace requests across services using Jaeger. This is what allows you to understand what is happening inside your running application.


Course Progress: 11 of 15 posts complete.

Phase 6 - API Gateway | Post 10 | API Gateway — Exposing Your Services to the Outside World

Post 10 of 15 | Phase 6: API Gateway


API Gateway — Exposing Your Services to the Outside World

In every post so far, your services have been talking to each other internally. No browser, no Postman, no mobile app can reach them directly. They are completely internal. The API Gateway is the single entry point that sits between the outside world and your internal services. It receives HTTP requests, translates them into Moleculer action calls, and sends the response back.


What is an API Gateway in Microservices

In a monolith, every route is directly accessible. In microservices, internal services should never be exposed directly to the internet. You expose one single service — the API Gateway — and everything else stays private.

Without API Gateway (wrong):
Browser → user-service:3001
Browser → order-service:3002
Browser → product-service:3003
All services exposed publicly. Security nightmare.

With API Gateway (correct):
Browser → API Gateway:3000 → user-service (internal)
                            → order-service (internal)
                            → product-service (internal)
Only one public entry point. Everything else is private.

The API Gateway handles:

  • Receiving HTTP requests from clients
  • Authentication and authorization
  • Request validation
  • Routing to the correct internal service
  • Sending the response back to the client

moleculer-web — The Official API Gateway

Moleculer has an official API Gateway package called moleculer-web. It is itself a Moleculer service. It runs inside your broker alongside other services and automatically maps HTTP routes to Moleculer actions.

You already have it in your project because you selected it during the CLI setup. Let us go through it properly now.

Check your package.json — moleculer-web should already be there. If not, install it:

npm install moleculer-web

The api.service.js File — Full Breakdown

Open services/api.service.js. The CLI generated a basic version. Let us rewrite it completely so you understand every part:

"use strict";

const ApiGateway = require("moleculer-web");

module.exports = {
    name: "api",

    // moleculer-web is used as a mixin
    // A mixin merges another service schema into this one
    // This gives your api service all the HTTP server capabilities
    mixins: [ApiGateway],

    settings: {
        // Port the HTTP server listens on
        port: process.env.PORT || 3000,

        // IP address to bind to
        // 0.0.0.0 means accept connections from any IP
        ip: "0.0.0.0",

        // Routes define how HTTP requests map to Moleculer actions
        routes: [
            {
                // All routes in this block start with /api
                path: "/api",

                // Enable whitelist — only listed actions are accessible
                // Use "**" to allow all actions
                whitelist: [
                    "**"
                ],

                // Automatically map REST routes from action definitions
                // This reads the rest property from each action
                // and creates the corresponding HTTP route
                autoAliases: true,

                // Body parser — needed for POST/PUT requests with JSON body
                bodyParsers: {
                    json: { strict: false, limit: "1MB" },
                    urlencoded: { extended: true, limit: "1MB" }
                },

                // CORS configuration
                cors: {
                    origin: "*",
                    methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
                    allowedHeaders: ["Content-Type", "Authorization"],
                    credentials: false
                },

                // Hooks run before and after every request in this route block
                onBeforeCall(ctx, route, req, res) {
                    // This runs before every action call
                    // Good place to extract auth token and put in ctx.meta
                    ctx.meta.clientIP = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
                    ctx.meta.userAgent = req.headers["user-agent"];
                },

                onAfterCall(ctx, route, req, res, data) {
                    // This runs after every action call
                    // data is the result returned by the action
                    // You can modify data here before sending to client
                    return data;
                }
            }
        ],

        // Global error handler
        onError(req, res, err) {
            res.setHeader("Content-Type", "application/json");
            res.writeHead(err.code || 500);
            res.end(JSON.stringify({
                error: true,
                message: err.message,
                code: err.code || 500
            }));
        }
    }
};

How autoAliases Works

When autoAliases: true is set, the API Gateway reads the rest property from every action in every service and automatically creates the corresponding HTTP route.

Your user service has:

actions: {
    list: {
        rest: { method: "GET", path: "/" },
        handler(ctx) { ... }
    },
    getById: {
        rest: { method: "GET", path: "/:id" },
        handler(ctx) { ... }
    },
    create: {
        rest: { method: "POST", path: "/" },
        handler(ctx) { ... }
    }
}

With autoAliases, these become:

GET    /api/user        → user.list
GET    /api/user/:id    → user.getById
POST   /api/user        → user.create

The URL pattern is: /api/serviceName/actionPath

This is automatic. You define the rest property in your service, and the API Gateway picks it up without any additional configuration.


Manual Aliases — Custom URL Mapping

Sometimes you want custom URLs that do not follow the automatic pattern. Use the aliases property:

routes: [
    {
        path: "/api",

        aliases: {
            // Custom URL → action mapping
            // Format: "METHOD URL": "service.action"

            "POST /auth/login":     "user.login",
            "POST /auth/register":  "user.register",
            "POST /auth/logout":    "user.logout",

            // REST shorthand — maps all CRUD routes automatically
            // GET /api/products        → product.list
            // GET /api/products/:id   → product.getById
            // POST /api/products      → product.create
            // PUT /api/products/:id   → product.update
            // DELETE /api/products/:id → product.remove
            "REST /products":       "product",

            // Custom name for a route
            "GET /me":              "user.getProfile",

            // Multi-word service names use dot notation
            "GET /admin/stats":     "admin.getStats"
        },

        // When using manual aliases, set autoAliases to false
        // or use both together
        autoAliases: true
    }
]

Authentication — The Most Important Part

This is where the real world gets serious. Your API Gateway must verify who is making each request before passing it to your services. The standard approach is JWT authentication.

Here is the complete pattern:

"use strict";

const ApiGateway = require("moleculer-web");
const jwt = require("jsonwebtoken");

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";

module.exports = {
    name: "api",
    mixins: [ApiGateway],

    settings: {
        port: 3000,

        routes: [
            // Route 1: Public routes — no authentication needed
            {
                path: "/api/public",
                whitelist: [
                    "user.login",
                    "user.register"
                ],
                bodyParsers: {
                    json: true,
                    urlencoded: { extended: true }
                },
                aliases: {
                    "POST /login":    "user.login",
                    "POST /register": "user.register"
                }
            },

            // Route 2: Protected routes — authentication required
            {
                path: "/api",
                whitelist: ["**"],
                autoAliases: true,
                bodyParsers: {
                    json: true,
                    urlencoded: { extended: true }
                },

                // authenticate runs before every request in this route block
                // Return the decoded user object to allow the request
                // Throw an error to reject the request
                async authenticate(ctx, route, req) {
                    const authHeader = req.headers["authorization"];

                    if (!authHeader) {
                        throw new ApiGateway.Errors.UnAuthorizedError(
                            ApiGateway.Errors.ERR_NO_TOKEN
                        );
                    }

                    // Header format: "Bearer <token>"
                    const token = authHeader.split(" ")[1];

                    if (!token) {
                        throw new ApiGateway.Errors.UnAuthorizedError(
                            ApiGateway.Errors.ERR_NO_TOKEN
                        );
                    }

                    try {
                        // Verify the JWT token
                        const decoded = jwt.verify(token, JWT_SECRET);

                        // Return the user — this gets stored in ctx.meta.user
                        // and is available in all downstream services
                        return decoded;

                    } catch (err) {
                        throw new ApiGateway.Errors.UnAuthorizedError(
                            ApiGateway.Errors.ERR_INVALID_TOKEN
                        );
                    }
                },

                // authorize runs after authenticate
                // Here you check if the authenticated user has
                // permission to access the requested action
                async authorize(ctx, route, req) {
                    const user = ctx.meta.user;

                    // Example: only admins can access admin actions
                    if (req.$action.name.startsWith("admin.")) {
                        if (!user || user.role !== "admin") {
                            throw new ApiGateway.Errors.ForbiddenError(
                                "Admin access required"
                            );
                        }
                    }
                }
            }
        ]
    }
};

Install jsonwebtoken:

npm install jsonwebtoken

Now update your user service to generate a JWT on login:

"use strict";

const jwt = require("jsonwebtoken");
const { MoleculerClientError } = require("moleculer").Errors;

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";

const users = [
    { id: "1", name: "Rahul Sharma", email: "rahul@example.com", password: "password123", role: "admin" },
    { id: "2", name: "Priya Singh", email: "priya@example.com", password: "password456", role: "user" }
];

module.exports = {
    name: "user",

    actions: {
        // Public action — no auth needed
        login: {
            params: {
                email: "email",
                password: "string"
            },
            handler(ctx) {
                const user = users.find(u =>
                    u.email === ctx.params.email &&
                    u.password === ctx.params.password
                );

                if (!user) {
                    throw new MoleculerClientError(
                        "Invalid email or password",
                        401,
                        "INVALID_CREDENTIALS"
                    );
                }

                // Generate JWT token
                const token = jwt.sign(
                    { id: user.id, email: user.email, role: user.role },
                    JWT_SECRET,
                    { expiresIn: "24h" }
                );

                // Put token in ctx.meta
                // API Gateway will read it from here
                ctx.meta.token = token;

                return {
                    message: "Login successful",
                    user: { id: user.id, name: user.name, email: user.email, role: user.role }
                };
            }
        },

        // Public action — no auth needed
        register: {
            params: {
                name: "string",
                email: "email",
                password: { type: "string", min: 6 }
            },
            handler(ctx) {
                const exists = users.find(u => u.email === ctx.params.email);
                if (exists) {
                    throw new MoleculerClientError(
                        "Email already registered",
                        422,
                        "EMAIL_EXISTS"
                    );
                }

                const newUser = {
                    id: String(users.length + 1),
                    name: ctx.params.name,
                    email: ctx.params.email,
                    password: ctx.params.password,
                    role: "user"
                };
                users.push(newUser);

                return {
                    message: "Registration successful",
                    user: { id: newUser.id, name: newUser.name, email: newUser.email }
                };
            }
        },

        // Protected action — requires authentication
        getProfile: {
            rest: { method: "GET", path: "/profile" },
            handler(ctx) {
                // ctx.meta.user is set by the API Gateway authenticate hook
                const currentUser = ctx.meta.user;
                const user = users.find(u => u.id === currentUser.id);

                if (!user) {
                    throw new MoleculerClientError("User not found", 404, "NOT_FOUND");
                }

                return {
                    id: user.id,
                    name: user.name,
                    email: user.email,
                    role: user.role
                };
            }
        },

        // Protected action — list all users
        list: {
            rest: { method: "GET", path: "/" },
            handler(ctx) {
                return users.map(u => ({
                    id: u.id,
                    name: u.name,
                    email: u.email,
                    role: u.role
                }));
            }
        }
    }
};

Sending the JWT Token Back to the Client

When the user logs in, the token is in ctx.meta.token. You need to send it to the client in the response. Add an onAfterCall hook to the public route:

// In the public route block
onAfterCall(ctx, route, req, res, data) {
    // If a token was set in meta during the action
    // send it as a response header
    if (ctx.meta.token) {
        res.setHeader("Authorization", `Bearer ${ctx.meta.token}`);
    }
    return data;
}

Now the client receives the token in the Authorization response header. They store it and send it with every subsequent request.


Testing the Full Auth Flow in Postman

Step 1: Register

POST http://localhost:3000/api/public/register
Body: {
    "name": "Amit Kumar",
    "email": "amit@example.com",
    "password": "secret123"
}

Step 2: Login

POST http://localhost:3000/api/public/login
Body: {
    "email": "amit@example.com",
    "password": "secret123"
}

Copy the token from the Authorization response header.

Step 3: Access protected route

GET http://localhost:3000/api/user/profile
Headers: Authorization: Bearer <paste token here>

You get the profile. Remove the token and try again — you get a 401 Unauthorized error.


Whitelist — Controlling Which Actions Are Accessible

The whitelist controls which Moleculer actions the API Gateway exposes. This is a security feature. Actions not in the whitelist return a 404.

routes: [
    {
        path: "/api",

        whitelist: [
            // Allow specific actions
            "user.list",
            "user.getById",
            "product.list",
            "product.getById",

            // Allow all actions of a service
            "order.*",

            // Allow everything (use carefully)
            "**"
        ]
    }
]

In production, never use "**" on authenticated routes without thinking. Be explicit about what you expose.


Global Rate Limiting

You can add basic rate limiting to prevent abuse:

routes: [
    {
        path: "/api",

        rateLimit: {
            // Window size in milliseconds
            window: 60 * 1000,  // 1 minute

            // Max requests per window per IP
            limit: 100,

            // Response when limit exceeded
            headers: true,
            key: (req) => {
                return req.headers["x-forwarded-for"] ||
                       req.socket.remoteAddress;
            }
        }
    }
]

Accessing URL Parameters

When you define a route with a parameter like /:id, Moleculer automatically merges it into ctx.params:

// Route definition in action
rest: { method: "GET", path: "/:id" }

// Request: GET /api/user/123

// Inside handler:
handler(ctx) {
    console.log(ctx.params.id); // "123"
}

Query string parameters also merge automatically:

GET /api/user?page=2&limit=10

handler(ctx) {
    console.log(ctx.params.page);  // "2"
    console.log(ctx.params.limit); // "10"
}

Complete Folder Structure at This Point

Your project should now look like this:

my-project/
  services/
    api.service.js        ← API Gateway with auth
    user.service.js       ← User service with login, register, profile
    product.service.js    ← Product service with caching
    order.service.js      ← Order service with events
    email.service.js      ← Email service listening to events
    notification.service.js ← Notification service
  moleculer.config.js
  package.json

You now have a real microservices backend taking shape.


Summary

  • The API Gateway is the single public entry point. Internal services are never exposed directly.
  • moleculer-web is the official API Gateway package. It runs as a Moleculer service using a mixin.
  • autoAliases: true automatically creates HTTP routes from the rest property in each action.
  • Manual aliases give you full control over URL to action mapping.
  • Two route blocks: public routes with no auth, protected routes with authentication.
  • The authenticate hook verifies the JWT token and returns the decoded user which goes into ctx.meta.user.
  • The authorize hook checks if the authenticated user has permission for the requested action.
  • onBeforeCall and onAfterCall hooks run before and after every request.
  • Use onAfterCall to send the JWT token back to the client in the response header.
  • Whitelist controls which actions are accessible through the gateway.
  • URL parameters and query strings are automatically merged into ctx.params.
  • Rate limiting is built-in and configured per route block.

Up Next

Post 11 covers Database Integration using moleculer-db with MongoDB and Mongoose. We will replace the in-memory arrays in our services with real database operations, and you will see how moleculer-db gives you free CRUD actions out of the box with zero boilerplate.


Course Progress: 10 of 15 posts complete.

Phase 5 - Caching | Post 9 | Caching — Making Your Services Fast With Zero Extra Effort

Post 9 of 15 | Phase 5: Caching


Caching — Making Your Services Fast With Zero Extra Effort

In the previous post you learned how to keep your application alive when things break. In this post you learn how to make your application fast. Caching is one of the biggest performance improvements you can add to any system, and Moleculer makes it extremely simple.


What is Caching and Why Do You Need It

Every time an action is called, it executes its handler. If that handler reads from a database, it makes a database query. If 1000 requests per second call user.getById with the same user ID, you are making 1000 database queries per second for the exact same data that never changed.

Caching solves this by storing the result of an action call. The next time the same action is called with the same parameters, Moleculer returns the stored result immediately without executing the handler or touching the database.

Think of it like a waiter who memorizes your regular order. The first time you come in, he goes to the kitchen and gets your food. Every time after that, he already knows what you want and brings it immediately without asking the kitchen.


How Moleculer Caching Works

Moleculer caching works at the action level. You mark an action as cacheable. Moleculer automatically generates a cache key based on the action name and the params. If that key exists in the cache, it returns the cached result. If not, it runs the handler, stores the result, and returns it.

First call: user.getById { id: "1" }
→ Cache miss
→ Handler runs, hits database
→ Result stored in cache with key "user.getById:1"
→ Result returned to caller

Second call: user.getById { id: "1" }
→ Cache hit
→ Handler does NOT run
→ Cached result returned immediately
→ Database not touched

This is completely transparent to the caller. They call the same action the same way. They get the same result. They just get it much faster.


Available Cache Adapters

Moleculer supports multiple caching backends:

  • Memory — stores cache in the Node.js process memory. Fastest possible. Lost on restart. Not shared between nodes.
  • MemoryLRU — same as Memory but with LRU eviction. Automatically removes least recently used items when memory fills up.
  • Redis — stores cache in Redis. Persists across restarts. Shared between all nodes. Recommended for production.

Enabling the Cache Adapter

In moleculer.config.js:

Memory cache:

module.exports = {
    cacher: "Memory"
};

MemoryLRU cache with max size:

module.exports = {
    cacher: {
        type: "MemoryLRU",
        options: {
            max: 1000,    // Maximum 1000 items in cache
            ttl: 30       // Default TTL 30 seconds
        }
    }
};

Redis cache:

module.exports = {
    cacher: {
        type: "Redis",
        options: {
            host: "localhost",
            port: 6379,
            password: "",
            db: 0,
            ttl: 30       // Default TTL 30 seconds
        }
    }
};

Install the Redis client if using Redis cache:

npm install ioredis

Making an Action Cacheable

Once you have a cache adapter configured, marking an action as cacheable is one line:

module.exports = {
    name: "user",

    actions: {
        getById: {
            // Add this one line — that is it
            cache: true,

            params: {
                id: "string"
            },
            async handler(ctx) {
                this.logger.info(`Hitting database for user ${ctx.params.id}`);
                // Simulate database call
                return { id: ctx.params.id, name: "Rahul Sharma" };
            }
        }
    }
};

Test this in REPL:

call user.getById {"id": "1"}
call user.getById {"id": "1"}
call user.getById {"id": "1"}

In your terminal you will see the logger message "Hitting database for user 1" only once. The second and third calls returned the cached result without running the handler.


Cache Keys — How Moleculer Generates Them

By default when you set cache: true, Moleculer generates a cache key using all the params. So:

user.getById { id: "1" }       → cache key: "user.getById:1"
user.getById { id: "2" }       → cache key: "user.getById:2"
user.list { page: 1, limit: 10 } → cache key: "user.list:1|10"

Each unique combination of params gets its own cache entry. This is correct behavior in most cases.

Custom cache keys:

Sometimes you want to control which params are used in the cache key. For example, if your action receives a large params object but only the id field determines the result, you only want the key based on id.

actions: {
    getById: {
        cache: {
            // Only use "id" field for the cache key
            // Other params are ignored for caching purposes
            keys: ["id"]
        },
        params: {
            id: "string"
        },
        handler(ctx) {
            return { id: ctx.params.id, name: "Rahul Sharma" };
        }
    }
}

Including ctx.meta in the cache key:

Sometimes the cache result depends on something in ctx.meta, like the current user's role or language preference. You can include meta fields in the key:

actions: {
    getProfile: {
        cache: {
            // Include both params and a meta field in the key
            keys: ["id", "#user.role"]
            // # prefix means it comes from ctx.meta, not ctx.params
        },
        handler(ctx) {
            const role = ctx.meta.user.role;
            // Admin gets full profile, user gets limited profile
            if (role === "admin") {
                return { id: ctx.params.id, name: "Rahul", salary: 50000 };
            }
            return { id: ctx.params.id, name: "Rahul" };
        }
    }
}

TTL — Time to Live

TTL defines how long a cached result stays valid. After the TTL expires, the next call runs the handler again and refreshes the cache.

Global TTL in moleculer.config.js:

module.exports = {
    cacher: {
        type: "Memory",
        options: {
            ttl: 30   // All cached items expire after 30 seconds by default
        }
    }
};

Per-action TTL override:

actions: {
    // User data changes rarely — cache for 5 minutes
    getById: {
        cache: {
            ttl: 300
        },
        handler(ctx) {
            return getUserFromDB(ctx.params.id);
        }
    },

    // Exchange rates change every minute — cache for 60 seconds
    getExchangeRate: {
        cache: {
            ttl: 60
        },
        handler(ctx) {
            return fetchExchangeRateFromAPI(ctx.params.currency);
        }
    },

    // Real-time stock price — do not cache at all
    getStockPrice: {
        // No cache property — handler runs every time
        handler(ctx) {
            return fetchLiveStockPrice(ctx.params.symbol);
        }
    }
}

Choose TTL based on how often data changes and how much staleness is acceptable for your use case.


Cache Invalidation — The Hard Part

Caching is easy. Knowing when to clear the cache is the hard part. When data changes, the cached version becomes stale. You need to clear it so the next call gets fresh data.

Moleculer gives you two ways to clear cache.

Method 1: Clear specific cache entries

module.exports = {
    name: "user",

    actions: {
        // READ — cacheable
        getById: {
            cache: { keys: ["id"], ttl: 300 },
            handler(ctx) {
                return getUserFromDB(ctx.params.id);
            }
        },

        // WRITE — clears the cache after updating
        update: {
            params: {
                id: "string",
                name: "string"
            },
            async handler(ctx) {
                // Update in database
                const updated = await updateUserInDB(ctx.params.id, ctx.params.name);

                // Clear the specific cache entry for this user
                // This uses the same key format as the getById cache
                await this.broker.cacher.del(`user.getById:${ctx.params.id}`);

                this.logger.info(`Cache cleared for user ${ctx.params.id}`);

                return updated;
            }
        }
    }
};

Method 2: Clear cache using built-in cache cleaning event

Moleculer has a cleaner approach using the built-in cache.clean event. Instead of manually building cache key strings, you broadcast a clean event with a pattern:

module.exports = {
    name: "user",

    actions: {
        getById: {
            cache: { keys: ["id"], ttl: 300 },
            handler(ctx) {
                return getUserFromDB(ctx.params.id);
            }
        },

        list: {
            cache: { ttl: 60 },
            handler(ctx) {
                return getAllUsersFromDB();
            }
        },

        update: {
            params: {
                id: "string",
                name: "string"
            },
            async handler(ctx) {
                const updated = await updateUserInDB(ctx.params.id, ctx.params.name);

                // Clear ALL cache entries for the user service
                // The pattern "user.**" matches every cache key that starts with "user."
                await this.broker.broadcast("cache.clean.user");

                return updated;
            }
        },

        create: {
            async handler(ctx) {
                const newUser = await createUserInDB(ctx.params);

                // Clear the user list cache since a new user was added
                await this.broker.broadcast("cache.clean.user");

                return newUser;
            }
        }
    },

    events: {
        // Listen for the cache clean event and clear matching entries
        "cache.clean.user"() {
            if (this.broker.cacher) {
                this.broker.cacher.clean("user.**");
            }
        }
    }
};

The broadcast approach is better than manual key deletion because:

  • It works across all nodes when using Redis cache with a transporter
  • You do not need to manually construct cache key strings
  • One event clears cache on every node simultaneously

Practical Example — Full Service With Caching

Let us build a complete product service with proper caching and cache invalidation. Create services/product.service.js:

"use strict";

// Simulated database
const products = [
    { id: "1", name: "Mechanical Keyboard", price: 2999, stock: 50 },
    { id: "2", name: "Wireless Mouse", price: 1499, stock: 100 },
    { id: "3", name: "USB Hub", price: 799, stock: 200 }
];

let nextId = 4;

module.exports = {
    name: "product",

    actions: {
        // List all products — cache for 60 seconds
        list: {
            rest: { method: "GET", path: "/" },
            cache: {
                ttl: 60
            },
            handler(ctx) {
                this.logger.info("Fetching product list from database");
                return products;
            }
        },

        // Get one product — cache for 5 minutes
        getById: {
            rest: { method: "GET", path: "/:id" },
            cache: {
                keys: ["id"],
                ttl: 300
            },
            params: {
                id: "string"
            },
            handler(ctx) {
                this.logger.info(`Fetching product ${ctx.params.id} from database`);
                const product = products.find(p => p.id === ctx.params.id);
                if (!product) {
                    throw new Error("Product not found");
                }
                return product;
            }
        },

        // Create product — invalidate list cache
        create: {
            rest: { method: "POST", path: "/" },
            params: {
                name: "string",
                price: "number",
                stock: "number"
            },
            async handler(ctx) {
                const newProduct = {
                    id: String(nextId++),
                    ...ctx.params
                };
                products.push(newProduct);

                // List changed — clear list cache
                await this.broker.broadcast("cache.clean.product");
                this.logger.info("Product created and cache cleared");

                return newProduct;
            }
        },

        // Update product — invalidate specific product cache and list cache
        update: {
            rest: { method: "PUT", path: "/:id" },
            params: {
                id: "string",
                name: { type: "string", optional: true },
                price: { type: "number", optional: true },
                stock: { type: "number", optional: true }
            },
            async handler(ctx) {
                const index = products.findIndex(p => p.id === ctx.params.id);
                if (index === -1) {
                    throw new Error("Product not found");
                }

                products[index] = { ...products[index], ...ctx.params };

                // Clear all product cache — both list and individual entries
                await this.broker.broadcast("cache.clean.product");
                this.logger.info(`Product ${ctx.params.id} updated and cache cleared`);

                return products[index];
            }
        }
    },

    events: {
        "cache.clean.product"() {
            if (this.broker.cacher) {
                // Clear all cache entries starting with "product."
                this.broker.cacher.clean("product.**");
                this.logger.info("Product cache cleared");
            }
        }
    }
};

Enable caching in moleculer.config.js:

module.exports = {
    cacher: {
        type: "Memory",
        options: {
            ttl: 30
        }
    }
};

Now test with these requests in sequence:

GET http://localhost:3000/api/product

Check terminal — you see "Fetching product list from database".

GET http://localhost:3000/api/product

Check terminal — nothing printed. Cache was returned.

POST http://localhost:3000/api/product
Body: { "name": "Monitor", "price": 15000, "stock": 20 }

Check terminal — "Product created and cache cleared".

GET http://localhost:3000/api/product

Check terminal — "Fetching product list from database" again. Cache was cleared and fresh data was fetched.


Checking Cache Status in REPL

The REPL has built-in commands for inspecting the cache:

npm run repl

List all cached keys:

cache keys

Get a specific cached value:

cache get product.getById:1

Delete a specific cache key:

cache del product.getById:1

Clear all cache:

cache clear

These are extremely useful during development when you want to inspect or manually clear cache without restarting the server.


Redis Cache for Production

In production with multiple nodes, Memory cache does not work properly. Each node has its own memory cache. When Node A updates a product and clears its cache, Node B still has the old cached value in its own memory.

Redis solves this because it is a shared external cache. All nodes read from and write to the same Redis instance. When any node clears a cache entry, it is cleared for everyone.

Switch to Redis cache in production simply by changing moleculer.config.js:

module.exports = {
    cacher: {
        type: "Redis",
        options: {
            host: "localhost",
            port: 6379,
            ttl: 30
        }
    }
};

Your service code does not change at all. The cache.clean event and this.broker.cacher.clean() work exactly the same way with Redis as they do with Memory.


What to Cache and What Not to Cache

Cache these:

  • Data that is read frequently but changes rarely — user profiles, product details, categories
  • Results of expensive database queries — aggregations, joins, reports
  • Results of external API calls — exchange rates, weather data, shipping rates

Do not cache these:

  • Real-time data — live stock prices, active user counts
  • User-specific sensitive data — unless you include user ID in the cache key
  • Write operations — create, update, delete actions should never be cached
  • Data that must always be fresh — inventory levels during checkout, payment status

Summary

  • Caching stores action results and returns them on subsequent identical calls without running the handler.
  • Three cache adapters: Memory (fastest, not shared), MemoryLRU (auto eviction), Redis (shared across nodes, recommended for production).
  • Enable caching on an action with cache: true or cache: { keys: [], ttl: 0 }.
  • Cache keys are generated from action name and params by default.
  • Use keys array to control which params are included in the cache key.
  • Use # prefix in keys array to include ctx.meta fields in the cache key.
  • TTL controls how long cached data stays valid. Set per-action based on how often data changes.
  • Cache invalidation on write: broadcast a clean event and listen to it in the service.
  • Use this.broker.cacher.clean("service.**") to clear all cache entries for a service.
  • Redis cache is mandatory in production with multiple nodes. Memory cache is per-node.
  • REPL commands: cache keys, cache get, cache del, cache clear.

Up Next

Post 10 covers the API Gateway — moleculer-web. This is how you expose your internal Moleculer services over HTTP so browsers, mobile apps, and external clients can reach them. We will cover routing, authentication hooks, request mapping, file uploads, and CORS configuration.


Course Progress: 9 of 15 posts complete.

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 services are always available and always respond correctly. In reality, services crash, slow down, run out of memory, and fail in unexpected ways. This is not a rare edge case. In a distributed system with many services, something is always failing somewhere.

Fault tolerance is the set of techniques that keep your overall application working even when individual parts of it are broken. Moleculer has four built-in fault tolerance mechanisms: Timeout, Retry, Circuit Breaker, and Bulkhead. This post covers all four.


The Restaurant Analogy Revisited

Before writing any code, understand the problem through a real-world scenario.

You own a restaurant. One day your kitchen equipment breaks down and every order takes 45 minutes instead of 15. Customers are waiting, getting frustrated, and new customers are still walking in and placing orders. Soon the entire restaurant is backed up. Nobody is getting served. The restaurant collapses under the load of waiting orders.

What should you have done?

  • After waiting 20 minutes with no food, tell the customer we cannot serve you right now. That is a Timeout.
  • If the kitchen fails on the first attempt, try again once or twice before giving up. That is a Retry.
  • If the kitchen has failed 10 times in a row, stop sending orders there and tell customers immediately instead of making them wait. That is a Circuit Breaker.
  • Only allow 5 orders in the kitchen at once. If more come in, queue them or reject them. That is a Bulkhead.

These four concepts apply directly to your microservices.


Fault Tolerance Mechanism 1: Timeout

A timeout says: if this action does not respond within a certain time, stop waiting and throw an error.

Without a timeout, a slow service can block your entire application. Your calling service waits forever, occupying resources, and eventually your whole system grinds to a halt.

Global timeout in moleculer.config.js:

module.exports = {
    requestTimeout: 10 * 1000  // 10 seconds for every action call
};

Per-call timeout override:

actions: {
    async createOrder(ctx) {
        // This specific call has a 3 second timeout
        // Overrides the global 10 second timeout
        const user = await ctx.call("user.getById", { id: ctx.params.userId }, {
            timeout: 3000
        });
        return user;
    }
}

Per-action timeout on the action definition itself:

module.exports = {
    name: "report",

    actions: {
        generate: {
            // This action allows up to 30 seconds
            // because generating reports is slow
            timeout: 30000,
            handler(ctx) {
                // slow report generation
            }
        }
    }
};

What happens when a timeout occurs:

Moleculer throws a RequestTimeoutError. The calling service receives this error and can handle it:

actions: {
    async getDashboard(ctx) {
        try {
            const data = await ctx.call("slow.service", {}, { timeout: 3000 });
            return data;
        } catch (err) {
            if (err.name === "RequestTimeoutError") {
                // Return a fallback response instead of crashing
                return { message: "Service is slow right now. Please try again." };
            }
            throw err;
        }
    }
}

Setting timeout to zero disables it for that call:

// No timeout for this call — wait forever
const result = await ctx.call("long.running.job", {}, { timeout: 0 });

Fault Tolerance Mechanism 2: Retry

A retry says: if this action call fails, automatically try again a few times before giving up.

Some failures are temporary. A service might be restarting, a database connection might be briefly lost, a network blip might have occurred. Retrying after a short delay often resolves these temporary failures without any user-visible error.

Global retry policy in moleculer.config.js:

module.exports = {
    retryPolicy: {
        enabled: true,
        retries: 3,          // Try up to 3 times after the first failure
        delay: 100,          // Wait 100ms before first retry
        maxDelay: 2000,      // Never wait more than 2 seconds between retries
        factor: 2,           // Double the delay each time (exponential backoff)
        check: err => err && !!err.retryable  // Only retry if error is marked retryable
    }
};

With factor: 2 and delay: 100, the retry timing looks like this:

First attempt  → fails
Wait 100ms
Second attempt → fails
Wait 200ms
Third attempt  → fails
Wait 400ms
Fourth attempt → fails or succeeds
Give up if still failing

This is called exponential backoff. Waiting longer between each retry gives the failing service more time to recover.

Per-call retry override:

actions: {
    async processPayment(ctx) {
        // Payment is critical — retry up to 5 times
        const result = await ctx.call("payment.charge", {
            amount: ctx.params.amount
        }, {
            retries: 5
        });
        return result;
    }
}

Making your errors retryable:

By default, Moleculer only retries errors marked as retryable. You control this when throwing errors:

const { MoleculerRetryableError } = require("moleculer").Errors;

actions: {
    getFromExternalAPI(ctx) {
        try {
            // Call an external API
        } catch (err) {
            if (err.message.includes("ECONNRESET")) {
                // Network error — worth retrying
                throw new MoleculerRetryableError("External API unavailable", 503);
            }
            // Logic error — do not retry
            throw err;
        }
    }
}

Important: Do not retry non-idempotent operations blindly

An idempotent operation is one that produces the same result no matter how many times you run it. Reading data is idempotent. Charging a credit card is not — you do not want to charge three times just because the response was slow.

Be careful enabling global retries. It is safer to enable retries per-call for operations you know are safe to retry.


Fault Tolerance Mechanism 3: Circuit Breaker

This is the most important fault tolerance pattern. Understand it well.

The Problem Without Circuit Breaker

Imagine your user service is down. Every time order service calls user.getById, it waits 10 seconds for the timeout, then fails. If 100 requests per second are coming in, that is 100 requests all waiting 10 seconds each, occupying memory and connections. Your order service slows to a crawl because of one broken downstream service.

What Circuit Breaker Does

The Circuit Breaker monitors calls to each service. If too many calls fail within a time window, it opens the circuit. An open circuit means it stops trying to call the service immediately — it throws an error right away without waiting for a timeout. This protects the calling service from being dragged down by a broken dependency.

There are three states:

Closed — Normal operation. Calls go through. Failures are counted.

Open — Too many failures detected. Calls are blocked immediately. No actual call is made. Error is thrown instantly.

Half-Open — After a cooldown period, one test call is allowed through. If it succeeds, circuit closes again. If it fails, circuit stays open.

CLOSED → (too many failures) → OPEN → (cooldown passes) → HALF-OPEN → (test succeeds) → CLOSED
                                                                      → (test fails)    → OPEN

Enabling Circuit Breaker in moleculer.config.js:

module.exports = {
    circuitBreaker: {
        enabled: true,
        threshold: 0.5,        // Open if 50% of calls fail
        minRequestCount: 20,   // Need at least 20 requests before evaluating
        windowTime: 60,        // Look at failures in the last 60 seconds
        halfOpenTime: 10000,   // Wait 10 seconds before allowing a test call
        check: err => err && err.code >= 500  // Only count 5xx errors as failures
    }
};

Let us go through each option:

  • threshold: 0.5 means if 50 percent or more of calls fail, open the circuit
  • minRequestCount: 20 means do not open the circuit until at least 20 calls have been made. Prevents opening on just one or two failures during startup.
  • windowTime: 60 means count failures that happened in the last 60 seconds only
  • halfOpenTime: 10000 means after the circuit opens, wait 10 seconds before trying one test call
  • check defines what counts as a failure. Here only server errors (500+) count. A 404 or validation error does not trip the circuit breaker.

What the caller sees:

actions: {
    async createOrder(ctx) {
        try {
            const user = await ctx.call("user.getById", { id: ctx.params.userId });
            return user;
        } catch (err) {
            if (err.name === "CircuitBreakerOpenError") {
                // Circuit is open. User service is known to be broken.
                // Return a graceful response instead of making the user wait.
                return { error: "User service is temporarily unavailable" };
            }
            throw err;
        }
    }
}

Without the circuit breaker, every call waits 10 seconds before failing. With the circuit breaker, once it opens, every call fails in milliseconds. Your order service stays responsive even though user service is broken.

Testing Circuit Breaker

Create this file to see circuit breaker behavior:

"use strict";

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

const broker = new ServiceBroker({
    logLevel: "info",
    circuitBreaker: {
        enabled: true,
        threshold: 0.5,
        minRequestCount: 3,   // Low number for testing purposes
        windowTime: 60,
        halfOpenTime: 5000
    }
});

// A service that always fails
broker.createService({
    name: "broken",
    actions: {
        doSomething(ctx) {
            throw new Error("I am always broken");
        }
    }
});

// A service that calls the broken service
broker.createService({
    name: "caller",
    actions: {
        async test(ctx) {
            try {
                await ctx.call("broken.doSomething", {});
            } catch (err) {
                return `Error type: ${err.name} — ${err.message}`;
            }
        }
    }
});

broker.start()
    .then(async () => {
        // Make several calls — watch the error type change
        for (let i = 1; i <= 8; i++) {
            const result = await broker.call("caller.test", {});
            console.log(`Call ${i}: ${result}`);
            await new Promise(r => setTimeout(r, 200));
        }
        await broker.stop();
    });

Run this file:

node circuit-test.js

Output:

Call 1: Error type: Error — I am always broken
Call 2: Error type: Error — I am always broken
Call 3: Error type: Error — I am always broken
Call 4: Error type: CircuitBreakerOpenError — Circuit breaker is open
Call 5: Error type: CircuitBreakerOpenError — Circuit breaker is open
Call 6: Error type: CircuitBreakerOpenError — Circuit breaker is open
Call 7: Error type: CircuitBreakerOpenError — Circuit breaker is open
Call 8: Error type: CircuitBreakerOpenError — Circuit breaker is open

After 3 failures the circuit opens. Subsequent calls fail instantly without actually calling the broken service.


Fault Tolerance Mechanism 4: Bulkhead

A bulkhead limits how many concurrent calls can be active at the same time for a service. If the limit is reached, additional calls are queued or rejected.

The name comes from ship design. A bulkhead is a wall that divides a ship into sections. If one section floods, the bulkhead prevents the entire ship from sinking. In software, if one service is overwhelmed, the bulkhead prevents it from taking down everything else.

Enabling Bulkhead in moleculer.config.js:

module.exports = {
    bulkhead: {
        enabled: true,
        concurrency: 10,      // Only 10 calls active at the same time
        maxQueueSize: 100     // Queue up to 100 additional calls
    }
};

With these settings:

  • First 10 calls execute immediately
  • Calls 11 to 110 wait in a queue
  • Call 111 and beyond are rejected with a QueueIsFullError

Per-action bulkhead:

You can also set bulkhead limits on individual actions:

module.exports = {
    name: "report",

    actions: {
        generate: {
            // Report generation is heavy — only 3 at a time
            bulkhead: {
                enabled: true,
                concurrency: 3,
                maxQueueSize: 10
            },
            async handler(ctx) {
                // Heavy report generation
                await generateHeavyReport();
                return { status: "done" };
            }
        },

        // Other actions in this service are not limited
        list: {
            handler(ctx) {
                return [];
            }
        }
    }
};

This is useful when one action is resource-heavy and you do not want it to consume all available resources and starve other actions.


Fallback — The Safety Net

A fallback is a function that runs when an action call fails for any reason — timeout, circuit open, service not found, any error. Instead of propagating the error to the user, you return a default response.

Fallback can be defined at the call level:

actions: {
    async getDashboard(ctx) {
        const result = await ctx.call("recommendations.get", {
            userId: ctx.params.userId
        }, {
            // If recommendations service fails for any reason,
            // return this instead of throwing an error
            fallbackResponse: {
                recommendations: [],
                message: "Recommendations unavailable right now"
            }
        });
        return result;
    }
}

Fallback can also be a function:

const result = await ctx.call("recommendations.get", {
    userId: ctx.params.userId
}, {
    fallbackResponse(ctx, err) {
        this.logger.warn(`Recommendations failed: ${err.message}`);
        return {
            recommendations: [],
            message: "Showing default recommendations"
        };
    }
});

Use fallback for non-critical features. Recommendations, personalization, analytics — these are nice to have but your app should work without them.


Putting It All Together — Production-Ready Config

Here is a realistic moleculer.config.js for a production application with all fault tolerance features enabled:

"use strict";

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

    logLevel: "warn",

    transporter: "nats://localhost:4222",

    // Global timeout — 10 seconds
    requestTimeout: 10 * 1000,

    // Retry policy — retry up to 3 times with exponential backoff
    retryPolicy: {
        enabled: true,
        retries: 3,
        delay: 100,
        maxDelay: 2000,
        factor: 2,
        check: err => err && !!err.retryable
    },

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

    // Bulkhead — limit concurrent calls per service
    bulkhead: {
        enabled: true,
        concurrency: 10,
        maxQueueSize: 100
    },

    // Load balancing
    registry: {
        strategy: "RoundRobin",
        preferLocal: true
    }
};

When to Use Each Mechanism

Timeout         — Always. Set a global timeout. Every call should have a limit.

Retry           — For network calls, external APIs, temporary failures.
                  Not for payment processing or any non-idempotent operation.

Circuit Breaker — Always in production. Protects healthy services from
                  being dragged down by broken ones.

Bulkhead        — For resource-heavy operations like report generation,
                  file processing, or calls to slow external services.

Fallback        — For non-critical features. Recommendations, analytics,
                  personalization. Your app should work without them.

Summary

  • Fault tolerance is not optional in production microservices. Things will break.
  • Timeout prevents your app from waiting forever. Set a global timeout always.
  • Retry automatically retries failed calls. Use exponential backoff. Be careful with non-idempotent operations.
  • Circuit Breaker monitors failure rates. When too many fail, it opens and rejects calls instantly. This protects healthy services from broken ones.
  • Circuit has three states: Closed (normal), Open (blocking), Half-Open (testing recovery).
  • Bulkhead limits concurrent calls to prevent resource exhaustion.
  • Fallback provides a default response when everything else fails.
  • Configure all four in moleculer.config.js for global behavior.
  • Override per-call or per-action when specific operations need different limits.

Up Next

Post 9 covers Caching — one of the easiest wins for performance in Moleculer. Built-in caching with zero extra code on most actions. We will cover memory caching, Redis caching, cache keys, TTL, and how to invalidate cache when data changes.


Course Progress: 8 of 15 posts complete.

Running PHP in the Terminal — No Browser Required

Most developers discover PHP through XAMPP, a local server, or a tutorial that immediately sets up Apache and a browser preview. So it's...