Beginner

Node.js Microservices The Big Picture

To understand microservices, let's start with a concrete example: the GloboTicket application. It is a web application that allows customers to purchase tickets for different events such...

Senior software engineer with 8 years of experience (startups, Google, Amazon)


Table of Contents

  1. Course presentation
  2. Introduction to microservices
  1. Advantages and challenges of microservices
  1. Node.js and microservices: a perfect match
  1. Patterns and best practices for microservices

1. Course presentation

Welcome to this course Node.js Microservices: The Big Picture. The author is Julian Mateu, a senior software engineer with eight years of experience, from startups to large tech companies like Google and Amazon.

This course will teach you the concepts needed to navigate the world of microservices and design them with Node.js.

Why focus on microservices?

Microservices are at the heart of many dynamic and evolving systems. Understanding them allows you to build efficient, modular and maintainable software architectures. Node.js is the ideal tool to implement them.

Topics covered in this course

  • What are microservices, their benefits and challenges
  • Why Node.js is an ideal technology for implementing scalable and maintainable microservices
  • How to avoid common problems with design patterns and best practices

Prerequisites

After completing this course, you will have the skills to move from monolithic architecture to the popular microservices paradigm.

Before starting, it is advisable to be familiar with the fundamentals of web development: APIs, HTTP, client-server architecture, and a basic knowledge of JavaScript.

After this course, you will be able to delve into more specific courses on:

  • Monitoring and logging
  • Resilience and fault tolerance
  • Deployment and scalability

2. Introduction to microservices


2.1 Example — Meet GloboTicket

The monolithic architecture of GloboTicket

To understand microservices, let’s start with a concrete example: the GloboTicket application. It is a web application that allows customers to purchase tickets for different events such as concerts and conferences.

For several years, GloboTicket has used the simplest architecture possible: the monolith. A monolith is a single unit — imagine a program written in Node.js, Java, Python, or any other language, that:

  • Runs as a single process on a machine
  • Listen for user network requests
  • Usually stores data in a separate database
  • Can be scaled by launching more instances of the process

The limits of the monolith

Although deploying and testing a single application is simpler, developing and deploying the entire application as a single unit has significant limitations.

Even by rigorously applying best practices (SOLID principles, modularization), certain functionalities will inevitably extend over several modules, and the code will begin to couple. This is sometimes referred to as a “big ball of mud” — a huge, highly coupled code base without a clearly defined structure, where adding features becomes complex, slow and risky.

Problem 1 — Increasing complexity: Because the monolith is tightly coupled, the code tends to become complex. A new feature typically requires modifying multiple components and coordinating many teams. As all teams work on the same application, it is often unstable, with many new features added simultaneously. This leads to long development and release cycles, increasing the likelihood of bugs and testing costs.

Problem 2 — Lack of flexibility: Since the entire application is written in a single language and framework, any change, experiment or upgrade becomes difficult. The application is so large that even upgrading a library to a newer version can be a pain, and rewriting it from scratch would be impractical as new features constantly appear.

Problem 3 — Partial scalability not possible: Imagine that there is a memory bottleneck in one part of the application and a CPU bottleneck in another. The only way to fix this is to increase both CPU and memory for the entire application, which is more expensive than doing it separately.

Issue 4 — Lack of fault isolation: A bug in one component can bring down the entire application.

Migrating to microservices

All of these issues led the GloboTicket team to consider migrating to microservices. If we divide the application into a set of smaller, interconnected services, we obtain an architecture where different functionalities are managed by specific services:

ServicesResponsibility
API GatewaySingle entry point to the application
User ServiceUser profile, preferences, etc.
Order ServiceOrders, credit cards, payments
Service NotificationSending emails and notifications
Service CatalogInformation on events, artists, venues

This is called functional decomposition. Each part of the application performs a different function. This makes it a type of service-oriented architecture (SOA), but not all SOAs are microservices architectures.

By organizing themselves into independent teams, each owning a group of services, GloboTicket engineers can deliver features faster and with less risk because they only have to worry about their own service and providing a clear set of operations to the outside world.


2.2 Microservice definitions

There is no single definition of microservices. Here’s how leading experts in the field define them:

Adrian Cockcroft: “A service-oriented architecture composed of loosely coupled elements with a bounded context.” »

Martin Fowler: “An approach to developing a single application as a suite of small services, each running in its own process and communicating via lightweight mechanisms, often an HTTP API. These services are built around business capabilities and are independently deployable through fully automated deployment mechanisms. »

Chris Richardson: “An architectural style that functionally decomposes an application into a set of services. What matters is that each department has a focused and consistent set of responsibilities. »

Sam Newman: “Small, autonomous services that work together. »

The definitions differ and highlight different aspects. All these notions will be detailed later in the course.


2.3 Key Features of Microservices

The definitions mention several key characteristics:

Small size

The term “micro” is partly misleading, because small size is not the most important requirement. One of the main benefits of microservices is that they allow large organizations to work efficiently by organizing themselves into small, autonomous teams.

Some authors propose that a microservice be small enough to be rewritten from scratch in a few weeks. A more realistic requirement is that it should be easily manageable by a small team of around 8 people — what is sometimes called a “two-pizza team” (a team that can be fed with two pizzas).

Bounded context

This term comes from the book Domain-Driven Design by Eric Evans. The key idea is that the definitions of terms like “customer”, “order” or “account”, which constitute the domain models of our design, are truly context dependent.

For example, in GloboTicket, an order could potentially contain hundreds of items: credit card information, customer details, artist details, venue details. Using bounded context ensures that when the order service processes a payment, it only takes care of the order service’s information (credit card number), not the artist’s details. We thus avoid what is sometimes called a “god class”.

Loosely coupled

