Phase 9 - Final Post | Post 15 | What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead

Post 15 of 15 | Final Post


What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead

You have completed the core Moleculer course. You can build a full microservices backend, containerize it, add fault tolerance, caching, authentication, observability, and deploy it with Docker Compose. That puts you ahead of most Node.js developers.

This final post covers what comes after. The topics here are not small additions. Each one is a significant skill area. Think of this post as your roadmap for the next six to twelve months.


Topic 1: Testing Moleculer Services

Testing is the first thing you should learn after completing this course. Untested microservices are dangerous because failures in one service cascade into others in unexpected ways.

Moleculer has excellent testing support built in.

Unit Testing Actions

For unit testing individual actions, you do not need a running broker. You test the handler function in isolation.

Install Jest:

npm install --save-dev jest

Create a test file:

tests/unit/user.service.test.js

"use strict";

const { ServiceBroker } = require("moleculer");
const UserService = require("../../services/user.service");

describe("User Service", () => {
    let broker;

    // Create a broker before all tests
    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });
        broker.createService(UserService);
        await broker.start();
    });

    // Stop broker after all tests
    afterAll(async () => {
        await broker.stop();
    });

    describe("user.login action", () => {
        it("should return a token on valid credentials", async () => {
            const result = await broker.call("user.login", {
                email: "rahul@example.com",
                password: "password123"
            });

            expect(result).toHaveProperty("message", "Login successful");
            expect(result).toHaveProperty("user");
            expect(result.user).not.toHaveProperty("password");
        });

        it("should throw error on invalid credentials", async () => {
            await expect(
                broker.call("user.login", {
                    email: "wrong@example.com",
                    password: "wrongpassword"
                })
            ).rejects.toThrow("Invalid email or password");
        });

        it("should throw validation error when email is missing", async () => {
            await expect(
                broker.call("user.login", {
                    password: "password123"
                })
            ).rejects.toThrow();
        });
    });
});

Integration Testing — Multiple Services

Integration tests test how services communicate with each other:

"use strict";

const { ServiceBroker } = require("moleculer");
const UserService = require("../../services/user.service");
const OrderService = require("../../services/order.service");
const ProductService = require("../../services/product.service");

describe("Order Flow Integration", () => {
    let broker;

    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });

        // Load all services into one broker for integration testing
        broker.createService(UserService);
        broker.createService(OrderService);
        broker.createService(ProductService);

        await broker.start();
    });

    afterAll(async () => {
        await broker.stop();
    });

    it("should create an order and reduce product stock", async () => {
        // Step 1: Create a product
        const product = await broker.call("product.create", {
            name: "Test Keyboard",
            price: 1000,
            stock: 10
        });

        // Step 2: Create an order
        const order = await broker.call("order.create", {
            productId: product._id.toString(),
            quantity: 2
        }, {
            meta: { user: { id: "user-1", role: "user" } }
        });

        expect(order).toHaveProperty("status", "confirmed");
        expect(order).toHaveProperty("totalPrice", 2000);

        // Step 3: Verify stock reduced
        const updatedProduct = await broker.call("product.get", {
            id: product._id.toString()
        });

        expect(updatedProduct.stock).toBe(8);
    });
});

Mocking Services in Tests

When you want to test one service in isolation without its dependencies, mock the dependency:

describe("Order Service with mocked User Service", () => {
    let broker;

    beforeAll(async () => {
        broker = new ServiceBroker({ logger: false });

        // Mock the user service — return fake data
        broker.createService({
            name: "user",
            actions: {
                get() {
                    return { id: "1", name: "Test User", email: "test@test.com" };
                }
            }
        });

        broker.createService(OrderService);
        await broker.start();
    });

    afterAll(() => broker.stop());

    it("should create order using mocked user", async () => {
        const order = await broker.call("order.create", {
            productId: "product-1",
            quantity: 1
        }, {
            meta: { user: { id: "1" } }
        });

        expect(order).toBeDefined();
    });
});

Add test script to package.json:

"scripts": {
    "test": "jest --testEnvironment=node",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
}

Topic 2: The Saga Pattern — Distributed Transactions

