Microservices Crash Course & Intro To Moleculer JS

javascript Apr 02, 2024

In this article, we're going to dive into microservices and talk about what they are, why they're important, and how they can be used to build scalable applications. We'll also look at some other architectures such as monolith and service oriented or SOA. Then we're going to build a simple project that uses microservices with Node.js and a framework called Moleculer.

You can find the video version of this article [here](https://youtu.be/fEDT4lWWe9g) and the source code [here](https://github.com/bradtraversy/microservices-example).

Monolith Architecture

Before we look into the microservices architecture, I think it's a good idea to look at the monolith or monolithic architecture. This is probably what you're used to, as it's the original architecture and the usual stepping stone for most developers.

With the monolithic architecture, the entire application is built as a single unit, and all the code is tightly coupled together. This makes it easy to develop, test, and deploy the application. With a monolithic architecture, developers have a clear understanding of the application's structure and functionality, and making changes is straightforward since everything is in one place

It's also cheaper in most cases because you have a relatively simple infrastructure, as the application is a monolith. Since all application components are bundled together, there's less overhead in managing multiple services, databases, communication channels, and so on.

These applications are typically developed and run with a single tech stack, whatever that may be. This also simplifies the process of development, maintenance, and deployment.

Problems can start to arise when your codebase expands. As your monolithic application grows, it can become difficult to scale and maintain, and you can run into many limitations. Again, this is for really large applications. You're not going to use microservices for your to-do app.

If you're building applications by yourself or with a small team, chances are most of your projects will use this structure, but if you're working on larger enterprise level projects, you may find that microservices are a better fit, which we'll talk about next.

I should also mention that this is a very simplistic overview of monolithic architecture. There are specific design patterns within this structure such as MVC, client-server, the layered pattern, etc.

What Are Microservices?

So we know what monolith architecture is, now let's talk about microservices.

In a nutshell, they structure an application as a collection of small, loosely coupled services that focus on specific business functions.

Each service is self-contained and can be developed, deployed, and scaled independently. For instance, you may have a service that handles user authentication, another one that handles user profiles, and another service that handles the product catalog. Each service is responsible for a specific domain or functionality within the application.

These microservices communicate with each other over a network using protocols like HTTP. When you scale a microservices application, you can scale individual services independently based on their load. This allows you to optimize resource usage and improve performance. You can use something called a load balancer to distribute incoming requests to the different services and distribute the load evenly. I probably should have included a load balancer in the diagram.

Microservices are not tied to any specific technology or programming language. You can use anything you want. For instance, you may have a service that requires high throughput, so you can use a language like Go, which is known for its performance. Another service may require complex business logic, so you can use a language like Python, known for its readability. You could use Node.js to build a service that requires real-time communication, as it's known for its event-driven architecture. And these could all be part of the same application.

Some of the downsides of microservices are that they add a lot of complexity to an application that would be completely unnecessary for a small project. There are complexities in terms of infrastructure, deployment, and communication between services.

Microservices also require more infrastructure to manage and can be more expensive to run. Many operational practices are overkill for smaller projects. DevOps tools and practices are crucial in the infrastructure for microservices.

As I mentioned earlier, they're not always the right tool for the job. In fact, they are rarely the right tool for the job if you're at a beginner or intermediate level. But as you grow and work on larger projects, they can be powerful tools for building scalable applications.

Deployment

Regarding deployment, monolithic applications are deployed as a single unit. The infrastructure is much simpler and so is the process, but it can be problematic with really large projects. If you only need to update a small part of the application, you must still redeploy the entire thing.

With microservices, you can deploy each service independently, which makes it easier to scale and maintain the application. You can also use containerization technologies like Docker to package each service with its dependencies, which makes it easy to deploy and run the services in any environment.

APIs

When it comes to microservices, APIs define the contract or interface through which services interact, specifying the endpoints, data formats, and protocols used for communication.

The API gateway is a common pattern used in microservices architectures. It acts as a single entry point for all client requests and routes them to the appropriate services. It can also handle authentication, rate limiting, caching, and other stuff.

Microservices can evolve independently without affecting other services, as long as they adhere to the agreed-upon API contract.

I'm sure that many of you have experience working with APIs. For instance, when you make a request to a REST API, you use the HTTP protocol to send a request to a server, and the server sends a response back in a specific data format like JSON or XML. This is how microservices communicate with each other.

I've done many projects where we create an API or backend with something like Node.js or Python and then a frontend with something like React or Angular. Usually, we deploy the frontend and backend together, which makes it a monolithic application. If you deploy your backend completely separate from your frontend, then your backend API is essentially a service. It can be considered a service in the context of a service-oriented architecture (SOA), which I'm actually going to talk about next.

To make something like that a microservices architecture, you would break the backend API down into smaller services like an authentication service, a user service, and a product service. Each one would be responsible for a specific domain or functionality within the application.

Service Oriented Architecture (SOA)

There's a third architecture pattern that I think is relevant to talk about and that is SOA or Service Oriented Architecture. SOA was coined in the late 90s, early 2000s and it's often compared to the microservices architecture in that it structures an application as a collection of services. These services then communicate with each other usually with something called an enterprise service bus (ESB) which handles message routing, applying business rules and so on.

There are some key differences between SOA and microservices:

  • Communication: Microservices can communicate with each other over a network using protocols like HTTP, whereas SOA services communicate with each other using middleware like an Enterprise Service Bus (ESB). This makes more complex and makes it a single point of failure as you can see in the diagram. If the ESB fails, all SOA services will be impacted whereas with microservices, if one service fails, it doesn't bring down the entire application.

  • Granularity: Microservices are more fine-grained than SOA services. Each microservice is responsible for a specific domain or functionality within the application, whereas SOA services are more coarse-grained and can be responsible for multiple domains or functionalities.

    • To give you a practical example, using SOA, you may have an inventory management service for an ecommerce application. But the microservices approach would to break the inventory management service down into smaller services like an availability checking service, fulfillment service, and accounting service. So everything is more fine-grained.
  • Data Management: Microservices are responsible for their own data management, whereas SOA services can share data through a common data store, as you can see in the diagram.

There are some other differences, but these are some of the main ones. SOA was popular in the early 2000s, but it fell out of favor because it was too complex and difficult to implement. Microservices are a more lightweight and flexible approach to building applications.

Pros & Cons?

We already went over a lot of this, but just to summarize, these are some pros and cons to using microservices:

Pros

  1. Scalability: As I mentioned, you can scale individual services independently based on their load. This allows you to optimize resource usage and improve performance.

  2. Flexibility: You can use different technologies for different services based on their requirements. For instance, you may have a service that requires high throughput, so you can use a language like Go, which is known for its performance. Another service may require complex business logic, so you can use a language like Python, known for its readability.

  3. Resilience: If one service fails, it doesn't affect the entire application. The other services can continue to function, and you can implement retry and circuit breaker patterns to handle failures gracefully.

  4. Modular and Decentralized Architecture: Microservices promote a modular architecture, where complex applications are broken down into smaller, manageable components and facilitates easier collaboration, testing, and deployment.

  5. Team Autonomy: Different teams can develop, deploy, and scale each service independently. This allows you to scale your development efforts and improve productivity.

Cons

  1. Complexity: There is quite a bit of additional complexity in terms of deployment, monitoring, and management. Coordinating communication between multiple services, managing distributed data and so on.

  2. Operational Overhead: Managing a large number of microservices requires robust infrastructure and DevOps practices. Tasks such as service discovery, load balancing, logging, monitoring, and security can become more complex and resource-intensive.

  3. Data Management Complexity: Microservices often have their own databases or data stores, which can lead to data duplication, inconsistency, and synchronization issues.

  4. Increased Development Time: Breaking down a big program into smaller ones called microservices takes more time and effort than just making one big program. It's like splitting up a big job into many smaller tasks – it needs more planning and coordination.

  5. Debugging and Troubleshooting:: Identifying and diagnosing issues in a distributed microservices architecture can be more challenging as well.

So basically these can pretty much be summed up into complexity and cost.

Alright, so that's my introduction to microservices. Now let's look at creating a couple simple microservices using Node.js and a framework called Moleculer.

Building A Microservices Project

I've mentioned it a few times, but you can use whatever technology you want to build microservices. Node.js is a popular option and that's what I'm most familiar with, so that's what we're going to use for this project. You could just use the Express framework to build your services, but we're going to use a framework called Molecular.

Moleculer is a fast and powerful microservices framework for Node.js. It helps you build efficient, reliable, and scalable services. Some crucial things need to be set in place to build powerful microservices that Moleculer includes by default, and I just want to touch on some of those things.

  1. Load Balancer: We talked a little bit about lload balancing. This is the process of distributing incoming network traffic across multiple servers or computing resources to optimize performance. Moleculer includes a load balancer by default. If you were using Express, you would typically need to set this up using a reverse proxy server with NGINX or HAProxy.
  2. Fault Tolerance: The ability of a system to continue operating and providing services in the presence of failures or errors. Moleculer has built in fault tolerance.
  3. Service Discovery: Mechanism that allows services in a distributed system dynamically locate and communicate with each other. It enables services to discover the network locations (e.g., IP addresses and ports) of other services. Moleculer maintains a local service registry within each node (microservice instance) in the cluster. When a service starts up, it registers itself with the local registry, providing information such as its name, version, available actions, and node address.

As you can see, Moleculer offers many advantages for building microservices out of the box. You can learn more about it here.

Getting Started

Let's start by creating a new project. Create a new directory and run npm init -y to create a new package.json file. Then install Moleculer by running npm install moleculer.

Create a new file called index.js. This will be our main entry point for the project.

I want to create a few different services, but let's start with something very simple. We'll have a greeter service that will return a greeting message. Here's what the code looks like:

import { ServiceBroker } from 'moleculer';

const broker = new ServiceBroker();

// Greeter service
broker.createService({
  name: 'greeter',
  actions: {
    sayHello(ctx) {
      return `Hello ${ctx.params.name}`;
    },
  },
});

// Start the broker
async function startApp( ) {
  await broker.start();
  const res = await broker.call('greeter.sayHello', { name: 'John' });
  console.log(res);
  broker.stop();
}

startApp();

We start by bringing in the ServiceBroker class and then initialize a broker. A service broker is the heart of Moleculer. It handles the management and communication between services (local and remote).

We then create a service using the createService method. We give the service a name of greeter and define an action called sayHello. The ctx param stands for context and it represents the context object passed to service actions and event handlers. We then return a message that says hello to the name that's passed in.

Finally, we start the broker in an async function called startApp() and call the sayHello action on the greeter service with the name John. We then log the result to the console.

This is probably the simplest microservice you could create, but it gives you an idea of how to use a broker to create a service. You can start and stop the broker, create services, and call actions on those services.

User Service

Let's create a user service that handles things like creating and fetching users. This time we will put the service in its own file. Create a new folder called services and a file called user.service.js and add the following code:

import { ServiceBroker } from 'moleculer';

const broker = new ServiceBroker();

// Function to generate a random user ID
function generateUserId() {
  return Math.random().toString(36).substr(2, 9);
}

// Initialize the users array
const users = [];

broker.createService({
  name: 'user',
  actions: {
    // Action to create a new user
    async createUser(ctx) {
      const { username, email } = ctx.params;
      const newUser = { id: generateUserId(), username, email };
      users.push(newUser); // Push the new user to the users array
      return newUser;
    },

    // Action to retrieve all users
    async getUsers(ctx) {
      return users; // Return the users array
    },
  },
});

export default broker;

This service is a little more complex than the greeter service. We have a function called generateUserId that generates a random user ID. We then initialize an array called users that will hold all of our users.

We then create a service called user with two actions. The first action is createUser which takes a username and email as parameters and creates a new user object with a random ID. We then push the new user to the users array and return the new user.

This is still pretty simple, but it is more realistic than the greeter service.

Let's now bring in the user service in our index.js file and call the actions. Here's what the updated index.js file looks like:

import UserService from './services/user.service.js';

async function startApp( ) {
  // Start the services
  await UserService.start();

  try {
    // Simulate user creation
    const newUser = await UserService.call('user.createUser', {
      username: 'john_doe',
      email: '[email protected]',
    });
    console.log('New user created:', newUser);

    // Fetch all users
    const users = await UserService.call('user.getUsers');
    console.log('All users:', users);
  } catch (error) {
    console.error('Error occurred:', error.message);
  } finally {
    // Stop the services when finished
    await UserService.stop();
  }
}

startApp();

We import the UserService from the user.service.js file and then start the service in the startApp function. We then call the createUser action to create a new user and log the result to the console. We then call the getUsers action to fetch all users and log the result to the console.

Run the index.js file with node index.js and you should see the new user created and all users logged to the console.

Email Service

Lets create a service that will simulate sending an email to a user. Create a new file called email.service.js in the services folder and add the following code:

import { ServiceBroker } from 'moleculer';

const broker = new ServiceBroker();

broker.createService({
  name: 'email',
  actions: {
    // Action to send an email
    async sendEmail(ctx) {
      const { recipient, subject, content } = ctx.params;
      // Simulated email sending logic
      console.log(`Sending email to ${recipient} with subject: ${subject}`);
      console.log(`Content: ${content}`);
      return `Email sent to ${recipient}`;
    },
  },
});

export default broker;

This service is very simple. We have an action called sendEmail that takes a recipient, subject, and content as parameters. We then log the recipient, subject, and content to the console and return a message saying that the email was sent.

Let's bring in the email service in our index.js file and call the action. Here's what the updated index.js file looks like:

import UserService from './services/user.service.js';
import EmailService from './services/email.service.js';

async function startApp( ) {
  // Start the services
  await UserService.start();
  await EmailService.start();

  try {
    // Simulate user creation
    const newUser = await UserService.call('user.createUser', {
      username: 'john_doe',
      email: '[email protected]',
    });
    console.log('New user created:', newUser);
    const users = await UserService.call('user.getUsers');
    console.log('All users:', users);

    // Simulate sending an email
    const emailResult = await EmailService.call('email.sendEmail', {
      recipient: newUser.email,
      subject: 'Welcome to our platform!',
      content: 'Thank you for signing up!',
    });
    console.log(emailResult);
  } catch (error) {
    console.error('Error occurred:', error.message);
  } finally {
    // Stop the services when finished
    await UserService.stop();
    await EmailService.stop();
  }
}

startApp();

We are bringing in the email service and starting it along with the user service. We then call the sendEmail action to simulate sending an email to the new user with a welcome message.

You should see the simulated email being sent to the new user when you run the index.js file.

Auth Service

We'll create one more service that will handle user authentication. Create a new file called auth.service.js in the services folder and add the following code:

import { ServiceBroker } from 'moleculer';

const broker = new ServiceBroker();

broker.createService({
  name: 'auth',
  actions: {
    // Action to authenticate a user
    async authenticateUser(ctx) {
      const { username, password } = ctx.params;
      // Simulated authentication logic
      if (username === 'admin' && password === 'password') {
        return { success: true, message: 'Authentication successful' };
      } else {
        return { success: false, message: 'Authentication failed' };
      }
    },
  },
});

export default broker;

This service has an action called authenticateUser that takes a username and password as parameters. We then simulate an authentication logic where if the username is admin and the password is password, we return a success message, otherwise we return a failure message.

Let's bring in the auth service in our index.js file and call the action. Here's what the updated index.js file looks like:

import UserService from './services/user.service.js';
import EmailService from './services/email.service.js';
import AuthService from './services/auth.service.js';

async function startApp( ) {
  // Start the services
  await UserService.start();
  await EmailService.start();
  await AuthService.start();

  try {
    // Simulate user creation
    const newUser = await UserService.call('user.createUser', {
      username: 'john_doe',
      email: '[email protected]',
    });
    console.log('New user created:', newUser);
    const users = await UserService.call('user.getUsers');
    console.log('All users:', users);

    // Simulate sending an email
    const emailResult = await EmailService.call('email.sendEmail', {
      recipient: newUser.email,
      subject: 'Welcome to our platform!',
      content: 'Thank you for signing up!',
    });
    console.log(emailResult);

    // Simulate user authentication
    const authResult = await AuthService.call('auth.authenticateUser', {
      username: 'john',
      password: 'password',
    });
    console.log('Authentication result:', authResult);
  } catch (error) {
    console.error('Error occurred:', error.message);
  } finally {
    // Stop the services when finished
    await UserService.stop();
    await EmailService.stop();
    await AuthService.stop();
  }
}

startApp();

We are bringing in the auth service and starting it with the user and email services. We then call the authenticateUser action to simulate authenticating a user with a username of john and a password of password.

You should see a fail message. Change the username to admin and you should see a success message.

As you can see, all of these services are independent and can be developed, deployed, and scaled independently. This is the power of microservices. To deploy these services in a production environment, you would typically use a containerization technology like Docker and an orchestration tool like Kubernetes. Obviously, that is beyond the scope of this tutorial, but it's something that you could look into if you are interested in deploying microservices.

Stay connected with news and updates!

Join our mailing list to receive the latest news and updates from our team.
Don't worry, your information will not be shared.

We hate SPAM. We will never sell your information, for any reason.