A service can change without affecting the others. This allows, for example, to rewrite part of a service or even the entire service without affecting its consumers.

High cohesion

The functionalities of a service must be closely related to each other. A department must have a single, well-defined responsibility.

Communication via APIs only

Services can only communicate via APIs. They cannot share data in database, memory or any other form. This guarantees the independence and autonomy of each service.


2.4 Battery life

Autonomy is one of the most important characteristics of microservices. Here are the elements that enable it:

Independent Deployability

Teams must be able to deploy changes to one service independently of all others. If they have to coordinate changes or respect a release schedule, the services are not really deployable independently, and therefore, we do not really use microservices.

No shared databases

Each service has its own database. So any schema change, migration or even a complete change from SQL to NoSQL only affects that particular service. It also allows teams to choose and manage each database independently of the others.

Communication only via APIs

Services cannot share data in the database, in memory, or in any other form.

The danger of the “distributed monolith”

Without these essential ingredients, teams risk creating a distributed monolith, which combines the worst of both worlds: all the disadvantages of monolithic architecture with all the complexity of distributed systems.

Concrete example: The instructor worked on a project where, despite claiming to use microservices, adding a simple Boolean column to a database table took weeks and required modifying thousands of lines of code across dozens of services. Worse still, after deploying the changes, numerous bugs appeared, requiring a rollback, and the product team had to wait two additional weeks to deliver this trivial change.


2.5 Why microservices?

Reminder of GloboTicket limits

GloboTicket faced several limitations due to the monolithic architecture:

  • Long development cycles
  • Lack of technological flexibility
  • Partial scalability not possible
  • No fault isolation

Microservices have emerged as a solution to all these problems. By breaking the large application into a set of smaller services, each service becomes more manageable, and the autonomy and loose coupling allow teams to work more efficiently.

The main benefit

The main benefit that microservices bring to organizations is that they allow them to operate as a large group of autonomous teams that can deploy their software independently of each other.

The cost to pay

This comes at a cost, as it undeniably increases the complexity of the system, bringing a set of problems and challenges associated with distributed systems. Each service now runs on a different machine, communicating over the network. Developers and architects must carefully design service coordination and manage consistency issues.

Who are microservices suitable for?

  • Not recommended: for a small startup in a garage with two friends
  • Recommended: for a large organization with dozens or even hundreds of teams

2.6 Key considerations

Microservices come with a set of practices and technologies closely associated with them:

Observability

With a monolith, it is easy to consult the logs and trace a bug in the code. But with microservices, dozens or even hundreds of different applications produce logs on different machines, and tracing becomes a challenge. System metrics also become central to debugging, understanding and preventing problems.

Tests

Testing a single application is generally simpler, as it can normally run on a developer’s laptop. Testing a distributed architecture is more difficult: integration between services, API contracts, load testing, performance characteristics, consistency issues, network outages or partial failures.

Continuous integration (CI/CD)

To truly leverage the autonomy and agility that microservices bring, teams need a continuous integration pipeline that allows them to deploy changes to production multiple times per day without having to coordinate releases with other teams. This significantly reduces the number of bugs and their impact because features are implemented incrementally and can be rolled back more easily with less impact to customers.

Virtualization

Virtualization technologies like Docker and Kubernetes really complement microservices, as well as a good DevOps culture. It’s so essential that even Martin Fowler included it in his definition of microservices.

Security

Securing a microservices application can be more difficult than securing a monolith, which also makes it a key concern for organizations.


2.7 Why Node.js?

Node.js is the perfect match for microservices for several reasons:

  • Lightweight: designed to be lightweight, allowing for efficient resource usage and fast startup times, which is ideal for scaling a microservice by deploying new instances
  • Non-blocking event-driven I/O model: allows you to manage a large number of concurrent connections
  • Single-threaded event loop architecture: and the use of asynchronous applications make Node.js very efficient by reducing response times
  • JavaScript: the same language as the front-end, with a huge developer community
  • npm: one of the most extensive package ecosystems, providing access to a wide range of libraries and modules

These characteristics will be explored in depth in a dedicated module.


3. Benefits and Challenges of Microservices


3.1 Advantages of microservices

Let us recall the main challenges of GloboTicket with their monolithic architecture:

  1. Lack of partial scalability
  2. Lack of technological flexibility
  3. Long development cycles
  4. Lack of fault isolation

Here’s how microservices solve each of these problems:

Advantage 1 — Independent scalability

Each service can be scaled independently. Imagine this scenario: GloboTicket is organizing ticket sales for a highly anticipated global concert. Traffic is expected to increase significantly.

  • With a monolith: the entire application would have to be scaled, consuming enormous resources
  • With microservices: we can scale only specific services that will be under heavy load (like the order service), without affecting the others

Advantage 2 — Technological flexibility

Each service can potentially be developed using a different technology. Imagine that GloboTicket initially built its platform in Java. She now wants to integrate a cutting-edge recommendation engine that would be better suited to Python.

  • In a monolithic architecture: it’s a difficult choice
  • In a microservices environment: they could easily add a new service in Python without disrupting the rest of the system

It’s not just a question of languages ​​— it’s also a question of frameworks. Perhaps one service benefits from Spring Boot’s rich feature set, while another requires Node.js for its event-driven architecture.

Practical note: In practice, companies often restrict the choices to a few languages ​​and frameworks, because it is difficult to get people to work on technologies they are not familiar with. But even if the whole company uses a single language, this flexibility allows, for example, to use different versions of libraries in each department.

Advantage 3 — Independent deployment and team autonomy

In a monolithic system like GloboTicket’s current one, deploying a small change often requires the entire application to be redeployed. With microservices architecture, each service is independent — it can be developed, deployed, and scaled separately from the rest.