This is one of the most important advanced topics in microservices. Understand the problem first.

In a monolith, if you create an order and charge a credit card in the same function, and the charge fails, you can roll back the entire database transaction. Both operations either succeed or both fail. This is called an ACID transaction.

In microservices, the order is saved in the order service database and the payment is charged in the payment service database. They are separate processes, separate databases. If the payment fails after the order was saved, there is no global rollback. You now have an order in the database with no corresponding payment. Data is inconsistent.

The Saga pattern solves this. A saga is a sequence of local transactions. If any step fails, the saga runs compensating transactions to undo the previous steps.

There are two types of sagas:

Choreography-based Saga — Services communicate through events. No central coordinator. Each service knows what to do when it receives an event and what to do if something goes wrong.

order.service     → emits "order.created"
payment.service   → listens, charges card, emits "payment.completed" or "payment.failed"
order.service     → listens to "payment.completed" → updates order to "confirmed"
order.service     → listens to "payment.failed"    → updates order to "cancelled"
inventory.service → listens to "order.confirmed"   → reduces stock

Orchestration-based Saga — A central saga orchestrator calls each step and handles failures:

// saga/order.saga.js
module.exports = {
    name: "order-saga",

    actions: {
        async start(ctx) {
            const { userId, productId, quantity } = ctx.params;

            let orderId = null;

            try {
                // Step 1: Create order
                const order = await ctx.call("order.create", {
                    userId, productId, quantity
                });
                orderId = order._id;

                // Step 2: Charge payment
                await ctx.call("payment.charge", {
                    userId,
                    amount: order.totalPrice,
                    orderId
                });

                // Step 3: Reduce stock
                await ctx.call("product.reduceStock", {
                    id: productId,
                    quantity
                });

                // All steps succeeded
                await ctx.call("order.updateStatus", {
                    id: orderId,
                    status: "confirmed"
                });

                return { success: true, orderId };

            } catch (err) {
                this.logger.error("Saga failed, running compensation", err);

                // Compensating transaction — undo what was done
                if (orderId) {
                    await ctx.call("order.updateStatus", {
                        id: orderId,
                        status: "cancelled"
                    });

                    // If payment was charged, refund it
                    await ctx.call("payment.refund", {
                        orderId
                    }).catch(e => this.logger.error("Refund failed", e));
                }

                throw err;
            }
        }
    }
};

Moleculer has an official package called moleculer-workflows that provides a more structured saga implementation. Look it up at github.com/moleculerjs/moleculer-workflows when you are ready for this.


Topic 3: Advanced moleculer-db — Populates and Hooks

You used basic moleculer-db in Post 11. Two features you will need in real projects:

Populates — Joining Data Across Services

Instead of manually calling user service from order service to get user details, moleculer-db can auto-populate related data:

module.exports = {
    name: "order",
    mixins: [DbMixin],
    model: OrderModel,

    settings: {
        populates: {
            // When listing orders, automatically fetch user details
            user: {
                action: "user.get",
                params: {
                    fields: ["name", "email"]
                }
            },

            // Automatically fetch product details
            product: {
                action: "product.get",
                params: {
                    fields: ["name", "price"]
                }
            }
        }
    },

    actions: {
        list: {
            rest: { method: "GET", path: "/" },
            params: {
                // Client can request population
                populate: { type: "array", items: "string", optional: true }
            }
        }
    }
};

Call with populate:

GET /api/order?populate[]=user&populate[]=product

Response:

{
    "rows": [
        {
            "_id": "order-1",
            "userId": "user-1",
            "user": { "name": "Rahul Sharma", "email": "rahul@example.com" },
            "productId": "product-1",
            "product": { "name": "Mechanical Keyboard", "price": 2999 },
            "quantity": 2,
            "totalPrice": 5998
        }
    ]
}

Entity Hooks — Before and After Database Operations