Each team owns a different set of services, which accelerates feature development and allows teams to be more agile. If GloboTicket decides to update its ordering service to add a new recommendation engine, it can do so without impacting other parts of the system.

Autonomy also brings *ownership. When a team owns a service, they are responsible for it from start to finish: design, development, deployment and maintenance. This fosters a sense of responsibility and encourages teams to build a high-quality service.

This shift toward team autonomy and ownership can also have a profound impact on company culture: it empowers teams, boosts their morale, and often leads to greater job satisfaction.

Benefit 4 — Resilience through fault isolation

In GloboTicket’s current monolithic system, a failure in one component could potentially bring down the entire application. In a microservices architecture, each service operates independently.

For example, if the catalog service goes down, it will not directly affect ticket purchasing or user authentication. Each service is isolated, which ensures that failure in one system does not propagate into widespread outage.


3.2 Microservices Challenges

Microservices are not a silver bullet. Here are their main challenges:

Challenge 1 — More complex tests

In a monolithic architecture, one can generally start the entire application on their local computer to test interactions between components. With microservices, there are complex workflows where multiple services interact.

  • Integration tests: more difficult to set up and longer to run, generally requiring a separate test environment
  • End-to-end testing: GloboTicket should simulate the complete ticket purchasing process, from login to payment to ticket issuance. We no longer check a single code base, we test an entire ecosystem of services
  • Realistic test environments: service dependencies, databases and even network latency must be reproduced
  • Resource-intensive tests: unlike a monolith where a single test suite may be sufficient, several suites should be maintained for each service

Challenge 2 — Complex deployment

Each service can be deployed independently, but a single change can impact other services. Even the best architects can’t predict every change the business will require, and sometimes features will require the coordination of multiple departments.

Delicate points include:

  • Backward compatibility of APIs: APIs must remain backward compatible so that all consumers can continue to communicate with new versions
  • API versioning: a robust strategy is necessary to manage breaking changes
  • Rollbacks: significantly more complicated in a microservices setup, requiring rollbacks to be coordinated between several services
  • Multiple CI/CD pipelines: GloboTicket will likely have a different pipeline for each microservice

Challenge 3 — Complexity of distributed systems

Engineers now have to deal with a distributed system where multiple services communicate over an unreliable network. Packets can get lost, arrive out of order, services can go down, become unresponsive, and even the network can be partitioned into disconnected portions for a period of time.

Challenge 4 — Increased complexity

Moving to microservices means moving from a single codebase to potentially dozens or even hundreds of smaller services. While this improves modularity and scalability, it also increases complexity exponentially.

Each service now has:

  • Its own database
  • Its own set of dependencies
  • Its own development and deployment pipeline

Microservices architecture requires a high level of architectural maturity. Questions need to be answered like: How will the services communicate? How will data consistency be maintained? How to guarantee transactional integrity across multiple services?

Challenge 5 — Observability

In a monolithic architecture, monitoring, logging and tracing are centralized. With microservices, each service can generate its own metrics and logs. The monitoring configuration should be distributed across multiple services, each potentially running in its own environment.

Tracing an end-to-end transaction (from ticket purchase to GloboTicket to payment) that passes through multiple microservices becomes important for debugging and performance.

Challenge 6 — CAP theorem and eventual consistency

The CAP theorem states that in a distributed system, only two of the following three properties can be guaranteed simultaneously:

PropertyDescription
ConsistencyAll reads receive the most recent write
AvailabilityAll queries are answered
Partition ToleranceSystem continues to work despite network partitions

For example, if we prioritize consistency and partition tolerance, we may have to sacrifice availability.


3.3 Example of the CAP theorem

The problem in GloboTicket

In a monolithic configuration, consistency is relatively simple because there is usually a single centralized database where ACID transactions are possible. ACID stands for: Atomic, Consistent, Isolated, Durable.

In a microservices system, ensuring data consistency across multiple services and databases becomes a complex task. For example, if someone buys the last ticket to a concert, this information must be quickly propagated to all relevant departments to avoid overselling.

The problem with database replicas

In a distributed database system, you don’t just have a single copy of the data — you have multiple replicas, often spread across different servers or even geographic locations. This is done for fault tolerance, load balancing and improving data availability.

Network partition scenario:

Let’s imagine that a network partition occurs, separating these replicas into isolated subnets. If a client writes data to a set of replicas, how can we ensure that this writing is consistent across all replicas when some are inaccessible?

We face a dilemma:

  • If we aim for consistency: we refuse the write until we can update all the replicas, making the system unavailable during this time
  • If we aim for availability: we accept writing on accessible replicas, but the system is now in an inconsistent state until the partition is resolved

Likewise for readings during a partition:

  • Consistency: we refuse the read request until we can guarantee that it reflects the most recent write → system unavailable
  • Availability: we serve the data from the closest accessible replica, knowing that it may not reflect the most recent write → compromised consistency

Eventual consistency (eventual consistency)

GloboTicket could adopt an *eventually consistent model. This means that although data may be inconsistent for a brief moment, it will eventually become consistent across all services. Other patterns to address these limitations will be explored in a later module.


3.4 Hidden costs of microservices

Microservices may have hidden costs that the GloboTicket team will need to be aware of. These costs are often overlooked, but equally crucial:

Infrastructure costs

Although microservices can be more efficient in using system resources, the need for more powerful observability and monitoring tools, databases for each service, and other supporting services can drive up infrastructure costs.

Tools and platforms

Specialized tools for continuous integration, continuous deployment, API gateways, service meshes, etc. are not cheap. GloboTicket will need comprehensive tooling to effectively manage its new architecture, which may represent a substantial investment.

Development and maintenance costs

Despite the benefits of smaller codebases and team autonomy, maintaining multiple codebases, libraries, and databases for each service can be a daunting task. This may slow down the GloboTicket development cycle and increase costs in terms of developer time.

Complexity costs

Increased complexity is a cost in itself. GloboTicket will need expertise in distributed systems, and that expertise comes at a price. The cognitive load on developers and the organization as a whole should not be underestimated.


3.5 When to adopt microservices?

Microservices are not a silver bullet. They come with their own challenges and costs. Here are the factors to consider:

Project needs

Each project has its own unique requirements. Whether it’s legacy systems, specific compliance needs, or team skills — each factor will influence the best architectural path.

Timing

For GloboTicket, the time had come for change. The growth and demands of their user base required a more robust and scalable architecture. But for a new startup or small team, a monolithic approach might be more appropriate, at least initially.

Long-term implications

Moving to microservices is a significant investment, in time and resources. The long-term implications must be considered: the complexities and costs discussed previously. If these are not aligned with the long-term vision of the project, the change could do more harm than good.

Continuous evaluation

Architectural decisions are not set in stone. GloboTicket will likely continue to evaluate the performance, costs and benefits of its new architecture, adapting as its needs evolve.


3.6 Conclusion of module 3

While microservices offer exciting opportunities for organizations like GloboTicket, they are not a panacea. It is crucial to adapt architectural choices to the context and specific needs of the project.

The migration to microservices is not only a technical evolution for GloboTicket, but also an organizational evolution. It promises to solve many of today’s pain points, setting the stage for a more agile, scalable and resilient future.


4. Node.js and microservices: a perfect match


4.1 What is Node.js?

Basic definition

Node.js is a runtime environment that allows you to execute JavaScript code on the server side. Simply put, it allows JavaScript — a language originally designed to run in web browsers — to run on servers.

It is essential to distinguish JavaScript (the language) from Node.js (the execution environment):

  • JavaScript is typically used for client-side scripting, which runs in the web browser to make websites interactive
  • Node.js allows this language to bypass the browser and run on a server
  • This allows developers to use JavaScript for both front-end and back-end

Why Node.js is great for microservices

Lightness: Node.js is not resource intensive, allowing it to run on different types of systems. This makes it particularly useful for microservices that often need to start quickly and remain lightweight.

Low memory footprint: This is critical for microservices which often need to be ephemeral and stateless. We can have dozens or even hundreds of instances of a microservice in parallel. If each instance consumes a lot of memory, you end up with significant operational costs and potential performance issues.

Quickstart: In microservices architecture, services may need to be dynamically scaled up and down depending on demand. The sooner a service can start, the sooner it can start serving requests. This quick start-up time can be a decisive advantage, especially during traffic peaks.

Non-blocking I/O model: Node.js can handle multiple operations simultaneously. This is excellent for microservices that may have to handle a lot of I/O operations like reading a database, an HTTP request, or accessing the file system. Although Node.js is single-threaded, it has a thread pool to delegate CPU-intensive tasks, allowing it to handle CPU-bound and I/O-bound tasks efficiently.

npm: Node.js comes with its npm package manager, offering a large ecosystem of libraries and tools. This means that you can quickly implement various features without having to build everything from scratch. For example, to quickly deploy a new ticket validation service, npm packages for rate limiting, caching, or even machine learning can significantly speed up the development cycle.

Cross-platform development: Whether on Linux, Windows or macOS, Node.js is compatible. This “build once, deploy anywhere” philosophy is essential in microservices architecture — we want services to be as agnostic as possible to the underlying infrastructure. Node.js works exceptionally well with containerization tools like Docker, which themselves are cross-platform.

Native asynchronous programming: Node.js has built-in support for asynchronous programming, ideal for I/O-bound, data-intensive, and real-time functionality. Under the hood, Node.js uses an event loop that handles asynchronous tasks, allowing a single thread to serve multiple clients simultaneously. Callbacks, promises, and async/await are all first-class citizens in the Node.js ecosystem.

Community and Corporate Support: Node.js benefits from a vibrant open source community and strong corporate support from companies like Netflix, Walmart, and LinkedIn. The Node.js Foundation oversees Long-Term Support (LTS) releases, ensuring long-term stability and viability.


4.2 npm basics

npm is an overloaded term. We can consider it from two angles:

  1. npm as command line tool: it allows you to interact with project dependencies
  2. npm as online repository: This is a large online repository where packages live. Think about a library marketplace or app store for Node.js

Install a package

Once npm is installed, adding a package to your project is as simple as:

npm install <package_name>

This command:

  • Download package from online repository
  • Add it to the node_modules folder
  • Update package.json file

The node_modules folder

Each time we install a package with npm install, the package and its dependencies are stored in the node_modules folder. Best practices:

  • Do not modify this folder manually
  • Don’t push it into version control (add to .gitignore)
  • Rely on package.json to reproduce this folder on different environments

The package.json file

The package.json file serves as the manifest of the Node.js application. It contains:

  • Project metadata (name, version)
  • The list of dependencies required for execution

Example of minimal package.json:

{
  "name": "mon-microservice",
  "version": "1.0.0",
  "description": "Un microservice exemple",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "jest": "^29.0.0"
  }
}

The package-lock.json file

When installing a package, npm also generates a package-lock.json file. This file:

  • Fix exact versions of each package and its dependencies
  • Ensures you and your team are using the same versions
  • Ensures reproducibility of builds: avoids the “it works on my machine” syndrome
  • Ensures consistency across different team members and environments

Restore dependencies

When someone wants to run the project, simply execute:

npm install

This command will read the package-lock.json and download all necessary packages to the node_modules folder.


There are hundreds of libraries in npm. Here are some popular choices for microservices:

HTTP Frameworks