module.exports = {
    name: "product",
    mixins: [DbMixin],

    hooks: {
        before: {
            // Runs before every create action
            create(ctx) {
                // Normalize data before saving
                ctx.params.name = ctx.params.name.trim();
                ctx.params.slug = ctx.params.name.toLowerCase().replace(/\s+/g, "-");
            }
        },
        after: {
            // Runs after every get action
            get(ctx, res) {
                // Transform data before returning
                if (res) {
                    res.priceFormatted = `Rs. ${res.price.toLocaleString()}`;
                }
                return res;
            }
        }
    }
};

Topic 4: Moleculer Middlewares

Middlewares in Moleculer are different from Express middlewares. They intercept action calls, event emissions, and broker lifecycle events globally. They are the correct way to add cross-cutting behavior like logging every action call, adding rate limiting, or transforming responses globally.

// middlewares/logger.middleware.js
module.exports = {
    name: "ActionLogger",

    // Wraps every action call
    localAction(next, action) {
        return async function(ctx) {
            const start = Date.now();

            try {
                const result = await next(ctx);
                const duration = Date.now() - start;

                ctx.broker.logger.info(`Action ${action.name} completed in ${duration}ms`);

                return result;
            } catch (err) {
                const duration = Date.now() - start;
                ctx.broker.logger.error(`Action ${action.name} failed after ${duration}ms`, err.message);
                throw err;
            }
        };
    }
};

Register in moleculer.config.js:

const ActionLogger = require("./middlewares/logger.middleware");

module.exports = {
    middlewares: [ActionLogger]
};

Now every action call in every service is automatically logged with duration. Zero changes to service code.


Topic 5: Kubernetes — Beyond Docker Compose

Docker Compose is excellent for development and small production deployments. For large scale production, Kubernetes is the standard.

Kubernetes gives you:

  • Automatic restart of crashed containers
  • Rolling deployments with zero downtime
  • Auto-scaling based on CPU or request count
  • Load balancing across pods
  • Health checks and readiness probes
  • Secrets management

A basic Kubernetes deployment for your user service:

k8s/user-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3              # Run 3 instances
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
        - name: user-service
          image: your-registry/ecommerce:latest
          env:
            - name: SERVICES
              value: "user"
            - name: MONGO_URI
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: mongo-uri
            - name: REDIS_URI
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: redis-uri
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10

The path from Docker Compose to Kubernetes is well documented. When you are ready, look at:

  • Minikube for running Kubernetes locally
  • Helm for packaging Kubernetes applications
  • Google Kubernetes Engine or AWS EKS for managed Kubernetes in the cloud

Topic 6: Additional Official Moleculer Packages

There are many official and community packages that extend Moleculer. These are the most useful ones for real projects:

moleculer-mail — Send emails via various providers (Mailgun, SendGrid, SMTP):

npm install moleculer-mail

moleculer-bull — Background job queues using Bull and Redis. For long-running tasks that should not block the request:

npm install moleculer-bull

moleculer-cron — Scheduled jobs using cron expressions:

npm install moleculer-cron

moleculer-io — Socket.IO integration for real-time features:

npm install moleculer-io

moleculer-apollo-server — GraphQL support alongside REST:

npm install moleculer-apollo-server

moleculer-telegram — Send Telegram messages from services. Useful for production alerts:

npm install moleculer-telegram

Topic 7: TypeScript with Moleculer

Moleculer has first-class TypeScript support. If your team uses TypeScript, you can write strongly typed services:

npm install typescript @types/node
npm install --save-dev ts-node

A TypeScript service looks like this:

import { Service, ServiceSchema, Context } from "moleculer";

interface UserCreateParams {
    name: string;
    email: string;
    password: string;
}

interface UserResponse {
    id: string;
    name: string;
    email: string;
}

const UserService: ServiceSchema = {
    name: "user",

    actions: {
        create: {
            params: {
                name: "string",
                email: "email",
                password: { type: "string", min: 6 }
            },
            async handler(ctx: Context<UserCreateParams>): Promise<UserResponse> {
                const { name, email, password } = ctx.params;
                // TypeScript now knows exactly what fields are available
                return { id: "1", name, email };
            }
        }
    }
};

export default UserService;

TypeScript catches errors at compile time instead of runtime. For large teams and long-lived projects, this is extremely valuable.


Where to Find Help

When you are stuck, these are the best places to get answers:

Official Documentation