FrameworkDescription
ExpressMinimal and flexible framework, very popular for Node.js services with a powerful middleware system
Nest.jsPopular choice, very complete and versatile
Seneca.jsMicroservices-specific toolkit for Node.js, allowing you to write clean and organized code, providing a rich plugin system and designed for pattern matching
MoleculateModern, fast framework designed for scalability, including an integrated registry service and native support for event-driven architectures

Logging

PackageUse
WinstonGeneral logging
PinoHigh performance logging
MorganLogging HTTP requests

Tests

PackageUse
JestComplete testing framework
MochaFlexible testing framework
JasmineBDD testing framework

4.4 Demo: synchronous vs asynchronous code

Understanding the differences between synchronous and asynchronous code is key to understanding why Node.js’ event loop architecture is one of its advantages.

Synchronous code

In synchronous code, each operation is executed sequentially, blocking the next one—nothing else can happen until that operation completes.

// Exemple synchrone
function synchronous_operation() {
  // Simule une tâche intensive en CPU
  let result = 0;
  for (let i = 0; i < 1e8; i++) {
    result += Math.log(i + 1);
  }
  return result;
}

console.log("Start");
synchronous_operation(); // Bloque jusqu'à la fin
console.log("End");

// Sortie :
// Start
// [... longue attente ...]
// End

As this is a blocking call, the “End” message will not be logged until the operation has finished executing.

Asynchronous code with callback

// Exemple asynchrone avec callback
function asynchronous_operation(callback) {
  setImmediate(() => {
    // Simule une tâche intensive en CPU (ou I/O)
    let result = 0;
    for (let i = 0; i < 1e8; i++) {
      result += Math.log(i + 1);
    }
    callback(result);
  });
}

console.log("Start");

asynchronous_operation((result) => {
  console.log("Operation completed with result:", result);
});

console.log("End");

// Sortie :
// Start
// End
// [... quelques instants plus tard ...]
// Operation completed with result: ...

How the event loop works

The setImmediate function tells Node.js to send the passed function to the event loop. It will be executed when the current task is completed.

We can imagine the event loop as a worker taking tasks from a queue. When one task is completed, he takes on the next one. When we make an asynchronous call, we place a callback in the queue to be executed in the future.

Simplification: In reality, Node.js has different queues for different types of events, each with a different priority.

In our program:

  1. We log “Start”
  2. We call the operation (asynchronous) → queuing
  3. We log “End” immediately (no blocking)
  4. The event loop is now free to take the next task
  5. The operation executes
  6. The callback is called

Importance for microservices: If this was an I/O intensive operation (waiting for data from a database), this would mean that we can continue to process other requests while we wait, and we receive a callback when finished. This is the key to high performance of Node.js for microservices.


4.5 Common APIs: REST, gRPC and GraphQL

REST (Representational State Transfer)

REST is an architectural style for designing network applications. Features :

  • HTTP as communication medium: uses GET, POST, PUT, DELETE to perform operations
  • Resource oriented: focuses on manipulating resources represented in JSON or XML
  • Stateless: each request is independent — no server-side sessions
  • Simple and versatile: easily consumable by a variety of clients (browsers, mobile devices)
// Exemple d'API REST avec Express
const express = require('express');
const app = express();

app.get('/tickets/:id', (req, res) => {
  const ticketId = req.params.id;
  // Récupérer le ticket
  res.json({ id: ticketId, event: 'Concert A', price: 75 });
});

app.post('/tickets', (req, res) => {
  // Créer un nouveau ticket
  res.status(201).json({ message: 'Ticket créé' });
});

app.listen(3000);

gRPC (Google Remote Procedure Call)

gRPC is a high-performance, language-agnostic RPC (Remote Procedure Call) framework. Features :

  • HTTP/2 for transport: optimized for low latency and high throughput
  • Protocol Buffers (protobuf): used as an interface description language — unlike JSON and XML which are untyped, gRPC is strongly typed, requiring a predefined schema which reduces problems during API changes
  • Streaming: supports streaming requests and responses
  • Multi-language: provides tools to generate client and server code in multiple languages
// Exemple de définition protobuf
syntax = "proto3";

service TicketService {
  rpc GetTicket (GetTicketRequest) returns (Ticket);
  rpc ListTickets (ListTicketsRequest) returns (stream Ticket);
}

message GetTicketRequest {
  string ticket_id = 1;
}

message Ticket {
  string id = 1;
  string event = 2;
  double price = 3;
}

GraphQL

GraphQL is a query language for APIs as well as a server-side runtime for executing queries. Features :

  • Flexible queries: clients request exactly the data they need (no more, no less) — solves REST/gRPC over-fetching and under-fetching problem
  • Strongly typed schemas: serves as a contract between client and server
  • Introspective: you can query the API schema to discover the types and operations supported
  • Single entry point: typically exposes a single HTTP endpoint for all interactions
# Exemple de requête GraphQL
query {
  ticket(id: "123") {
    id
    event
    price
  }
}

# Réponse : uniquement les champs demandés
{
  "data": {
    "ticket": {
      "id": "123",
      "event": "Concert A",
      "price": 75
    }
  }
}

Comparative summary

AppearanceRESTgRPCGraphQL
ProtocolHTTP/1.1HTTP/2HTTP
FormatJSON/XMLProtobufJSON
TypingNoStrongStrong
Query flexibilityLimitedLimitedVery flexible
StreamingNoYesLimited
MaturityVery matureGrowingGrowing

It is not uncommon for companies like GloboTicket, which use many microservices, to potentially combine these three types of APIs for different services with different needs.


5. Patterns and best practices for microservices


5.1 Introduction

In this module, we will explore the patterns and best practices associated with microservices. Having learned a lot about microservices so far, we will now look at:

  1. Observability — How to monitor and understand the system
  2. Communication — How services communicate with each other
  3. Resilience and Fault Tolerance — How to Make Services Robust
  4. Service coordination — How to coordinate actions across services
  5. Testing and Deployment — How to test and deploy with confidence
  6. Virtualization and containers — Docker, Kubernetes

5.2 Observability

Understanding what’s happening in the microservices architecture is crucial, both for daily operations and long-term maintenance.

Centralized logging

When you have multiple services running independently, each service will have its own set of logs and metrics. It is essential to use a consistent log format and aggregate logs so they can be seen in one place.

Popular tools:

  • ELK Stack (Elasticsearch, Logstash, Kibana)
  • Loki (by Grafana)
  • Fluentd
  • Splunk

Distributed Tracing

Because a single request from an external user or client will likely travel across many services and branch into multiple internal requests, it is crucial to use a unique ID to correlate all actions originating from the same initiating event.

// Exemple de middleware pour le tracing
const { v4: uuidv4 } = require('uuid');

function tracingMiddleware(req, res, next) {
  req.traceId = req.headers['x-trace-id'] || uuidv4();
  res.setHeader('x-trace-id', req.traceId);
  
  // Propagation vers les services aval
  req.traceHeaders = { 'x-trace-id': req.traceId };
  
  next();
}

Metrics

Logs are not enough to guarantee the health of the system. Effective microservices require collecting metrics across services and aggregating them into dashboards.

Examples of important metrics:

  • CPU and memory usage of different services
  • Response success rate
  • Number of messages in different queues
  • Request latency

Automated alerts

It is not practical to constantly monitor dashboards for anomalies. Different tools can provide alerts via email, Slack or SMS.

Tools for metrics and alerts:

  • Prometheus + Grafana: metrics and visualization
  • Zabbix: popular alternative for metrics
  • PagerDuty: alert management

Tools for distributed tracing:

  • Zipkin
  • Jaeger
  • Datadog (cloud service)

5.3 Communication

Communication between services is the cornerstone of any microservices architecture.

Synchronous vs asynchronous communication

As with code in Node.js, communication between services can be synchronous or asynchronous:

  • Synchronous communication: the calling service is blocking while waiting for the called party to respond

  • Analogy: telephone call — we wait for the other person’s response

  • Technologies: REST, gRPC, GraphQL

  • Asynchronous communication: the calling service continues to work while it waits

  • Analogy: e-mail — we do something else while waiting for the response

  • Technologies: message brokers, message queues

Message Brokers and Message Queues

Message brokers and queues are the most common ways to implement asynchronous communication, helping to decouple services.

Popular brokers message:

  • RabbitMQ
  • Apache Kafka

These systems manage communications between different services by providing a broker that holds the message until it can be processed by the receiver.

Advantages of message brokers:

  1. Decoupling: services do not need to know about the existence of others. For example, if GloboTicket adds a new purchase analytics service, it can simply consume the already published purchase events without other services needing to know. The alternative would be to modify many other services to notify the new service, which increases coupling.

  2. Scalability: brokers can easily be scaled to handle traffic peaks.

Queues are essentially reservoirs where messages can be stored until they can be retrieved by the intended recipient. They are durable (messages are not lost if the recipient is unavailable) and can prioritize messages according to certain criteria.

API Gateway

An API gateway acts as the entry point for clients. It routes requests to the appropriate microservice and can also handle features like:

  • Load balancing
  • Caching
  • Authentication
  • SSL Termination
  • Rate limiting
Client → API Gateway → Service de commandes
                    → Service de catalogue
                    → Service utilisateur

Best practice: It is not recommended to include business logic in the gateway.

Even if dozens or hundreds of microservices exist, clients do not need to know the specifics or manage them directly through the API gateway.


5.4 Resilience and fault tolerance

When running software on thousands of machines communicating via the network, it’s a matter of time before a service is slow or unresponsive, a computer crashes, networks are partitioned or packages are lost.

For GloboTicket, when a popular artist announces a new show, they receive a spike in traffic from customers wanting to buy tickets, and they need to be able to handle it gracefully.

Load Balancing

Distributes incoming requests across multiple servers. This is critical in microservices to ensure that no node becomes a bottleneck, thereby improving reliability and availability.

Caching

Stores copies of data that are frequently accessed and expensive to retrieve or compute. It is common for services to cache data from their own database or other services to avoid additional queries.

Caution: Caching is also a common source of problems, as the cache could contain stale data.

Timeouts and retries

If service A calls service B but does not receive a response, it could mean that B is unresponsive, crashed, or the network is slow. If A continues to wait indefinitely, it can create a domino effect.

// Exemple avec timeout et retry
const axios = require('axios');

async function callServiceWithRetry(url, maxRetries = 3) {
  let lastError;
  let delay = 1000; // 1 seconde initiale
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await axios.get(url, { timeout: 5000 }); // timeout 5s
      return response.data;
    } catch (error) {
      lastError = error;
      if (attempt < maxRetries) {
        // Stratégie de backoff exponentiel avec jitter
        const jitter = Math.random() * 500;
        await new Promise(resolve => setTimeout(resolve, delay + jitter));
        delay *= 2; // Doubler le délai à chaque tentative
      }
    }
  }
  throw lastError;
}

Important points:

  • Only idempotent operations can be retried (operations that give the same result if executed one or more times)
  • The exponential backoff strategy: double the waiting time between each attempt to avoid overloading the service
  • Random jitter: add a random value to prevent all clients from retrying at the same time (thundering herd problem)

Rate Limiter

A rate limiter ensures that when service A calls service B, it does not make more requests per second than a specified threshold, to avoid overloading B.

Circuit Breaker

A circuit breaker is a pattern that service A can use when requests to service B fail.