moleculer.services/docs/0.15 — Always read the official docs first. They are well written and kept up to date.

GitHub Repository

github.com/moleculerjs/moleculer — Read the source code, check existing issues, open new issues.

Discord Community

discord.gg/TSEcDRP — The Moleculer Discord has an active community. The maintainers themselves answer questions there.

Stack Overflow

Search for questions tagged with moleculer. Many common problems are already answered.

GitHub Discussions

github.com/moleculerjs/moleculer/discussions — For longer questions and architectural discussions.


Recommended Learning Path After This Course

Here is a structured path for the next six to twelve months based on what you have learned:

Month 1 and 2: Solidify what you learned

  • Add tests to the project you built in this course
  • Add the saga pattern to your order flow
  • Deploy your project to a real server using Docker Compose
  • Get comfortable with Jaeger traces and Prometheus metrics

Month 3 and 4: Go deeper into the ecosystem

  • Learn moleculer-bull for background jobs
  • Add GraphQL support with moleculer-apollo-server
  • Add WebSocket support with moleculer-io
  • Practice the populates feature of moleculer-db

Month 5 and 6: Production skills

  • Learn Kubernetes basics with Minikube
  • Convert your Docker Compose deployment to Kubernetes
  • Set up a proper CI/CD pipeline with GitHub Actions
  • Learn about service mesh concepts with Istio

Month 7 and beyond: Architecture

  • Study the twelve-factor app methodology at 12factor.net
  • Read about domain-driven design — it helps you decide how to split services
  • Study event sourcing and CQRS patterns
  • Look at other microservices frameworks to understand tradeoffs — NestJS microservices, Seneca

Final Thoughts

When you started this course you knew MERN and MEAN. You could build a monolith. You did not know how to think in services or how to handle the complexity that comes with distributed systems.

Now you understand:

  • Why microservices exist and when they are the right choice
  • How to design services with clear boundaries
  • How to make services communicate reliably
  • How to protect your system when things fail
  • How to observe what is happening inside your running application
  • How to package and deploy a complete microservices system

One important thing to remember as you go forward: microservices are not always the right choice. For a small team, a small application, or an early startup, a well-structured monolith is often better. Start with a monolith, identify the boundaries, and extract services when you have a real reason — a team needs to work independently, a specific component needs to scale differently, or a part needs a different technology.

Moleculer is powerful but it is a tool. Use it when it solves a real problem. The knowledge you have gained about distributed systems, fault tolerance, event-driven architecture, and observability applies far beyond Moleculer. These concepts work in any language, any framework, any cloud.

You are now equipped to build production-grade distributed systems with Node.js. That is a significant achievement.


Complete Course Summary — All 15 Posts

Phase 1 — Foundation
  Post 1:  What is microservices and why Moleculer
  Post 2:  Installation and first working service

Phase 2 — Core Concepts
  Post 3:  ServiceBroker and moleculer.config.js
  Post 4:  Services, Actions, validation, error handling
  Post 5:  Events, emit vs broadcast, wildcard listeners

Phase 3 — Communication
  Post 6:  Context object, requestID, meta, call chain
  Post 7:  Transporters, TCP, NATS, Redis, service discovery

Phase 4 — Fault Tolerance
  Post 8:  Timeout, Retry, Circuit Breaker, Bulkhead, Fallback

Phase 5 — Caching
  Post 9:  Memory and Redis caching, TTL, cache invalidation

Phase 6 — API Gateway
  Post 10: moleculer-web, authentication, authorization, JWT

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

Phase 8 — Observability
  Post 12: Logging, Prometheus metrics, Jaeger tracing

Phase 9 — Deployment
  Post 13: Docker and Docker Compose
  Post 14: Complete project review and best practices
  Post 15: Testing, Sagas, Kubernetes, what to learn next

Course Progress: 15 of 15 posts complete.

You have completed the Moleculer Microservices Course.

No comments:

Post a Comment

Phase 9 - Final Post | Post 15 | What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead

Post 15 of 15 | Final Post What to Learn Next — Testing, Advanced Patterns, Kubernetes, and the Road Ahead You have completed the core Mo...