Operation:

  1. Closed circuit (normal): all requests pass
  2. Open circuit (too many failures): A automatically rejects all requests without calling B, allowing it to recover
  3. Half-open circuit: A lets a request pass from time to time to check if B is available again
// Exemple simplifié de circuit breaker
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF-OPEN
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF-OPEN';
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

These patterns can improve the stability of the entire system and make it more resilient to traffic spikes.


5.5 Service coordination

In monolithic systems, coordination between different parts of the system is easy, because they run in the same process, sharing memory and other resources. Consistency can be guaranteed with ACID transactions and locking mechanisms (mutexes, semaphores).

In the distributed world of microservices, locking is difficult for two reasons:

  1. Performance degradation: Locks create bottlenecks
  2. Deadlocks: multiple services requesting locks can bring the system to a halt

Distributed Transactions

Distributed transactions extend the idea of ​​local transactions, but across multiple services.

Two-phase commit

2PC is a distributed transaction protocol that allows multiple resources to agree on the commit or rollback of a transaction.

Operation:

  1. Prepare phase: the coordinator asks all participants if they can commit
  2. Commit phase: if everyone is ready, the coordinator orders the commit; otherwise, it orders the rollback

Limitations of 2PC:

  • Blocking nature: if one participant crashes after the preparation phase, the others remain blocked
  • Single point of failure: the coordinator itself can become a bottleneck
  • Lack of scalability: the more services involved, the more complex and slow the protocol is

Distributed transactions and 2PC are often considered too risky or too heavy for microservices architectures. They simply don’t align well with the principles of loose coupling and scalability that microservices aim to achieve.


5.6 The Boss Saga

To achieve consistency in microservices, we use the Saga pattern. A saga is a sequence of local transactions that updates data in a single service using ACID transactions and local locking mechanisms to achieve consistency.

Basics

  • Coordination is done with asynchronous messages to avoid 2PC issues
  • We do not want to keep a service stuck in pending state while waiting for other nodes
  • Sagas lack isolation (the “I” in ACID): multiple sagas can occur simultaneously and interfere with each other

Compensation

The way sagas handle problems and partial failures is not to lock resources and wait for all participants to commit or rollback, but rather to issue a compensatory transaction — a sort of rollback — to return the system to the desired state when the saga failed to complete.


5.7 Saga Example

Let’s illustrate the Saga pattern with a GloboTicket example. Premium users receive loyalty points and a merchandise gift with every ticket purchase.

Services involved

ServicesResponsibility
Order ServiceStores order data
User ServiceUser details (premium status or not)
Merchandise ServiceMerchandise Item Data
Payment ServicePayment details, calls to external credit card APIs
Service PointsLoyalty points management

Nominal case (Happy Path)

Time is flowing downward in this diagram:

Order Service        User Service       Merchandise Service    Payment Service    Points Service
     |                     |                    |                    |                  |
     | Crée commande        |                    |                    |                  |
     | (état: pending)      |                    |                    |                  |
     | [commit local DB]    |                    |                    |                  |
     |-------------------->|                    |                    |                  |
     |                     | Vérifie si         |                    |                  |
     |                     | l'utilisateur est  |                    |                  |
     |                     | premium            |                    |                  |
     |                     |-------------------->                    |                  |
     |                     |                    | Vérifie dispo.     |                  |
     |                     |                    | [commit: reserved] |                  |
     |                     |                    |------------------->|                  |
     |                     |                    |                    | Traite paiement  |
     |                     |                    |                    | [commit local DB]|
     |                     |                    |<-------------------|                  |
     |                     |                    | Marque: sold       |                  |
     |                     |                    | [commit local DB]  |                  |
     |                     |                    |                    |                  |-->|
     |                     |                    |                    |                  | Attribue points|
     |<---------------------------------------------------------------------------|    |
     | Met à jour commande  |                    |                    |                  |
     | (état: approved)     |                    |                    |                  |

Important points:

  • Each transaction is local to its service
  • No locks distributed between services
  • Each service “forgets” and continues processing other things

Failure Case — Credit card declined

When the payment fails, a cascade of compensatory transactions is issued:

Payment Service         Merchandise Service      Order Service
     |                          |                     |
     | Paiement refusé          |                     |
     | [commit local: échec]    |                     |
     |------------------------->|                     |
     |                          | Transaction         |
     |                          | compensatoire:      |
     |                          | reserved → available|
     |                          | [commit local DB]   |
     |                          |-------------------->|
     |                          |                     | Transaction
     |                          |                     | compensatoire:
     |                          |                     | pending → rejected
     |                          |                     | [commit local DB]

Note: In this scenario, no loyalty points were ever awarded.

In a real-world example, the saga will allow for the possibility of each step failing and all corresponding compensating transactions.


5.8 Tests

Multiple environments

Having different environments helps isolate changes and identify problems early.

Recommended minimum environments:

  1. Development: to build and test features initially
  2. Staging: mirror of production as faithful as possible
  3. Production (prod): exposure to end users

It is important to keep these environments consistent, otherwise there is a risk of bugs reaching production or test environments failing without real bugs.

Continuous Integration / Continuous Deployment (CI/CD)

CI (Continuous Integration): Changes are integrated into the main repository several times a day, and each change is automatically built and tested for bugs as early as possible.

CD (Continuous Deployment): Each change that passes testing is automatically deployed to production.

Advantages:

  • Problems are detected earlier
  • Less time wasted
  • Rollbacks are easier
  • Automation decreases the likelihood of human error

Popular tools:

  • Jenkins
  • GitLab CI/CD
  • GitHub Actions

Test Types

Performance tests: measure the responsiveness and stability of services under various conditions.

Load Testing: Check how the system responds to a large number of users. For GloboTicket, that means simulating 10,000 people buying a ticket after a big star announces a new concert.

Tools for performance testing:

  • Apache JMeter
  • Gatling
  • LoadRunner
  • K6

API Contract Testing

A contract defines the operations that a service provides. In a microservices environment, it is crucial to test that contracts between services remain valid during changes.

Consumer-Driven Contract Testing: Consumers define their expectations in contracts that producers must meet.

Popular tool: Pact


5.9 Deployment

Rolling Upgrades

Rolling upgrades allow zero downtime. Instead of stopping all instances of a service at the same time and replacing them with a new version, we do it in batches. There is a period where some instances serve queries with the old version and some with the new one.

Avant:    [v1] [v1] [v1] [v1]

Pendant:  [v2] [v1] [v1] [v1]  → [v2] [v2] [v1] [v1]  → [v2] [v2] [v2] [v1]

Après:    [v2] [v2] [v2] [v2]

Important point: during a rolling upgrade, the old and new versions of the service coexist for a time — backward compatibility of the APIs is therefore essential.

Canary Deployments

Canary deployments add an additional layer to rolling upgrades. We decide to only deploy the changes to a subset of the instances to test a new functionality in production before deploying it to all instances.

Production:  [v1 - 90%] [v1 - 90%] [v1 - 90%] | [v2 - 10%]

Si succès → Rolling upgrade complet
Si problème → Rollback seulement le canary

Feature Flags

Feature flags allow you to work on a new feature, but keep it inactive. We activate the functionality when we are ready, without requiring a new deployment.

// Exemple avec feature flags
const featureFlags = require('./featureFlags');

async function processOrder(order, userId) {
  // Traitement standard
  const result = await standardProcessing(order);
  
  // Nouvelle fonctionnalité controllée par feature flag
  if (await featureFlags.isEnabled('premium-recommendations', userId)) {
    await addPremiumRecommendations(result, userId);
  }
  
  return result;
}

Advantages of feature flags:

  • Quick activation and deactivation without redeployment
  • Ability to activate for a subset of users (early adopters, premium users)
  • Allows A/B testing

A/B Testing

A/B testing is about experimentation. We have two versions of a feature and, to decide which one to use, we deploy both, but show them to different users and compare the results. Widely used for UI changes, but also applicable on the back-end side.


5.10 Virtualization and containers

Virtualization and container technologies solve infrastructure problems and make services easier to manage and scale.

The “it works on my machine” problem

The same exact code behaves differently in one environment versus another. Indeed, the code interacts with other elements of the environment: shared libraries, different operating systems with different versions of programs, etc.

Virtualization

Virtualization allows you to create multiple simulated environments or dedicated resources from a single physical hardware system. It allows multiple operating systems to run simultaneously on a single physical machine.

Machine physique
├── Hyperviseur
│   ├── VM 1 (OS complet + App A)
│   ├── VM 2 (OS complet + App B)
│   └── VM 3 (OS complet + App C)

Advantages: better resource allocation and isolation between virtual machines

Disadvantages: Having to run an entire operating system in each VM involves overhead and costs

Containers (Docker)

Containers take the same idea, but only deliver a minimal version of the operating system that actually shares some resources with the host operating system.

Machine physique
└── OS hôte
    ├── Docker Engine
    │   ├── Container 1 (minimal OS + App A)
    │   ├── Container 2 (minimal OS + App B)
    │   └── Container 3 (minimal OS + App C)

Advantages:

  • Less overhead than a full VM
  • Lighter and faster startup
  • Portability — once it works on one machine, it works everywhere
# Exemple de Dockerfile pour un microservice Node.js
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["node", "index.js"]
# Construire et lancer un conteneur
docker build -t globoticket-order-service .
docker run -p 3000:3000 globoticket-order-service

Orchestration with Kubernetes

Managing a cluster of virtual machines or machines running containers can be difficult. Tools like Kubernetes take this to the next level by automating the distribution, scalability, and management of containerized applications.

# Exemple de deployment Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: globoticket/order-service:1.0.0
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "256Mi"
            cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Kubernetes Features:

  • Horizontal Pod Autoscaler (HPA) based on usage or other metrics
  • Automatic restart of a container if it crashes
  • Descriptive API to control cluster infrastructure
  • Integrated discovery service
  • Integrated metrics and logs
  • Native support for patterns like sidecar containers, service meshes, A/B testing

5.11 General conclusion

This course covered a wide range of topics. Here’s what you learned:

What are microservices:

  • An architecture that functionally decomposes an application into a set of independent services
  • Each service is autonomous, with its own database and communicating only via APIs

The advantages:

  • Independent scalability per service
  • Technological flexibility
  • Independent deployment and development
  • Resilience through fault isolation

The challenges:

  • Increased test complexity
  • More complex deployment
  • Distributed systems management
  • Observability
  • Data consistency (CAP theorem)

Patterns and good practices:

  • Observability: centralized logging, distributed tracing, metrics, alerts
  • Communication: REST/gRPC/GraphQL, message brokers, API gateway
  • Resilience: load balancing, caching, timeouts, retries, circuit breakers
  • Coordination: Saga pattern with compensatory transactions
  • Testing: multiple environments, CI/CD, performance tests
  • Deployment: rolling upgrades, canary deployments, feature flags, A/B testing
  • Infrastructure: Docker and Kubernetes

You now have a better map and solid foundation to explore any of the concepts or tools discussed as part of your journey with microservices and Node.js.



Search Terms

node.js · microservices · apis · backend · full-stack · web · challenge · communication · costs · deployment · distributed · globoticket · tests · advantage · asynchronous · complexity · continuous · independent · observability · synchronous · virtualization · api · benefit · cap

Interested in this course?

Contact us to book it or get a custom training plan for your team.